@nanobpm/urban 0.77.1 → 0.77.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.
@@ -17,6 +17,48 @@ export const API_BINDINGS_TS = "operations.ts";
17
17
  * `tsc` (ADR 0059). The runtime dispatches through this map instead of per-op path imports. */
18
18
  export const API_CONTROLLER_TS = "controller.ts";
19
19
  export { DEFAULT_OPERATIONS_DIR };
20
+ /** Order two import source specifiers the way Biome's `organizeImports` assist does, so the
21
+ * controller emits its delegate imports in Biome's canonical order and the generated file passes
22
+ * `biome check` without a post-generation import sort. Biome's collation is, per character:
23
+ * digit-runs compared numerically (`item2` before `item10`), letters compared case-insensitively
24
+ * as the primary key, and — when two characters are the same letter differing only in case — the
25
+ * uppercase one sorts first as an immediate tiebreak (`getAgentInstructions` before
26
+ * `getAgenticRegistry` before `getAgentSkill`). */
27
+ function naturalCompare(a, b) {
28
+ const tokens = (s) => s.match(/\d+|\D/g) ?? [];
29
+ const isDigitRun = (t) => t.charCodeAt(0) >= 48 && t.charCodeAt(0) <= 57;
30
+ const ax = tokens(a);
31
+ const bx = tokens(b);
32
+ const n = Math.min(ax.length, bx.length);
33
+ for (let i = 0; i < n; i++) {
34
+ const ca = ax[i];
35
+ const cb = bx[i];
36
+ if (ca === undefined || cb === undefined)
37
+ break;
38
+ if (isDigitRun(ca) && isDigitRun(cb)) {
39
+ // Compare digit runs by numeric value *without* floating-point loss: strip leading zeros,
40
+ // then a longer run is the larger number, else compare the equal-length digits lexically.
41
+ // (`Number(ca) - Number(cb)` collapses 17+-digit runs to the same IEEE-754 value and yields
42
+ // `NaN` beyond `Number.MAX_SAFE_INTEGER`, both of which break the total order `sort` needs.)
43
+ const na = ca.replace(/^0+/, "");
44
+ const nb = cb.replace(/^0+/, "");
45
+ if (na.length !== nb.length)
46
+ return na.length < nb.length ? -1 : 1;
47
+ if (na !== nb)
48
+ return na < nb ? -1 : 1;
49
+ if (ca !== cb)
50
+ return ca < cb ? -1 : 1; // equal value, differing leading zeros
51
+ }
52
+ else if (ca !== cb) {
53
+ const la = ca.toLowerCase();
54
+ const lb = cb.toLowerCase();
55
+ if (la !== lb)
56
+ return la < lb ? -1 : 1; // case-insensitive primary key
57
+ return ca < cb ? -1 : 1; // same letter, different case → uppercase (lower code point) first
58
+ }
59
+ }
60
+ return ax.length - bx.length;
61
+ }
20
62
  /** A TS identifier form of an operationId (used for the per-op type names). */
21
63
  function typeStem(operationId) {
22
64
  const cleaned = operationId.replace(/[^A-Za-z0-9]+/g, " ").trim();
@@ -139,7 +181,7 @@ function objectToTs(schema, depth) {
139
181
  function paramsTs(op, where, ejected) {
140
182
  const ps = op.parameters.filter((p) => p.in === where);
141
183
  if (ps.length === 0)
142
- return "{}";
184
+ return "Record<string, never>";
143
185
  const rawWire = where === "query" ? "string | string[]" : "string";
144
186
  const fields = ps.map((p) => {
145
187
  // Coerced to the schema type (number/boolean/array/enum/…); optionality is carried by the `?`
@@ -188,8 +230,7 @@ export function emitApiBindings(doc, surfaceEject = false) {
188
230
  "// The endpoint type map: each operationId maps to its typed request (coerced path/query params\n" +
189
231
  "// + JSON body) and response (a union of every documented JSON response body), derived from the\n" +
190
232
  "// spec. Do not edit — regenerated from\n" +
191
- "// the spec; erased to plain JS at compile.\n" +
192
- "// eslint-disable\n\n";
233
+ "// the spec; erased to plain JS at compile.\n\n";
193
234
  const componentDecls = Object.entries(components)
194
235
  .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
195
236
  .map(([name, schema]) => `export type ${typeStem(name)} = ${schemaToTs(schema)};`)
@@ -242,13 +283,19 @@ export function emitApiBindings(doc, surfaceEject = false) {
242
283
  return ` ${JSON.stringify(op.operationId)}: { request: ${stem}Request; response: ${stem}Response };`;
243
284
  })
244
285
  .join("\n");
286
+ // With generated `.ts` now linted in the scaffold, an empty operation set must NOT emit an empty
287
+ // `interface ApiOperations {}` (`suspicious/noEmptyInterface`); fall back to the same non-empty-safe
288
+ // `Record<never, never>` used elsewhere (its `keyof` is `never`, matching `ApiOperationId = never`).
289
+ const apiOperations = ops.length > 0
290
+ ? `export interface ApiOperations {\n${mapEntries}\n}\n`
291
+ : `export type ApiOperations = Record<never, never>;\n`;
245
292
  return (header +
246
293
  (componentDecls ? `${componentDecls}\n\n` : "") +
247
294
  (opDecls ? `${opDecls}\n\n` : "") +
248
295
  `/** Every declared operationId (ADR 0058): the delegate key set. */\n` +
249
296
  `export type ApiOperationId = ${idUnion};\n\n` +
250
297
  `/** Request/response contract per operationId. */\n` +
251
- `export interface ApiOperations {\n${mapEntries}\n}\n`);
298
+ apiOperations);
252
299
  }
253
300
  /** Emit `operations.ts`: the typed `defineOperation` wrapper, keyed by operationId (mirrors the
254
301
  * generated typed `defineWorker`). Written verbatim (it never changes with the spec) — it only
@@ -257,10 +304,9 @@ export function emitApiBindingsRuntime() {
257
304
  return ("// AUTO-GENERATED by @nanobpm/urban (ADR 0058): the typed `defineOperation`.\n" +
258
305
  "// Re-exports the operation SDK and overrides `defineOperation` with an operationId-keyed\n" +
259
306
  "// signature (the delegate's validated request + result typed from the spec). Erased to a\n" +
260
- "// pass-through at runtime. Do not edit.\n" +
261
- "// eslint-disable\n\n" +
262
- `import { defineOperation as defineOperationRaw } from "@nanobpm/urban";\n` +
307
+ "// pass-through at runtime. Do not edit.\n\n" +
263
308
  `import type { OperationHandler } from "@nanobpm/urban";\n` +
309
+ `import { defineOperation as defineOperationRaw } from "@nanobpm/urban";\n` +
264
310
  `import type { ApiOperationId, ApiOperations } from "./${API_BINDINGS_DTS}";\n\n` +
265
311
  `type ReqFor<K extends ApiOperationId> = ApiOperations[K]["request"];\n` +
266
312
  `type ResFor<K extends ApiOperationId> = ApiOperations[K]["response"];\n\n` +
@@ -303,17 +349,22 @@ export function emitApiController(doc, operationsDir = DEFAULT_OPERATIONS_DIR) {
303
349
  "// registry. Statically imports every operation delegate and asserts the complete, correctly-\n" +
304
350
  "// typed set via `satisfies` — a missing, mismatched, or orphan delegate fails `tsc`. The\n" +
305
351
  "// runtime dispatches through this map, so endpoints cannot drift from the spec. Do not edit —\n" +
306
- "// regenerated from the spec.\n" +
307
- "// eslint-disable\n\n";
308
- const typeImports = `import type { OperationHandler } from "@nanobpm/urban";\n` +
309
- `import type { ApiOperationId, ApiOperations } from "./${API_BINDINGS_DTS}";\n`;
352
+ "// regenerated from the spec.\n\n";
353
+ // Emit imports in Biome's canonical `organizeImports` order so the generated file is lint-clean
354
+ // without a post-gen fixup: the bare package import first, then the delegate imports sorted by
355
+ // source specifier (Biome's natural, numeric-aware collation), then the relative `./api-io.d.ts`
356
+ // type import last. Alias bindings (`op<i>`) stay tied to the original index so the `operations`
357
+ // map below still resolves each operationId to its delegate regardless of import line order.
310
358
  const delegateImports = ops
311
- .map((op, i) => `import op${i} from ${JSON.stringify(`../${dir}/${op.operationId}.ts`)};`)
359
+ .map((op, i) => ({ i, spec: `../${dir}/${op.operationId}.ts` }))
360
+ .sort((a, b) => naturalCompare(a.spec, b.spec))
361
+ .map(({ i, spec }) => `import op${i} from ${JSON.stringify(spec)};`)
312
362
  .join("\n");
313
363
  const mapEntries = ops.map((op, i) => ` ${JSON.stringify(op.operationId)}: op${i},`).join("\n");
314
364
  return (header +
315
- typeImports +
365
+ `import type { OperationHandler } from "@nanobpm/urban";\n` +
316
366
  (delegateImports ? `${delegateImports}\n` : "") +
367
+ `import type { ApiOperationId, ApiOperations } from "./${API_BINDINGS_DTS}";\n` +
317
368
  `\n` +
318
369
  `type ReqFor<K extends ApiOperationId> = ApiOperations[K]["request"];\n` +
319
370
  `type ResFor<K extends ApiOperationId> = ApiOperations[K]["response"];\n\n` +
@@ -83,10 +83,9 @@ function tableInterface(t) {
83
83
  */
84
84
  export function emitDomainDts(tables) {
85
85
  const header = "// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\n" +
86
- "// Do not edit — regenerated from the live tables. Erased at `deno compile`.\n" +
87
- "// eslint-disable\n";
86
+ "// Do not edit — regenerated from the live tables. Erased at `deno compile`.\n";
88
87
  if (tables.length === 0) {
89
- return `${header}\nexport interface DomainTables {}\n`;
88
+ return `${header}\nexport type DomainTables = Record<never, never>;\n`;
90
89
  }
91
90
  const interfaces = tables.map(tableInterface).join("\n\n");
92
91
  const map = tables
@@ -113,15 +112,14 @@ export function emitDomainDtsForSources(sources, defaultSource) {
113
112
  return emitDomainDts(sources[0]?.tables ?? []);
114
113
  }
115
114
  const header = "// AUTO-GENERATED by nanobpmn from the datasource schemas (ADR 0029 §4.1/§6).\n" +
116
- "// Do not edit — regenerated from the live tables. Erased at `deno compile`.\n" +
117
- "// eslint-disable\n";
115
+ "// Do not edit — regenerated from the live tables. Erased at `deno compile`.\n";
118
116
  const interfaces = sources
119
117
  .flatMap((s) => s.tables.map((t) => tableInterfaceNamed(prefixedInterfaceName(s.source, t.name), t)))
120
118
  .join("\n\n");
121
119
  const sourceMap = sources
122
120
  .map((s) => {
123
121
  if (s.tables.length === 0)
124
- return ` ${JSON.stringify(s.source)}: {};`;
122
+ return ` ${JSON.stringify(s.source)}: Record<never, never>;`;
125
123
  const tableMap = s.tables
126
124
  .map((t) => ` ${JSON.stringify(t.name)}: ${prefixedInterfaceName(s.source, t.name)};`)
127
125
  .join("\n");
@@ -181,7 +179,7 @@ export function emitDomainTypeRegistry(types) {
181
179
  return ` ${key}${opt}: ${fieldTsType(field, ids)};`;
182
180
  })
183
181
  .join("\n");
184
- const body = fields.length > 0 ? `{\n${fields}\n }` : "{}";
182
+ const body = fields.length > 0 ? `{\n${fields}\n }` : "Record<never, never>";
185
183
  return ` ${JSON.stringify(id)}: ${body};`;
186
184
  })
187
185
  .join("\n");
@@ -233,8 +231,7 @@ export function emitDomainBindings(sources, defaultSource) {
233
231
  const header = "// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1/§6).\n" +
234
232
  "// The typed data-object layer: `openDomain()` → one `Table<T>` per table, so\n" +
235
233
  "// workers manipulate typed rows instead of hand-writing SQL. Do not edit —\n" +
236
- "// regenerated from the live tables. Erased to plain JS at `deno compile`.\n" +
237
- "// eslint-disable\n";
234
+ "// regenerated from the live tables. Erased to plain JS at `deno compile`.\n";
238
235
  const multi = sources.length > 1;
239
236
  const def = defaultSource && sources.some((s) => s.source === defaultSource)
240
237
  ? defaultSource
@@ -279,7 +276,7 @@ export function emitDomainBindings(sources, defaultSource) {
279
276
  (binds ? `${binds}\n` : "") +
280
277
  ` close: () => raw.close(),\n };\n}`;
281
278
  return `${header}\n` +
282
- `import { openDataSource, type DataSource, type Table } from "./data-sdk.ts";\n` +
279
+ `import { type DataSource, openDataSource, type Table } from "./data-sdk.ts";\n` +
283
280
  importTypes +
284
281
  `\n${domainIface}\n\n${openFn}\n\nexport type { Table };\n`;
285
282
  }
@@ -323,13 +320,14 @@ function emitKeyedDomainBindings(header, sources, def) {
323
320
  `export async function openDomain<K extends DomainSource = ${JSON.stringify(def)}>(\n` +
324
321
  ` source?: K,\n` +
325
322
  `): Promise<Domain<DomainSources[K]>> {\n` +
326
- ` const key = (source ?? DEFAULT_SOURCE) as string;\n` +
323
+ ` const key = source ?? DEFAULT_SOURCE;\n` +
327
324
  ` const raw = await openDataSource(key);\n` +
328
325
  ` const db: Record<string, unknown> = { raw, close: () => raw.close() };\n` +
329
326
  ` for (const t of DOMAIN_TABLES[key] ?? []) db[t.name] = raw.table(t.name, t.pk);\n` +
327
+ ` // biome-ignore lint/plugin: the per-source table set is bound dynamically from DOMAIN_TABLES; the Domain<M> shape can't be expressed statically over the runtime loop.\n` +
330
328
  ` return db as Domain<DomainSources[K]>;\n}`;
331
329
  return `${header}\n` +
332
- `import { openDataSource, type DataSource, type Table } from "./data-sdk.ts";\n` +
330
+ `import { type DataSource, openDataSource, type Table } from "./data-sdk.ts";\n` +
333
331
  `import type { DomainSources } from "./${DOMAIN_DTS}";\n\n` +
334
332
  `${runtimeMap}\n\n${types}\n\n${openFn}\n\nexport type { Table };\n`;
335
333
  }
@@ -79,15 +79,14 @@ export function emitMessageBindings(messages, declaredTypeIds) {
79
79
  "// The bridge from a `bpmn:message`'s data envelope to the message type system:\n" +
80
80
  "// each named message maps to the TS type of its payload, so the typed\n" +
81
81
  "// `publishMessage` types a call by its message name. Do not edit — regenerated\n" +
82
- "// from the model. Erased to plain JS at compile.\n" +
83
- "// eslint-disable\n";
82
+ "// from the model. Erased to plain JS at compile.\n";
84
83
  const needsRegistry = payloads.length > 0;
85
84
  const importTypes = needsRegistry
86
85
  ? `import type { DomainTypes } from "./${DOMAIN_DTS}";\n`
87
86
  : "";
88
87
  const payloadsIface = payloads.length > 0
89
88
  ? `export interface MessagePayloads {\n${payloads.join("\n")}\n}\n`
90
- : `export interface MessagePayloads {}\n`;
89
+ : `export type MessagePayloads = Record<never, never>;\n`;
91
90
  return `${header}\n` +
92
91
  importTypes +
93
92
  `\n/** Untyped fallback payload for a message that declares no envelope type. */\n` +
@@ -149,11 +148,10 @@ export function emitMessageBindingsRuntime() {
149
148
  return "// AUTO-GENERATED by nanobpmn (ADR 0040 slice 2): the typed `publishMessage`.\n" +
150
149
  "// Re-exports the worker SDK's `publishMessage` and overrides it with a\n" +
151
150
  "// messageName-keyed typed signature (variables typed from the message's declared\n" +
152
- "// envelope). Erased to a pass-through at runtime. Do not edit.\n" +
153
- "// eslint-disable\n\n" +
154
- `import { publishMessage as publishMessageRaw } from "./worker-sdk.ts";\n` +
151
+ "// envelope). Erased to a pass-through at runtime. Do not edit.\n\n" +
152
+ `import type { MessageName, MessagePayloads, MessageVars } from "./${MESSAGE_BINDINGS_DTS}";\n` +
155
153
  `import type { PublishMessageOptions, PublishMessageResult } from "./worker-sdk.ts";\n` +
156
- `import type { MessageName, MessagePayloads, MessageVars } from "./${MESSAGE_BINDINGS_DTS}";\n\n` +
154
+ `import { publishMessage as publishMessageRaw } from "./worker-sdk.ts";\n\n` +
157
155
  `export type { PublishMessageOptions, PublishMessageResult } from "./worker-sdk.ts";\n\n` +
158
156
  `type PayloadFor<K extends MessageName> = K extends keyof MessagePayloads ? MessagePayloads[K] : MessageVars;\n\n` +
159
157
  `/**\n` +
@@ -166,6 +164,7 @@ export function emitMessageBindingsRuntime() {
166
164
  ` name: K,\n` +
167
165
  ` opts?: PublishMessageOptions<PayloadFor<K> & object>,\n` +
168
166
  `): Promise<PublishMessageResult> {\n` +
167
+ ` // biome-ignore lint/plugin: generic-to-raw erasure at the message-SDK boundary; the wire stays untyped JSON (ADR 0040 slice 2).\n` +
169
168
  ` return publishMessageRaw(name, opts as unknown as PublishMessageOptions);\n` +
170
169
  `}\n`;
171
170
  }
@@ -67,23 +67,25 @@ export function emitMeta(metas) {
67
67
  const header = "// AUTO-GENERATED by nanobpmn from the process model (ADR 0040 §5): the typed\n" +
68
68
  "// model-metadata accessor. Each `nano:meta` key/value the models declare becomes\n" +
69
69
  "// a checked `AppMeta` property, so `meta(key)` is typed and app-wide queryable\n" +
70
- "// without a second store. Do not edit — regenerated from the model.\n" +
71
- "// eslint-disable\n\n";
70
+ "// without a second store. Do not edit — regenerated from the model.\n\n";
72
71
  const iface = keys.length > 0
73
72
  ? `export interface AppMeta {\n${keys.map((k) => ` ${propKey(k)}: string;`).join("\n")}\n}\n`
74
- : `export interface AppMeta {}\n`;
73
+ : `export type AppMeta = Record<never, never>;\n`;
74
+ // Dot-access satisfies Biome's `useLiteralKeys`, but `__proto__` must stay a computed key: dot
75
+ // access would trip `noProto` and (off a null-prototype dict) change semantics.
76
+ const assignTarget = (k) => isIdent(k) && k !== "__proto__" ? `m.${k}` : `m[${JSON.stringify(k)}]`;
75
77
  const constDecl = keys.length > 0
76
78
  ? `export const appMeta: AppMeta = (() => {\n` +
77
- ` const m: Record<string, string> = Object.create(null);\n` +
78
- keys.map((k) => ` m[${JSON.stringify(k)}] = ${JSON.stringify(folded[k])};`).join("\n") +
79
- `\n return m as AppMeta;\n})();\n`
80
- : `export const appMeta: AppMeta = Object.create(null) as AppMeta;\n`;
79
+ ` const m: AppMeta = Object.create(null);\n` +
80
+ keys.map((k) => ` ${assignTarget(k)} = ${JSON.stringify(folded[k])};`).join("\n") +
81
+ `\n return m;\n})();\n`
82
+ : `export const appMeta: AppMeta = Object.create(null);\n`;
81
83
  const accessor = `\n/** Read a model-level metadata value. A declared key is typed; a dynamic key\n` +
82
84
  ` * returns \`string | undefined\`. */\n` +
83
85
  `export function meta<K extends keyof AppMeta>(key: K): AppMeta[K];\n` +
84
86
  `export function meta(key: string): string | undefined;\n` +
85
87
  `export function meta(key: string): string | undefined {\n` +
86
- ` return (appMeta as Record<string, string>)[key];\n` +
88
+ ` return Reflect.get(appMeta, key);\n` +
87
89
  `}\n`;
88
90
  return `${header}/** Every model-level metadata key the App's models declare (ADR 0040 §5). */\n` +
89
91
  iface + "\n" + constDecl + accessor;
@@ -143,21 +143,20 @@ export function emitWorkerBindings(workers, declaredTypeIds) {
143
143
  "// The bridge from the process model to the worker type system: each declared\n" +
144
144
  "// worker's `taskType` maps to the TS type of its input payload (`job.variables`)\n" +
145
145
  "// and result, so the typed `defineWorker` types a handler by its job type. Do\n" +
146
- "// not edit — regenerated from the manifest. Erased to plain JS at compile.\n" +
147
- "// eslint-disable\n";
146
+ "// not edit — regenerated from the manifest. Erased to plain JS at compile.\n";
148
147
  const needsRegistry = inputs.length > 0 || outputs.length > 0;
149
148
  const importTypes = needsRegistry
150
149
  ? `import type { DomainTypes } from "./${DOMAIN_DTS}";\n`
151
150
  : "";
152
151
  const inputsIface = inputs.length > 0
153
152
  ? `export interface WorkerInputs {\n${inputs.join("\n")}\n}\n`
154
- : `export interface WorkerInputs {}\n`;
153
+ : `export type WorkerInputs = Record<never, never>;\n`;
155
154
  const outputsIface = outputs.length > 0
156
155
  ? `export interface WorkerOutputs {\n${outputs.join("\n")}\n}\n`
157
- : `export interface WorkerOutputs {}\n`;
156
+ : `export type WorkerOutputs = Record<never, never>;\n`;
158
157
  const headersIface = headers.length > 0
159
158
  ? `export interface WorkerHeaders {\n${headers.join("\n")}\n}\n`
160
- : `export interface WorkerHeaders {}\n`;
159
+ : `export type WorkerHeaders = Record<never, never>;\n`;
161
160
  return `${header}\n` +
162
161
  importTypes +
163
162
  `\n/** Untyped fallback for a job whose worker declares no input/output type. */\n` +
@@ -315,11 +314,10 @@ export function emitWorkerBindingsRuntime() {
315
314
  "// Re-exports the worker SDK and overrides `defineWorker` with a taskType-keyed\n" +
316
315
  "// typed signature (job.variables, job.customHeaders + result typed from the\n" +
317
316
  "// worker's declared input/output/header contract). Erased to a pass-through at\n" +
318
- "// runtime. Do not edit.\n" +
319
- "// eslint-disable\n\n" +
320
- `import { defineWorker as defineWorkerRaw } from "./worker-sdk.ts";\n` +
317
+ "// runtime. Do not edit.\n\n" +
318
+ `import type { WorkerHdrs, WorkerHeaders, WorkerInputs, WorkerOutputs, WorkerTaskType, WorkerVars } from "./${WORKER_BINDINGS_DTS}";\n` +
321
319
  `import type { WorkerOptions } from "./worker-sdk.ts";\n` +
322
- `import type { WorkerInputs, WorkerOutputs, WorkerHeaders, WorkerTaskType, WorkerVars, WorkerHdrs } from "./${WORKER_BINDINGS_DTS}";\n\n` +
320
+ `import { defineWorker as defineWorkerRaw } from "./worker-sdk.ts";\n\n` +
323
321
  `export * from "./worker-sdk.ts";\n\n` +
324
322
  `type InFor<K extends WorkerTaskType> = K extends keyof WorkerInputs ? WorkerInputs[K] : WorkerVars;\n` +
325
323
  `type OutFor<K extends WorkerTaskType> = K extends keyof WorkerOutputs ? WorkerOutputs[K] : WorkerVars;\n` +
@@ -334,6 +332,7 @@ export function emitWorkerBindingsRuntime() {
334
332
  `export function defineWorker<K extends WorkerTaskType>(\n` +
335
333
  ` opts: { type: K } & WorkerOptions<InFor<K> & object, OutFor<K> & object, HdrFor<K> & object>,\n` +
336
334
  `): void {\n` +
335
+ ` // biome-ignore lint/plugin: generic-to-raw erasure at the worker-SDK boundary; the wire stays untyped JSON (ADR 0033 §3).\n` +
337
336
  ` defineWorkerRaw(opts as unknown as WorkerOptions);\n` +
338
337
  `}\n`;
339
338
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.77.1",
3
+ "version": "0.77.2",
4
4
  "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -103,7 +103,7 @@
103
103
  "@nanobpm/nano-sdk": "^1.2.5",
104
104
  "@nanobpm/workflow": "^0.14.0",
105
105
  "bpmn-auto-layout": "^2.0.0-alpha.2",
106
- "create-urban-app": "^0.15.0",
106
+ "create-urban-app": "^0.15.1",
107
107
  "yaml": "^2.9.0"
108
108
  },
109
109
  "devDependencies": {
@@ -108,6 +108,34 @@ test("emitApiController on an empty spec is a valid empty registry (never = {})"
108
108
  assert.match(out, /export const operations = \{\s*\} satisfies \{/);
109
109
  });
110
110
 
111
+ test("emitApiBindings on an empty spec emits the Record<never, never> ApiOperations fallback (not an empty interface)", () => {
112
+ const empty: OpenApiDoc = { openapi: "3.0.0", paths: {} };
113
+ const out = emitApiBindings(empty);
114
+ assert.match(out, /export type ApiOperationId = never;/);
115
+ // A generated `.ts` file is now linted in the scaffold, where `interface ApiOperations {}` would
116
+ // trip `suspicious/noEmptyInterface`; the empty spec must degrade to the non-empty-safe alias.
117
+ assert.match(out, /export type ApiOperations = Record<never, never>;/);
118
+ assert.doesNotMatch(out, /interface ApiOperations/);
119
+ });
120
+
121
+ test("emitApiController orders imports by true numeric value for digit runs beyond IEEE-754 precision", () => {
122
+ // 16- vs 17-digit runs: `Number()` collapses these to the same float (and can yield NaN), so a
123
+ // float-subtraction sort would treat them as equal and leave order to chance. The 17-digit run is
124
+ // numerically larger and must sort last regardless.
125
+ const doc: OpenApiDoc = {
126
+ openapi: "3.0.0",
127
+ paths: {
128
+ "/b": { get: { operationId: "op10000000000000001", responses: { "200": {} } } },
129
+ "/a": { get: { operationId: "op9999999999999999", responses: { "200": {} } } },
130
+ },
131
+ };
132
+ const out = emitApiController(doc, "handlers");
133
+ const iShort = out.indexOf("op9999999999999999.ts");
134
+ const iLong = out.indexOf("op10000000000000001.ts");
135
+ assert.ok(iShort >= 0 && iLong >= 0, out);
136
+ assert.ok(iShort < iLong, `16-digit run must sort before the larger 17-digit run:\n${out}`);
137
+ });
138
+
111
139
  test("emitApiController throws on a duplicate operationId (would silently overwrite a map key)", () => {
112
140
  const dupe: OpenApiDoc = {
113
141
  openapi: "3.0.0",
@@ -34,6 +34,44 @@ export const API_CONTROLLER_TS = "controller.ts";
34
34
  export { DEFAULT_OPERATIONS_DIR };
35
35
 
36
36
 
37
+ /** Order two import source specifiers the way Biome's `organizeImports` assist does, so the
38
+ * controller emits its delegate imports in Biome's canonical order and the generated file passes
39
+ * `biome check` without a post-generation import sort. Biome's collation is, per character:
40
+ * digit-runs compared numerically (`item2` before `item10`), letters compared case-insensitively
41
+ * as the primary key, and — when two characters are the same letter differing only in case — the
42
+ * uppercase one sorts first as an immediate tiebreak (`getAgentInstructions` before
43
+ * `getAgenticRegistry` before `getAgentSkill`). */
44
+ function naturalCompare(a: string, b: string): number {
45
+ const tokens = (s: string): string[] => s.match(/\d+|\D/g) ?? [];
46
+ const isDigitRun = (t: string): boolean => t.charCodeAt(0) >= 48 && t.charCodeAt(0) <= 57;
47
+ const ax = tokens(a);
48
+ const bx = tokens(b);
49
+ const n = Math.min(ax.length, bx.length);
50
+ for (let i = 0; i < n; i++) {
51
+ const ca = ax[i];
52
+ const cb = bx[i];
53
+ if (ca === undefined || cb === undefined) break;
54
+ if (isDigitRun(ca) && isDigitRun(cb)) {
55
+ // Compare digit runs by numeric value *without* floating-point loss: strip leading zeros,
56
+ // then a longer run is the larger number, else compare the equal-length digits lexically.
57
+ // (`Number(ca) - Number(cb)` collapses 17+-digit runs to the same IEEE-754 value and yields
58
+ // `NaN` beyond `Number.MAX_SAFE_INTEGER`, both of which break the total order `sort` needs.)
59
+ const na = ca.replace(/^0+/, "");
60
+ const nb = cb.replace(/^0+/, "");
61
+ if (na.length !== nb.length) return na.length < nb.length ? -1 : 1;
62
+ if (na !== nb) return na < nb ? -1 : 1;
63
+ if (ca !== cb) return ca < cb ? -1 : 1; // equal value, differing leading zeros
64
+ } else if (ca !== cb) {
65
+ const la = ca.toLowerCase();
66
+ const lb = cb.toLowerCase();
67
+ if (la !== lb) return la < lb ? -1 : 1; // case-insensitive primary key
68
+ return ca < cb ? -1 : 1; // same letter, different case → uppercase (lower code point) first
69
+ }
70
+ }
71
+ return ax.length - bx.length;
72
+ }
73
+
74
+
37
75
  /** A TS identifier form of an operationId (used for the per-op type names). */
38
76
  function typeStem(operationId: string): string {
39
77
  const cleaned = operationId.replace(/[^A-Za-z0-9]+/g, " ").trim();
@@ -156,7 +194,7 @@ function objectToTs(schema: OpenApiSchema, depth: number): string {
156
194
  * not enforce presence for ejected ops, so a "required" query param may in fact be absent. */
157
195
  function paramsTs(op: OperationInfo, where: "path" | "query", ejected: boolean): string {
158
196
  const ps = op.parameters.filter((p) => p.in === where);
159
- if (ps.length === 0) return "{}";
197
+ if (ps.length === 0) return "Record<string, never>";
160
198
  const rawWire = where === "query" ? "string | string[]" : "string";
161
199
  const fields = ps.map((p) => {
162
200
  // Coerced to the schema type (number/boolean/array/enum/…); optionality is carried by the `?`
@@ -211,8 +249,7 @@ export function emitApiBindings(doc: OpenApiDoc, surfaceEject = false): string {
211
249
  "// The endpoint type map: each operationId maps to its typed request (coerced path/query params\n" +
212
250
  "// + JSON body) and response (a union of every documented JSON response body), derived from the\n" +
213
251
  "// spec. Do not edit — regenerated from\n" +
214
- "// the spec; erased to plain JS at compile.\n" +
215
- "// eslint-disable\n\n";
252
+ "// the spec; erased to plain JS at compile.\n\n";
216
253
 
217
254
  const componentDecls = Object.entries(components)
218
255
  .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
@@ -272,6 +309,13 @@ export function emitApiBindings(doc: OpenApiDoc, surfaceEject = false): string {
272
309
  })
273
310
  .join("\n");
274
311
 
312
+ // With generated `.ts` now linted in the scaffold, an empty operation set must NOT emit an empty
313
+ // `interface ApiOperations {}` (`suspicious/noEmptyInterface`); fall back to the same non-empty-safe
314
+ // `Record<never, never>` used elsewhere (its `keyof` is `never`, matching `ApiOperationId = never`).
315
+ const apiOperations = ops.length > 0
316
+ ? `export interface ApiOperations {\n${mapEntries}\n}\n`
317
+ : `export type ApiOperations = Record<never, never>;\n`;
318
+
275
319
  return (
276
320
  header +
277
321
  (componentDecls ? `${componentDecls}\n\n` : "") +
@@ -279,7 +323,7 @@ export function emitApiBindings(doc: OpenApiDoc, surfaceEject = false): string {
279
323
  `/** Every declared operationId (ADR 0058): the delegate key set. */\n` +
280
324
  `export type ApiOperationId = ${idUnion};\n\n` +
281
325
  `/** Request/response contract per operationId. */\n` +
282
- `export interface ApiOperations {\n${mapEntries}\n}\n`
326
+ apiOperations
283
327
  );
284
328
  }
285
329
 
@@ -291,10 +335,9 @@ export function emitApiBindingsRuntime(): string {
291
335
  "// AUTO-GENERATED by @nanobpm/urban (ADR 0058): the typed `defineOperation`.\n" +
292
336
  "// Re-exports the operation SDK and overrides `defineOperation` with an operationId-keyed\n" +
293
337
  "// signature (the delegate's validated request + result typed from the spec). Erased to a\n" +
294
- "// pass-through at runtime. Do not edit.\n" +
295
- "// eslint-disable\n\n" +
296
- `import { defineOperation as defineOperationRaw } from "@nanobpm/urban";\n` +
338
+ "// pass-through at runtime. Do not edit.\n\n" +
297
339
  `import type { OperationHandler } from "@nanobpm/urban";\n` +
340
+ `import { defineOperation as defineOperationRaw } from "@nanobpm/urban";\n` +
298
341
  `import type { ApiOperationId, ApiOperations } from "./${API_BINDINGS_DTS}";\n\n` +
299
342
  `type ReqFor<K extends ApiOperationId> = ApiOperations[K]["request"];\n` +
300
343
  `type ResFor<K extends ApiOperationId> = ApiOperations[K]["response"];\n\n` +
@@ -342,23 +385,26 @@ export function emitApiController(doc: OpenApiDoc, operationsDir: string = DEFAU
342
385
  "// registry. Statically imports every operation delegate and asserts the complete, correctly-\n" +
343
386
  "// typed set via `satisfies` — a missing, mismatched, or orphan delegate fails `tsc`. The\n" +
344
387
  "// runtime dispatches through this map, so endpoints cannot drift from the spec. Do not edit —\n" +
345
- "// regenerated from the spec.\n" +
346
- "// eslint-disable\n\n";
347
-
348
- const typeImports =
349
- `import type { OperationHandler } from "@nanobpm/urban";\n` +
350
- `import type { ApiOperationId, ApiOperations } from "./${API_BINDINGS_DTS}";\n`;
388
+ "// regenerated from the spec.\n\n";
351
389
 
390
+ // Emit imports in Biome's canonical `organizeImports` order so the generated file is lint-clean
391
+ // without a post-gen fixup: the bare package import first, then the delegate imports sorted by
392
+ // source specifier (Biome's natural, numeric-aware collation), then the relative `./api-io.d.ts`
393
+ // type import last. Alias bindings (`op<i>`) stay tied to the original index so the `operations`
394
+ // map below still resolves each operationId to its delegate regardless of import line order.
352
395
  const delegateImports = ops
353
- .map((op, i) => `import op${i} from ${JSON.stringify(`../${dir}/${op.operationId}.ts`)};`)
396
+ .map((op, i) => ({ i, spec: `../${dir}/${op.operationId}.ts` }))
397
+ .sort((a, b) => naturalCompare(a.spec, b.spec))
398
+ .map(({ i, spec }) => `import op${i} from ${JSON.stringify(spec)};`)
354
399
  .join("\n");
355
400
 
356
401
  const mapEntries = ops.map((op, i) => ` ${JSON.stringify(op.operationId)}: op${i},`).join("\n");
357
402
 
358
403
  return (
359
404
  header +
360
- typeImports +
405
+ `import type { OperationHandler } from "@nanobpm/urban";\n` +
361
406
  (delegateImports ? `${delegateImports}\n` : "") +
407
+ `import type { ApiOperationId, ApiOperations } from "./${API_BINDINGS_DTS}";\n` +
362
408
  `\n` +
363
409
  `type ReqFor<K extends ApiOperationId> = ApiOperations[K]["request"];\n` +
364
410
  `type ResFor<K extends ApiOperationId> = ApiOperations[K]["response"];\n\n` +
@@ -7,7 +7,7 @@ import { emitMessageBindings, emitMessageBindingsRuntime } from "./messages.ts";
7
7
  // Byte-exact goldens for the emitters ported into the toolkit for the domaintypes op (host dry-out
8
8
  // nano-bpm#576). Captured verbatim from the console's own emitters via Deno; these lock the toolkit
9
9
  // output to the console byte-for-byte so the two can never drift while both exist.
10
- const GOLDENS: Record<string, string> = JSON.parse("{\"bindings\":\"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1/§6).\\n// The typed data-object layer: `openDomain()` → one `Table<T>` per table, so\\n// workers manipulate typed rows instead of hand-writing SQL. Do not edit —\\n// regenerated from the live tables. Erased to plain JS at `deno compile`.\\n// eslint-disable\\n\\nimport { openDataSource, type DataSource, type Table } from \\\"./data-sdk.ts\\\";\\nimport type { Customers, Orders } from \\\"./domain-rows.d.ts\\\";\\n\\nexport interface Domain {\\n /** The underlying datasource handle — the raw-SQL escape hatch. */\\n readonly raw: DataSource;\\n readonly orders: Table<Orders>;\\n readonly customers: Table<Customers>;\\n /** Close the underlying connection. */\\n close(): void;\\n}\\n\\n/**\\n * Open the App's datasource as a typed domain: each table is\\n * a `Table<T>` gateway (`db.orders.insert(...)`, `db.orders.get(id)`), with the\\n * raw `DataSource` at `db.raw` for anything the gateway doesn't cover.\\n */\\nexport async function openDomain(source?: string): Promise<Domain> {\\n const raw = await openDataSource(source);\\n return {\\n raw,\\n orders: raw.table<Orders>(\\\"orders\\\", \\\"id\\\"),\\n customers: raw.table<Customers>(\\\"customers\\\", \\\"id\\\"),\\n close: () => raw.close(),\\n };\\n}\\n\\nexport type { Table };\\n\",\"workerRuntime\":\"// AUTO-GENERATED by nanobpmn (ADR 0033 §3): the typed `defineWorker`.\\n// Re-exports the worker SDK and overrides `defineWorker` with a taskType-keyed\\n// typed signature (job.variables, job.customHeaders + result typed from the\\n// worker's declared input/output/header contract). Erased to a pass-through at\\n// runtime. Do not edit.\\n// eslint-disable\\n\\nimport { defineWorker as defineWorkerRaw } from \\\"./worker-sdk.ts\\\";\\nimport type { WorkerOptions } from \\\"./worker-sdk.ts\\\";\\nimport type { WorkerInputs, WorkerOutputs, WorkerHeaders, WorkerTaskType, WorkerVars, WorkerHdrs } from \\\"./worker-io.d.ts\\\";\\n\\nexport * from \\\"./worker-sdk.ts\\\";\\n\\ntype InFor<K extends WorkerTaskType> = K extends keyof WorkerInputs ? WorkerInputs[K] : WorkerVars;\\ntype OutFor<K extends WorkerTaskType> = K extends keyof WorkerOutputs ? WorkerOutputs[K] : WorkerVars;\\ntype HdrFor<K extends WorkerTaskType> = K extends keyof WorkerHeaders ? WorkerHeaders[K] : WorkerHdrs;\\n\\n/**\\n * Typed `defineWorker`: `type` is constrained to the model's declared job types\\n * (`WorkerTaskType`, ADR 0033 §3) so it autocompletes and rejects unknown jobs,\\n * and the handler's `job.variables`, `job.customHeaders` + result are typed from\\n * the worker's declared `inputType`/`outputType`/header keys. A declared job type\\n * with no declared type falls back to WorkerVars / WorkerHdrs.\\n */\\nexport function defineWorker<K extends WorkerTaskType>(\\n opts: { type: K } & WorkerOptions<InFor<K> & object, OutFor<K> & object, HdrFor<K> & object>,\\n): void {\\n defineWorkerRaw(opts as unknown as WorkerOptions);\\n}\\n\",\"messageRuntime\":\"// AUTO-GENERATED by nanobpmn (ADR 0040 slice 2): the typed `publishMessage`.\\n// Re-exports the worker SDK's `publishMessage` and overrides it with a\\n// messageName-keyed typed signature (variables typed from the message's declared\\n// envelope). Erased to a pass-through at runtime. Do not edit.\\n// eslint-disable\\n\\nimport { publishMessage as publishMessageRaw } from \\\"./worker-sdk.ts\\\";\\nimport type { PublishMessageOptions, PublishMessageResult } from \\\"./worker-sdk.ts\\\";\\nimport type { MessageName, MessagePayloads, MessageVars } from \\\"./message-io.d.ts\\\";\\n\\nexport type { PublishMessageOptions, PublishMessageResult } from \\\"./worker-sdk.ts\\\";\\n\\ntype PayloadFor<K extends MessageName> = K extends keyof MessagePayloads ? MessagePayloads[K] : MessageVars;\\n\\n/**\\n * Typed `publishMessage`: `name` is constrained to the model's declared message\\n * names (`MessageName`, ADR 0040 slice 2) so it autocompletes and rejects unknown\\n * messages, and `variables` are typed from the message's declared envelope. A\\n * declared message with no domain type falls back to MessageVars.\\n */\\nexport function publishMessage<K extends MessageName>(\\n name: K,\\n opts?: PublishMessageOptions<PayloadFor<K> & object>,\\n): Promise<PublishMessageResult> {\\n return publishMessageRaw(name, opts as unknown as PublishMessageOptions);\\n}\\n\",\"workerBindingsWithHeaders\":\"// AUTO-GENERATED by nanobpmn from the App manifest (ADR 0033 §3).\\n// The bridge from the process model to the worker type system: each declared\\n// worker's `taskType` maps to the TS type of its input payload (`job.variables`)\\n// and result, so the typed `defineWorker` types a handler by its job type. Do\\n// not edit — regenerated from the manifest. Erased to plain JS at compile.\\n// eslint-disable\\n\\nimport type { DomainTypes } from \\\"./domain-rows.d.ts\\\";\\n\\n/** Untyped fallback for a job whose worker declares no input/output type. */\\nexport type WorkerVars = Record<string, unknown>;\\n\\n/** Untyped fallback for a job whose worker declares no custom headers. */\\nexport type WorkerHdrs = Record<string, unknown>;\\n\\n/** Every declared worker `taskType` (ADR 0033 §3): the model-derived set the\\n * typed `defineWorker` accepts, so `type` autocompletes and rejects unknown jobs. */\\nexport type WorkerTaskType = \\\"review\\\";\\n\\n/** Input payload (`job.variables`) per declared worker, keyed by `taskType`. */\\nexport interface WorkerInputs {\\n \\\"review\\\": DomainTypes[\\\"Order\\\"];\\n}\\n\\n/** Output payload (worker result) per declared worker, keyed by `taskType`. */\\nexport interface WorkerOutputs {\\n \\\"review\\\": DomainTypes[\\\"Order\\\"];\\n}\\n\\n/** Custom headers (`job.customHeaders`) per declared worker, keyed by `taskType`.\\n * Header values are strings on the wire, so each declared key maps to `string`;\\n * the extra index signature keeps undeclared headers accessible (non-breaking). */\\nexport interface WorkerHeaders {\\n \\\"review\\\": { \\\"priority\\\": string; \\\"x-flag\\\": string; [key: string]: string };\\n}\\n\",\"messageBindings\":\"// AUTO-GENERATED by nanobpmn from the process model (ADR 0040 slice 2).\\n// The bridge from a `bpmn:message`'s data envelope to the message type system:\\n// each named message maps to the TS type of its payload, so the typed\\n// `publishMessage` types a call by its message name. Do not edit — regenerated\\n// from the model. Erased to plain JS at compile.\\n// eslint-disable\\n\\nimport type { DomainTypes } from \\\"./domain-rows.d.ts\\\";\\n\\n/** Untyped fallback payload for a message that declares no envelope type. */\\nexport type MessageVars = Record<string, unknown>;\\n\\n/** Every declared message `name` (ADR 0040 slice 2): the model-derived set the\\n * typed `publishMessage` accepts, so `name` autocompletes and rejects unknown messages. */\\nexport type MessageName = \\\"approved\\\";\\n\\n/** Payload (`variables`) per declared message, keyed by `name`. */\\nexport interface MessagePayloads {\\n \\\"approved\\\": DomainTypes[\\\"Order\\\"];\\n}\\n\"}\n");
10
+ const GOLDENS: Record<string, string> = JSON.parse("{\"bindings\":\"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1/§6).\\n// The typed data-object layer: `openDomain()` → one `Table<T>` per table, so\\n// workers manipulate typed rows instead of hand-writing SQL. Do not edit —\\n// regenerated from the live tables. Erased to plain JS at `deno compile`.\\n\\nimport { type DataSource, openDataSource, type Table } from \\\"./data-sdk.ts\\\";\\nimport type { Customers, Orders } from \\\"./domain-rows.d.ts\\\";\\n\\nexport interface Domain {\\n /** The underlying datasource handle — the raw-SQL escape hatch. */\\n readonly raw: DataSource;\\n readonly orders: Table<Orders>;\\n readonly customers: Table<Customers>;\\n /** Close the underlying connection. */\\n close(): void;\\n}\\n\\n/**\\n * Open the App's datasource as a typed domain: each table is\\n * a `Table<T>` gateway (`db.orders.insert(...)`, `db.orders.get(id)`), with the\\n * raw `DataSource` at `db.raw` for anything the gateway doesn't cover.\\n */\\nexport async function openDomain(source?: string): Promise<Domain> {\\n const raw = await openDataSource(source);\\n return {\\n raw,\\n orders: raw.table<Orders>(\\\"orders\\\", \\\"id\\\"),\\n customers: raw.table<Customers>(\\\"customers\\\", \\\"id\\\"),\\n close: () => raw.close(),\\n };\\n}\\n\\nexport type { Table };\\n\",\"workerRuntime\":\"// AUTO-GENERATED by nanobpmn (ADR 0033 §3): the typed `defineWorker`.\\n// Re-exports the worker SDK and overrides `defineWorker` with a taskType-keyed\\n// typed signature (job.variables, job.customHeaders + result typed from the\\n// worker's declared input/output/header contract). Erased to a pass-through at\\n// runtime. Do not edit.\\n\\nimport type { WorkerHdrs, WorkerHeaders, WorkerInputs, WorkerOutputs, WorkerTaskType, WorkerVars } from \\\"./worker-io.d.ts\\\";\\nimport type { WorkerOptions } from \\\"./worker-sdk.ts\\\";\\nimport { defineWorker as defineWorkerRaw } from \\\"./worker-sdk.ts\\\";\\n\\nexport * from \\\"./worker-sdk.ts\\\";\\n\\ntype InFor<K extends WorkerTaskType> = K extends keyof WorkerInputs ? WorkerInputs[K] : WorkerVars;\\ntype OutFor<K extends WorkerTaskType> = K extends keyof WorkerOutputs ? WorkerOutputs[K] : WorkerVars;\\ntype HdrFor<K extends WorkerTaskType> = K extends keyof WorkerHeaders ? WorkerHeaders[K] : WorkerHdrs;\\n\\n/**\\n * Typed `defineWorker`: `type` is constrained to the model's declared job types\\n * (`WorkerTaskType`, ADR 0033 §3) so it autocompletes and rejects unknown jobs,\\n * and the handler's `job.variables`, `job.customHeaders` + result are typed from\\n * the worker's declared `inputType`/`outputType`/header keys. A declared job type\\n * with no declared type falls back to WorkerVars / WorkerHdrs.\\n */\\nexport function defineWorker<K extends WorkerTaskType>(\\n opts: { type: K } & WorkerOptions<InFor<K> & object, OutFor<K> & object, HdrFor<K> & object>,\\n): void {\\n // biome-ignore lint/plugin: generic-to-raw erasure at the worker-SDK boundary; the wire stays untyped JSON (ADR 0033 §3).\\n defineWorkerRaw(opts as unknown as WorkerOptions);\\n}\\n\",\"messageRuntime\":\"// AUTO-GENERATED by nanobpmn (ADR 0040 slice 2): the typed `publishMessage`.\\n// Re-exports the worker SDK's `publishMessage` and overrides it with a\\n// messageName-keyed typed signature (variables typed from the message's declared\\n// envelope). Erased to a pass-through at runtime. Do not edit.\\n\\nimport type { MessageName, MessagePayloads, MessageVars } from \\\"./message-io.d.ts\\\";\\nimport type { PublishMessageOptions, PublishMessageResult } from \\\"./worker-sdk.ts\\\";\\nimport { publishMessage as publishMessageRaw } from \\\"./worker-sdk.ts\\\";\\n\\nexport type { PublishMessageOptions, PublishMessageResult } from \\\"./worker-sdk.ts\\\";\\n\\ntype PayloadFor<K extends MessageName> = K extends keyof MessagePayloads ? MessagePayloads[K] : MessageVars;\\n\\n/**\\n * Typed `publishMessage`: `name` is constrained to the model's declared message\\n * names (`MessageName`, ADR 0040 slice 2) so it autocompletes and rejects unknown\\n * messages, and `variables` are typed from the message's declared envelope. A\\n * declared message with no domain type falls back to MessageVars.\\n */\\nexport function publishMessage<K extends MessageName>(\\n name: K,\\n opts?: PublishMessageOptions<PayloadFor<K> & object>,\\n): Promise<PublishMessageResult> {\\n // biome-ignore lint/plugin: generic-to-raw erasure at the message-SDK boundary; the wire stays untyped JSON (ADR 0040 slice 2).\\n return publishMessageRaw(name, opts as unknown as PublishMessageOptions);\\n}\\n\",\"workerBindingsWithHeaders\":\"// AUTO-GENERATED by nanobpmn from the App manifest (ADR 0033 §3).\\n// The bridge from the process model to the worker type system: each declared\\n// worker's `taskType` maps to the TS type of its input payload (`job.variables`)\\n// and result, so the typed `defineWorker` types a handler by its job type. Do\\n// not edit — regenerated from the manifest. Erased to plain JS at compile.\\n\\nimport type { DomainTypes } from \\\"./domain-rows.d.ts\\\";\\n\\n/** Untyped fallback for a job whose worker declares no input/output type. */\\nexport type WorkerVars = Record<string, unknown>;\\n\\n/** Untyped fallback for a job whose worker declares no custom headers. */\\nexport type WorkerHdrs = Record<string, unknown>;\\n\\n/** Every declared worker `taskType` (ADR 0033 §3): the model-derived set the\\n * typed `defineWorker` accepts, so `type` autocompletes and rejects unknown jobs. */\\nexport type WorkerTaskType = \\\"review\\\";\\n\\n/** Input payload (`job.variables`) per declared worker, keyed by `taskType`. */\\nexport interface WorkerInputs {\\n \\\"review\\\": DomainTypes[\\\"Order\\\"];\\n}\\n\\n/** Output payload (worker result) per declared worker, keyed by `taskType`. */\\nexport interface WorkerOutputs {\\n \\\"review\\\": DomainTypes[\\\"Order\\\"];\\n}\\n\\n/** Custom headers (`job.customHeaders`) per declared worker, keyed by `taskType`.\\n * Header values are strings on the wire, so each declared key maps to `string`;\\n * the extra index signature keeps undeclared headers accessible (non-breaking). */\\nexport interface WorkerHeaders {\\n \\\"review\\\": { \\\"priority\\\": string; \\\"x-flag\\\": string; [key: string]: string };\\n}\\n\",\"messageBindings\":\"// AUTO-GENERATED by nanobpmn from the process model (ADR 0040 slice 2).\\n// The bridge from a `bpmn:message`'s data envelope to the message type system:\\n// each named message maps to the TS type of its payload, so the typed\\n// `publishMessage` types a call by its message name. Do not edit — regenerated\\n// from the model. Erased to plain JS at compile.\\n\\nimport type { DomainTypes } from \\\"./domain-rows.d.ts\\\";\\n\\n/** Untyped fallback payload for a message that declares no envelope type. */\\nexport type MessageVars = Record<string, unknown>;\\n\\n/** Every declared message `name` (ADR 0040 slice 2): the model-derived set the\\n * typed `publishMessage` accepts, so `name` autocompletes and rejects unknown messages. */\\nexport type MessageName = \\\"approved\\\";\\n\\n/** Payload (`variables`) per declared message, keyed by `name`. */\\nexport interface MessagePayloads {\\n \\\"approved\\\": DomainTypes[\\\"Order\\\"];\\n}\\n\"}");
11
11
 
12
12
  const sources: SourceSchema[] = [
13
13
  {
@@ -17,7 +17,7 @@ import type { ToolkitManifest } from "./migrations.ts";
17
17
 
18
18
  // Goldens captured verbatim from the console's `emitDomainModel` (server domain_types.ts) via
19
19
  // Deno — this IS the per-emitter drift gate (ADR 0053): the toolkit must be byte-identical.
20
- const GOLDENS: Record<string, string> = JSON.parse("{\n \"empty\": \"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n// eslint-disable\\n\\nexport interface DomainTables {}\\n\",\n \"tablesOnly\": \"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n// eslint-disable\\n\\nexport interface Customers {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n email: string;\\n /** TEXT */\\n tier: string | null;\\n}\\n\\nexport interface OrderItems {\\n /** INTEGER (primary key) */\\n id: number;\\n /** INTEGER */\\n qty: number;\\n /** REAL */\\n \\\"unit price\\\": number | null;\\n}\\n\\n/** Every table keyed by its wire name — index it for a row type. */\\nexport interface DomainTables {\\n \\\"customers\\\": Customers;\\n \\\"order_items\\\": OrderItems;\\n}\\n\",\n \"typesOnly\": \"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n// eslint-disable\\n\\nexport interface DomainTables {}\\n\\n/** Declared (transient) domain types from the manifest `types` registry (ADR 0029 §4.2), keyed by id. */\\nexport interface DomainTypes {\\n \\\"taxReturn\\\": {\\n year: number;\\n notes?: string;\\n lines: DomainTypes[\\\"lineItem\\\"][];\\n };\\n \\\"lineItem\\\": {\\n \\\"line no\\\": number;\\n amount: number;\\n };\\n}\\n\",\n \"both\": \"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n// eslint-disable\\n\\nexport interface Greetings {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n who: string;\\n /** TEXT */\\n message: string;\\n /** TEXT */\\n createdAt: string | null;\\n}\\n\\n/** Every table keyed by its wire name — index it for a row type. */\\nexport interface DomainTables {\\n \\\"greetings\\\": Greetings;\\n}\\n\\n/** Declared (transient) domain types from the manifest `types` registry (ADR 0029 §4.2), keyed by id. */\\nexport interface DomainTypes {\\n \\\"greeting\\\": {\\n who: string;\\n message: string;\\n createdAt?: string;\\n };\\n}\\n\",\n \"multi\": \"// AUTO-GENERATED by nanobpmn from the datasource schemas (ADR 0029 §4.1/§6).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n// eslint-disable\\n\\nexport interface AppCustomers {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n email: string;\\n}\\n\\nexport interface AnalyticsEvents {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n kind: string;\\n}\\n\\n/** Every datasource keyed by alias, then table by wire name. */\\nexport interface DomainSources {\\n \\\"app\\\": {\\n \\\"customers\\\": AppCustomers;\\n };\\n \\\"analytics\\\": {\\n \\\"events\\\": AnalyticsEvents;\\n };\\n}\\n\\n/** The default datasource's tables — index by wire name. */\\nexport type DomainTables = DomainSources[\\\"analytics\\\"];\\n\"\n}");
20
+ const GOLDENS: Record<string, string> = JSON.parse("{\"empty\":\"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n\\nexport type DomainTables = Record<never, never>;\\n\",\"tablesOnly\":\"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n\\nexport interface Customers {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n email: string;\\n /** TEXT */\\n tier: string | null;\\n}\\n\\nexport interface OrderItems {\\n /** INTEGER (primary key) */\\n id: number;\\n /** INTEGER */\\n qty: number;\\n /** REAL */\\n \\\"unit price\\\": number | null;\\n}\\n\\n/** Every table keyed by its wire name — index it for a row type. */\\nexport interface DomainTables {\\n \\\"customers\\\": Customers;\\n \\\"order_items\\\": OrderItems;\\n}\\n\",\"typesOnly\":\"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n\\nexport type DomainTables = Record<never, never>;\\n\\n/** Declared (transient) domain types from the manifest `types` registry (ADR 0029 §4.2), keyed by id. */\\nexport interface DomainTypes {\\n \\\"taxReturn\\\": {\\n year: number;\\n notes?: string;\\n lines: DomainTypes[\\\"lineItem\\\"][];\\n };\\n \\\"lineItem\\\": {\\n \\\"line no\\\": number;\\n amount: number;\\n };\\n}\\n\",\"both\":\"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n\\nexport interface Greetings {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n who: string;\\n /** TEXT */\\n message: string;\\n /** TEXT */\\n createdAt: string | null;\\n}\\n\\n/** Every table keyed by its wire name — index it for a row type. */\\nexport interface DomainTables {\\n \\\"greetings\\\": Greetings;\\n}\\n\\n/** Declared (transient) domain types from the manifest `types` registry (ADR 0029 §4.2), keyed by id. */\\nexport interface DomainTypes {\\n \\\"greeting\\\": {\\n who: string;\\n message: string;\\n createdAt?: string;\\n };\\n}\\n\",\"multi\":\"// AUTO-GENERATED by nanobpmn from the datasource schemas (ADR 0029 §4.1/§6).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n\\nexport interface AppCustomers {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n email: string;\\n}\\n\\nexport interface AnalyticsEvents {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n kind: string;\\n}\\n\\n/** Every datasource keyed by alias, then table by wire name. */\\nexport interface DomainSources {\\n \\\"app\\\": {\\n \\\"customers\\\": AppCustomers;\\n };\\n \\\"analytics\\\": {\\n \\\"events\\\": AnalyticsEvents;\\n };\\n}\\n\\n/** The default datasource's tables — index by wire name. */\\nexport type DomainTables = DomainSources[\\\"analytics\\\"];\\n\"}");
21
21
 
22
22
  const col = (name: string, type: string, notNull: boolean, primaryKey: boolean) =>
23
23
  ({ name, type, notNull, primaryKey });