@nanobpm/urban 0.17.0 → 0.18.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 (37) hide show
  1. package/dist/cli.js +69 -5
  2. package/dist/toolkit/derivers/domain.d.ts +69 -0
  3. package/dist/toolkit/derivers/domain.js +243 -0
  4. package/dist/toolkit/derivers/messages.d.ts +36 -0
  5. package/dist/toolkit/derivers/messages.js +134 -0
  6. package/dist/toolkit/derivers/meta.d.ts +34 -0
  7. package/dist/toolkit/derivers/meta.js +106 -0
  8. package/dist/toolkit/derivers/migrations.d.ts +34 -1
  9. package/dist/toolkit/derivers/migrations.js +37 -8
  10. package/dist/toolkit/derivers/worker-io.d.ts +14 -0
  11. package/dist/toolkit/derivers/worker-io.js +13 -2
  12. package/dist/toolkit/fsio.js +31 -2
  13. package/dist/toolkit/gen.d.ts +28 -0
  14. package/dist/toolkit/gen.js +60 -11
  15. package/dist/toolkit/index.d.ts +18 -4
  16. package/dist/toolkit/index.js +12 -3
  17. package/dist/toolkit/models.d.ts +63 -0
  18. package/dist/toolkit/models.js +142 -0
  19. package/package.json +1 -1
  20. package/src/cli.ts +70 -5
  21. package/src/toolkit/derivers/domain.test.ts +140 -0
  22. package/src/toolkit/derivers/domain.ts +317 -0
  23. package/src/toolkit/derivers/messages.test.ts +174 -0
  24. package/src/toolkit/derivers/messages.ts +166 -0
  25. package/src/toolkit/derivers/meta.test.ts +140 -0
  26. package/src/toolkit/derivers/meta.ts +128 -0
  27. package/src/toolkit/derivers/migrations.test.ts +42 -4
  28. package/src/toolkit/derivers/migrations.ts +61 -9
  29. package/src/toolkit/derivers/worker-io.test.ts +10 -0
  30. package/src/toolkit/derivers/worker-io.ts +14 -2
  31. package/src/toolkit/fsio.test.ts +23 -0
  32. package/src/toolkit/fsio.ts +22 -2
  33. package/src/toolkit/gen.test.ts +84 -2
  34. package/src/toolkit/gen.ts +86 -12
  35. package/src/toolkit/index.ts +66 -3
  36. package/src/toolkit/models.test.ts +159 -0
  37. package/src/toolkit/models.ts +193 -0
@@ -0,0 +1,106 @@
1
+ // Deriver: BPMN models → `meta.ts`, the typed model-metadata accessor (`@nanobpm/meta`,
2
+ // ADR 0040 §5). Scans each process for `nano:meta` key/value siblings of the `nano:shapes`
3
+ // container and folds them into a single app-wide record. It emits the SAME `meta.ts` the
4
+ // console generates (a faithful port of `foldMeta`/`emitMeta` in the server's domain_types.ts),
5
+ // so the toolkit is a drop-in for the IDE's codegen (ADR 0053).
6
+ import { GENERATED_DIR } from "../artifact.js";
7
+ import { byModelPath } from "./worker-io.js";
8
+ /** Basename the console uses for the generated accessor; kept identical (drop-in). */
9
+ export const META_TS = "meta.ts";
10
+ function attr(tag, name) {
11
+ const m = tag.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`));
12
+ return m ? m[1] : undefined;
13
+ }
14
+ function processId(xml) {
15
+ const m = xml.match(/<[\w.-]*:?process\b[^>]*>/);
16
+ return m ? attr(m[0], "id") : undefined;
17
+ }
18
+ // A `nano:shape` carrier (`<...:shape>…</...:shape>`, and self-closing) — meta is only
19
+ // recognised *outside* a shape carrier (its op children, e.g. `extend`, are a different
20
+ // vocabulary), so strip carriers before matching `nano:meta` (mirrors envelope_scan.rs).
21
+ const SHAPE_BLOCK_RE = /<([\w.-]*:)?shape\b[^>]*(?:\/>|>[\s\S]*?<\/\1?shape\s*>)/g;
22
+ // A `nano:meta` element (any namespace prefix; matches the Rust local-name scan).
23
+ const META_RE = /<([\w.-]*:)?meta\b([^>]*?)\/?>/g;
24
+ /** Scan one BPMN document for model-level `nano:meta` key/value entries, in document
25
+ * order, tagged with the enclosing process. A meta with no `key` attribute is ignored;
26
+ * a missing `value` folds to the empty string (matches envelope_scan.rs). */
27
+ export function scanModelMeta(xml) {
28
+ const proc = processId(xml);
29
+ const withoutShapes = xml.replace(SHAPE_BLOCK_RE, "");
30
+ const out = [];
31
+ let m;
32
+ while ((m = META_RE.exec(withoutShapes)) !== null) {
33
+ const key = attr(`<x ${m[2]}>`, "key");
34
+ if (key == null)
35
+ continue;
36
+ out.push({ process: proc, key, value: attr(`<x ${m[2]}>`, "value") ?? "" });
37
+ }
38
+ return out;
39
+ }
40
+ // --- Faithful port of the console's foldMeta/emitMeta (domain_types.ts) so the emitted
41
+ // meta.ts is byte-compatible with the IDE's own codegen. ---
42
+ function isIdent(name) {
43
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
44
+ }
45
+ /** Fold the scanned model-level metadata into a single key→value record. When two models
46
+ * declare the same key the last (scan order) wins. Empty keys are ignored. The accumulator
47
+ * is a **null-prototype** dict so user-authored keys (including `__proto__`) round-trip
48
+ * deterministically as own data properties. */
49
+ export function foldMeta(metas) {
50
+ const out = Object.create(null);
51
+ for (const m of metas) {
52
+ const key = (m?.key ?? "").trim();
53
+ if (!key)
54
+ continue;
55
+ out[key] = m.value ?? "";
56
+ }
57
+ return out;
58
+ }
59
+ /** Emit `meta.ts`: the typed model-metadata accessor (`@nanobpm/meta`, ADR 0040 §5).
60
+ * `AppMeta` keys the declared metadata as string-literal properties; `appMeta` is the
61
+ * concrete null-prototype record; the overloaded `meta()` returns the typed value for a
62
+ * known key and `string | undefined` for a dynamic one. Strip-safe (ADR 0036). */
63
+ export function emitMeta(metas) {
64
+ const folded = foldMeta(metas);
65
+ const keys = Object.keys(folded);
66
+ const propKey = (k) => (isIdent(k) ? k : JSON.stringify(k));
67
+ const header = "// AUTO-GENERATED by nanobpmn from the process model (ADR 0040 §5): the typed\n" +
68
+ "// model-metadata accessor. Each `nano:meta` key/value the models declare becomes\n" +
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";
72
+ const iface = keys.length > 0
73
+ ? `export interface AppMeta {\n${keys.map((k) => ` ${propKey(k)}: string;`).join("\n")}\n}\n`
74
+ : `export interface AppMeta {}\n`;
75
+ const constDecl = keys.length > 0
76
+ ? `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`;
81
+ const accessor = `\n/** Read a model-level metadata value. A declared key is typed; a dynamic key\n` +
82
+ ` * returns \`string | undefined\`. */\n` +
83
+ `export function meta<K extends keyof AppMeta>(key: K): AppMeta[K];\n` +
84
+ `export function meta(key: string): string | undefined;\n` +
85
+ `export function meta(key: string): string | undefined {\n` +
86
+ ` return (appMeta as Record<string, string>)[key];\n` +
87
+ `}\n`;
88
+ return `${header}/** Every model-level metadata key the App's models declare (ADR 0040 §5). */\n` +
89
+ iface + "\n" + constDecl + accessor;
90
+ }
91
+ /**
92
+ * Derive `nano-generated/meta.ts` from BPMN models. Deterministic: models are scanned in
93
+ * path order, then their `nano:meta` entries folded (last write wins) into the accessor.
94
+ */
95
+ export function deriveMeta(models) {
96
+ const metas = [];
97
+ for (const model of [...models].sort(byModelPath)) {
98
+ metas.push(...scanModelMeta(model.xml));
99
+ }
100
+ return [{ path: `${GENERATED_DIR}/${META_TS}`, content: emitMeta(metas) }];
101
+ }
102
+ export const metaDeriver = {
103
+ id: "model->meta",
104
+ describe: "Derive meta.ts (the typed @nanobpm/meta accessor) from BPMN nano:meta entries.",
105
+ derive: ({ models }) => deriveMeta(models),
106
+ };
@@ -2,6 +2,8 @@ import type { DerivedArtifact, Deriver } from "../artifact.ts";
2
2
  export interface ToolkitField {
3
3
  type?: string;
4
4
  optional?: boolean;
5
+ /** Marks a repeated field: the domain row-type projects it as `T[]` in the `DomainTypes` registry. */
6
+ list?: boolean;
5
7
  }
6
8
  export interface ToolkitType {
7
9
  table?: string;
@@ -20,7 +22,38 @@ export interface ToolkitManifest {
20
22
  /** Map a domain field type to a SQLite column type. Unknown types fall back to TEXT. */
21
23
  export declare function sqlType(t: string | undefined): string;
22
24
  export declare function assertSqlIdent(kind: string, name: string): string;
23
- /** Build the CREATE TABLE statement for one type. */
25
+ /** Double-quote an identifier for DDL. `assertSqlIdent` already rejects non-identifier
26
+ * chars (the injection guard), but quoting is what makes valid-but-reserved words
27
+ * (`order`, `group`, `select`, …) legal SQL — no reserved-word denylist to maintain.
28
+ * Embedded double quotes are doubled per the SQL standard (defensive; can't occur here). */
29
+ export declare function quoteSqlIdent(name: string): string;
30
+ /** A column in a manifest type's canonical table schema. `sqlType` is the SQLite
31
+ * declared type (the affinity source for both the CREATE TABLE and the row types). */
32
+ export interface DerivedColumn {
33
+ name: string;
34
+ sqlType: string;
35
+ notNull: boolean;
36
+ primaryKey: boolean;
37
+ }
38
+ /** The canonical table schema for one manifest type. */
39
+ export interface DerivedTable {
40
+ table: string;
41
+ columns: DerivedColumn[];
42
+ }
43
+ /**
44
+ * Derive the canonical table schema for one manifest type: an implicit
45
+ * autoincrement `id` primary key plus one column per declared field. This is the
46
+ * single source of truth both the SQL migration (`createTableSql`) and the domain
47
+ * row-types (`domain.ts`) derive from, so the schema can never drift between them.
48
+ */
49
+ export declare function tableSchemaForType(typeName: string, def: ToolkitType): DerivedTable;
50
+ /** Group manifest types by their datasource, preserving declaration order within each
51
+ * source. Shared by the migrations and domain derivers so both partition and order
52
+ * sources identically (a single source of truth for the source spine). */
53
+ export declare function groupTypesBySource(manifest: ToolkitManifest): Map<string, Array<[string, ToolkitType]>>;
54
+ /** Build the CREATE TABLE statement for one type, from its canonical schema. Table and
55
+ * column identifiers are quoted so valid-but-reserved names (`order`, `group`, …) emit
56
+ * legal SQL. */
24
57
  export declare function createTableSql(typeName: string, def: ToolkitType): string;
25
58
  export declare function deriveMigrations(manifest: ToolkitManifest): DerivedArtifact[];
26
59
  export declare const migrationsDeriver: Deriver<ToolkitManifest>;
@@ -31,25 +31,40 @@ export function assertSqlIdent(kind, name) {
31
31
  }
32
32
  return name;
33
33
  }
34
- /** Build the CREATE TABLE statement for one type. */
35
- export function createTableSql(typeName, def) {
34
+ /** Double-quote an identifier for DDL. `assertSqlIdent` already rejects non-identifier
35
+ * chars (the injection guard), but quoting is what makes valid-but-reserved words
36
+ * (`order`, `group`, `select`, …) legal SQL — no reserved-word denylist to maintain.
37
+ * Embedded double quotes are doubled per the SQL standard (defensive; can't occur here). */
38
+ export function quoteSqlIdent(name) {
39
+ return `"${name.replace(/"/g, '""')}"`;
40
+ }
41
+ /**
42
+ * Derive the canonical table schema for one manifest type: an implicit
43
+ * autoincrement `id` primary key plus one column per declared field. This is the
44
+ * single source of truth both the SQL migration (`createTableSql`) and the domain
45
+ * row-types (`domain.ts`) derive from, so the schema can never drift between them.
46
+ */
47
+ export function tableSchemaForType(typeName, def) {
36
48
  const table = assertSqlIdent("table name", def.table ?? typeName);
37
- const cols = [" id INTEGER PRIMARY KEY AUTOINCREMENT"];
49
+ const columns = [
50
+ { name: "id", sqlType: "INTEGER", notNull: false, primaryKey: true },
51
+ ];
38
52
  const fields = def.fields ?? {};
39
53
  for (const [name, f] of Object.entries(fields)) {
40
54
  assertSqlIdent("field name", name);
41
55
  if (name === "id") {
42
56
  throw new Error(`type "${typeName}": field "id" is reserved (the table's implicit primary key)`);
43
57
  }
44
- const notNull = f.optional ? "" : " NOT NULL";
45
- cols.push(` ${name} ${sqlType(f.type)}${notNull}`);
58
+ columns.push({ name, sqlType: sqlType(f.type), notNull: !f.optional, primaryKey: false });
46
59
  }
47
- return `CREATE TABLE IF NOT EXISTS ${table} (\n${cols.join(",\n")}\n);`;
60
+ return { table, columns };
48
61
  }
49
- export function deriveMigrations(manifest) {
62
+ /** Group manifest types by their datasource, preserving declaration order within each
63
+ * source. Shared by the migrations and domain derivers so both partition and order
64
+ * sources identically (a single source of truth for the source spine). */
65
+ export function groupTypesBySource(manifest) {
50
66
  const types = manifest.types ?? {};
51
67
  const defaultSource = manifest.data?.default ?? "app";
52
- // Group types by their datasource.
53
68
  const bySource = new Map();
54
69
  for (const [name, def] of Object.entries(types)) {
55
70
  const src = def.source ?? defaultSource;
@@ -57,6 +72,20 @@ export function deriveMigrations(manifest) {
57
72
  list.push([name, def]);
58
73
  bySource.set(src, list);
59
74
  }
75
+ return bySource;
76
+ }
77
+ /** Build the CREATE TABLE statement for one type, from its canonical schema. Table and
78
+ * column identifiers are quoted so valid-but-reserved names (`order`, `group`, …) emit
79
+ * legal SQL. */
80
+ export function createTableSql(typeName, def) {
81
+ const { table, columns } = tableSchemaForType(typeName, def);
82
+ const cols = columns.map((c) => c.primaryKey
83
+ ? ` ${quoteSqlIdent(c.name)} ${c.sqlType} PRIMARY KEY AUTOINCREMENT`
84
+ : ` ${quoteSqlIdent(c.name)} ${c.sqlType}${c.notNull ? " NOT NULL" : ""}`);
85
+ return `CREATE TABLE IF NOT EXISTS ${quoteSqlIdent(table)} (\n${cols.join(",\n")}\n);`;
86
+ }
87
+ export function deriveMigrations(manifest) {
88
+ const bySource = groupTypesBySource(manifest);
60
89
  const artifacts = [];
61
90
  for (const [src, list] of bySource) {
62
91
  const header = `-- Derived from manifest \`types\` by @nanobpm/urban (urban gen). Do not edit.\n` +
@@ -4,6 +4,16 @@ export interface ModelSource {
4
4
  path: string;
5
5
  xml: string;
6
6
  }
7
+ /** Deterministic comparator for ordering `ModelSource` lists by path. Returns 0 on equal
8
+ * paths so it satisfies the JS sort contract (a comparator that reports both `a>b` and
9
+ * `b>a` can reorder equal elements unpredictably) — keeping the derivers' cross-model
10
+ * fold order stable even if a manifest pattern yields duplicate paths. Shared by every
11
+ * model-scanning deriver so there is one canonical ordering. */
12
+ export declare function byModelPath(a: {
13
+ path: string;
14
+ }, b: {
15
+ path: string;
16
+ }): number;
7
17
  export interface WorkerIo {
8
18
  taskType: string;
9
19
  elementId?: string;
@@ -19,6 +29,10 @@ export declare const WORKER_BINDINGS_DTS = "worker-io.d.ts";
19
29
  export declare const DOMAIN_DTS = "domain-rows.d.ts";
20
30
  /** Scan one BPMN document for service-task worker I/O. */
21
31
  export declare function scanModelWorkers(xml: string): WorkerIo[];
32
+ /** A `DomainTypes[...]` ref for a declared type id (else undefined). The one canonical
33
+ * ref helper shared by the worker- and message-IO emitters (matches the console's single
34
+ * `typeRefFor` in domain_types.ts), so both stay identical to the host codegen. */
35
+ export declare function typeRefFor(id: string | undefined, declared: Set<string>): string | undefined;
22
36
  export interface WorkerBindingDecl {
23
37
  taskType?: string;
24
38
  inputType?: string;
@@ -4,6 +4,14 @@
4
4
  // `worker-io.d.ts` the console generates (a faithful port of `emitWorkerBindings` in the
5
5
  // server's domain_types.ts), so the toolkit is a drop-in for the IDE's codegen (ADR 0053).
6
6
  import { GENERATED_DIR } from "../artifact.js";
7
+ /** Deterministic comparator for ordering `ModelSource` lists by path. Returns 0 on equal
8
+ * paths so it satisfies the JS sort contract (a comparator that reports both `a>b` and
9
+ * `b>a` can reorder equal elements unpredictably) — keeping the derivers' cross-model
10
+ * fold order stable even if a manifest pattern yields duplicate paths. Shared by every
11
+ * model-scanning deriver so there is one canonical ordering. */
12
+ export function byModelPath(a, b) {
13
+ return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
14
+ }
7
15
  /** Filename the console uses; kept identical so the artifact is a drop-in. */
8
16
  export const WORKER_BINDINGS_DTS = "worker-io.d.ts";
9
17
  /** The domain type declarations `worker-io.d.ts` imports from (console: domain-rows.d.ts). */
@@ -51,7 +59,10 @@ export function scanModelWorkers(xml) {
51
59
  }
52
60
  // --- Faithful port of the console's emitWorkerBindings (domain_types.ts) so the emitted
53
61
  // worker-io.d.ts is byte-compatible with the IDE's own codegen. ---
54
- function typeRefFor(id, declared) {
62
+ /** A `DomainTypes[...]` ref for a declared type id (else undefined). The one canonical
63
+ * ref helper shared by the worker- and message-IO emitters (matches the console's single
64
+ * `typeRefFor` in domain_types.ts), so both stay identical to the host codegen. */
65
+ export function typeRefFor(id, declared) {
55
66
  return id != null && declared.has(id) ? `DomainTypes[${JSON.stringify(id)}]` : undefined;
56
67
  }
57
68
  /** Emit `worker-io.d.ts` from worker bindings + the set of declared domain type ids. */
@@ -120,7 +131,7 @@ export function emitWorkerBindings(workers, declaredTypeIds) {
120
131
  */
121
132
  export function deriveWorkerBindings(models, declaredTypeIds = []) {
122
133
  const workers = [];
123
- for (const model of [...models].sort((a, b) => (a.path < b.path ? -1 : 1))) {
134
+ for (const model of [...models].sort(byModelPath)) {
124
135
  for (const w of scanModelWorkers(model.xml)) {
125
136
  workers.push({ taskType: w.taskType, inputType: w.in, outputType: w.out });
126
137
  }
@@ -1,7 +1,16 @@
1
1
  // Node/Deno filesystem implementation of the GenIO port. Uses node:fs/promises, which both
2
2
  // Node and Deno provide, so a single implementation serves both runtimes.
3
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
4
- import { dirname } from "node:path";
3
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
4
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
5
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
6
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
7
+ });
8
+ }
9
+ return path;
10
+ };
11
+ import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
12
+ import { dirname, resolve } from "node:path";
13
+ import { pathToFileURL } from "node:url";
5
14
  export function createNodeGenIO() {
6
15
  return {
7
16
  async readText(path) {
@@ -31,5 +40,25 @@ export function createNodeGenIO() {
31
40
  return false;
32
41
  }
33
42
  },
43
+ async remove(path) {
44
+ await rm(path, { force: true });
45
+ },
46
+ // Import an app module for code-first model derivation. `import()` of a file URL loads TS on
47
+ // Deno natively and on Node with type-stripping (≥22.18 default, or `--experimental-strip-types`).
48
+ // The cache key is the file's mtime, not a monotonic clock: an unchanged workflow reuses the
49
+ // cached module instance (bounding the ESM module map in long-lived processes like `urban dev`)
50
+ // while an edited one still reloads.
51
+ async importModule(path) {
52
+ const abs = resolve(path);
53
+ let version = "";
54
+ try {
55
+ version = `?v=${(await stat(abs)).mtimeMs}`;
56
+ }
57
+ catch {
58
+ // stat failed (e.g. a virtual path); fall back to no cache key and let import() surface it.
59
+ }
60
+ const href = `${pathToFileURL(abs).href}${version}`;
61
+ return import(__rewriteRelativeImportExtension(href));
62
+ },
34
63
  };
35
64
  }
@@ -1,5 +1,6 @@
1
1
  import type { DerivedArtifact } from "./artifact.ts";
2
2
  import { type ModelSource } from "./derivers/worker-io.ts";
3
+ import { type DerivedModels, type ModelError } from "./models.ts";
3
4
  /** Minimal filesystem port. Node/Deno impls live in `fsio.ts`. */
4
5
  export interface GenIO {
5
6
  readText(path: string): Promise<string>;
@@ -7,19 +8,43 @@ export interface GenIO {
7
8
  /** File names (not paths) in a directory; empty if it does not exist. */
8
9
  listDir(path: string): Promise<string[]>;
9
10
  exists(path: string): Promise<boolean>;
11
+ /**
12
+ * Import an app module and return its exports. Optional: only present when the caller runs on
13
+ * a runtime that can load the app's TypeScript (Node with type-stripping, or Deno). Code-first
14
+ * model derivation (`workflows/*.ts` → BPMN) needs it; when absent, gen still derives everything
15
+ * else (migrations, domain, worker I/O from authored/derived models).
16
+ */
17
+ importModule?(path: string): Promise<Record<string, unknown>>;
18
+ /** Delete a file. Optional: enables the provenance-scoped stale-model sweep (skipped when absent). */
19
+ remove?(path: string): Promise<void>;
10
20
  }
11
21
  export interface GenOptions {
12
22
  root: string;
13
23
  io: GenIO;
14
24
  manifestFile?: string;
25
+ /**
26
+ * Whether to emit (write) derived `.bpmn` models. Default true. When false, code-first models
27
+ * are still derived *in-memory* to feed the type-contract derivers (worker I/O, meta, messages)
28
+ * — they are just not written to disk or swept. This is the `urban gen --no-models` path, so
29
+ * the console's type-contract regen never mutates `processes/*.bpmn`.
30
+ */
31
+ emitModels?: boolean;
32
+ /** Derive ONLY the models (skip migrations/domain/worker-I/O). The `urban derive` path. */
33
+ modelsOnly?: boolean;
15
34
  }
16
35
  export interface GenResult {
17
36
  artifacts: DerivedArtifact[];
18
37
  /** Paths that differ from disk (only populated by `check`). */
19
38
  drift: string[];
39
+ /** True if any code-first workflow failed to import/derive. */
40
+ incomplete: boolean;
41
+ /** Per-file model-derivation errors (empty on success). */
42
+ modelErrors: ModelError[];
20
43
  }
21
44
  declare function join(root: string, rel: string): string;
22
45
  declare function dirOf(p: string): string;
46
+ /** Resolve a `dir/*.ext` (or literal) manifest pattern to file paths relative to root. */
47
+ export declare function expandPattern(root: string, io: GenIO, pattern: string): Promise<string[]>;
23
48
  /** Read + resolve the app's process models from the manifest's `models.processes` patterns. */
24
49
  export declare function readModels(root: string, io: GenIO, manifest: {
25
50
  models?: {
@@ -33,3 +58,6 @@ export declare function runGen(opts: GenOptions & {
33
58
  check?: boolean;
34
59
  }): Promise<GenResult>;
35
60
  export { dirOf, join as joinPath };
61
+ /** Derive code-first models in-memory (no writes) — backs `urban derive --stdout`, the
62
+ * non-mutating preview the console's read-only model viewer uses. */
63
+ export declare function previewModels(opts: GenOptions): Promise<DerivedModels>;
@@ -4,7 +4,11 @@
4
4
  // pure; all IO is confined here behind a tiny FS port so the same code runs on Node and Deno.
5
5
  import { sortArtifacts } from "./artifact.js";
6
6
  import { deriveMigrations } from "./derivers/migrations.js";
7
+ import { deriveDomain } from "./derivers/domain.js";
7
8
  import { deriveWorkerBindings } from "./derivers/worker-io.js";
9
+ import { deriveMeta } from "./derivers/meta.js";
10
+ import { deriveMessageBindings } from "./derivers/messages.js";
11
+ import { deriveModels, MODEL_PROVENANCE } from "./models.js";
8
12
  function join(root, rel) {
9
13
  // Trim either separator so callers may pass Windows-style paths; GenIO
10
14
  // implementations accept forward slashes on all platforms.
@@ -15,7 +19,7 @@ function dirOf(p) {
15
19
  return i <= 0 ? "." : p.slice(0, i);
16
20
  }
17
21
  /** Resolve a `dir/*.ext` (or literal) manifest pattern to file paths relative to root. */
18
- async function expandPattern(root, io, pattern) {
22
+ export async function expandPattern(root, io, pattern) {
19
23
  const star = pattern.indexOf("*");
20
24
  if (star === -1) {
21
25
  return (await io.exists(join(root, pattern))) ? [pattern] : [];
@@ -43,26 +47,47 @@ export async function readModels(root, io, manifest) {
43
47
  }
44
48
  /** Collect all artifacts a run would produce, without touching disk beyond reads. */
45
49
  export async function collectArtifacts(opts) {
50
+ return (await collectAll(opts)).artifacts;
51
+ }
52
+ /** Internal: collect artifacts AND the model-derivation details (needed by `runGen` for the
53
+ * incomplete signal + the provenance-scoped stale sweep). */
54
+ async function collectAll(opts) {
46
55
  const { root, io } = opts;
56
+ const emitModels = opts.emitModels ?? true;
47
57
  const manifestPath = join(root, opts.manifestFile ?? "nano.app.json");
48
58
  const manifest = JSON.parse(await io.readText(manifestPath));
49
59
  const artifacts = [];
50
- // 1. types → migrations
51
- if (manifest.types && Object.keys(manifest.types).length > 0) {
60
+ // 1. types → migrations + domain row types (DomainTables spine + DomainTypes registry)
61
+ if (!opts.modelsOnly && manifest.types && Object.keys(manifest.types).length > 0) {
52
62
  artifacts.push(...deriveMigrations(manifest));
63
+ artifacts.push(...deriveDomain(manifest));
53
64
  }
54
- // 2. models → worker I/O index
55
- const models = await readModels(root, io, manifest);
56
- if (models.length > 0) {
57
- const declaredTypeIds = Object.keys(manifest.types ?? {});
58
- artifacts.push(...deriveWorkerBindings(models, declaredTypeIds));
65
+ // 2. code-first models → derive BPMN from `workflows/*.ts` (executes the app's TS). The derived
66
+ // `.bpmn` are emitted only when `emitModels` (default), but always fed to the type-contract
67
+ // derivers below so worker I/O works for a code-first app even on a `--no-models` run.
68
+ const derived = await deriveModels(root, io, manifest);
69
+ if (emitModels)
70
+ artifacts.push(...derived.artifacts);
71
+ // 3. models (authored on-disk + derived) → worker I/O index + meta accessor + message map.
72
+ // Drop any on-disk model whose path a derived model already covers, to avoid double-counting.
73
+ if (!opts.modelsOnly) {
74
+ const derivedPaths = new Set(derived.artifacts.map((a) => a.path));
75
+ const diskModels = (await readModels(root, io, manifest)).filter((m) => !derivedPaths.has(m.path));
76
+ const models = [...diskModels, ...derived.models];
77
+ if (models.length > 0) {
78
+ const declaredTypeIds = Object.keys(manifest.types ?? {});
79
+ artifacts.push(...deriveWorkerBindings(models, declaredTypeIds));
80
+ artifacts.push(...deriveMeta(models));
81
+ artifacts.push(...deriveMessageBindings(models, declaredTypeIds));
82
+ }
59
83
  }
60
- return sortArtifacts(artifacts);
84
+ return { artifacts: sortArtifacts(artifacts), derived };
61
85
  }
62
86
  /** Run the derivers and write artifacts (or, with `check`, report drift without writing). */
63
87
  export async function runGen(opts) {
64
- const artifacts = await collectArtifacts(opts);
88
+ const { artifacts, derived } = await collectAll(opts);
65
89
  const { root, io } = opts;
90
+ const emitModels = opts.emitModels ?? true;
66
91
  const drift = [];
67
92
  for (const a of artifacts) {
68
93
  const abs = join(root, a.path);
@@ -75,6 +100,30 @@ export async function runGen(opts) {
75
100
  await io.writeText(abs, a.content);
76
101
  }
77
102
  }
78
- return { artifacts, drift };
103
+ // Provenance-scoped stale sweep: delete derived `.bpmn` for flows that no longer exist. Only
104
+ // in write mode, only when we emitted models, only on a complete derivation (never delete when
105
+ // a workflow failed to load), and only files bearing our marker (authored models are untouched).
106
+ if (!opts.check && emitModels && derived.attempted && !derived.incomplete && io.remove) {
107
+ const keep = new Set(derived.artifacts.map((a) => a.path));
108
+ for (const name of await io.listDir(join(root, derived.outDir))) {
109
+ if (!name.endsWith(".bpmn"))
110
+ continue;
111
+ const rel = `${derived.outDir}/${name}`;
112
+ if (keep.has(rel))
113
+ continue;
114
+ const content = await io.readText(join(root, rel));
115
+ if (content.includes(MODEL_PROVENANCE))
116
+ await io.remove(join(root, rel));
117
+ }
118
+ }
119
+ return { artifacts, drift, incomplete: derived.incomplete, modelErrors: derived.errors };
79
120
  }
80
121
  export { dirOf, join as joinPath };
122
+ /** Derive code-first models in-memory (no writes) — backs `urban derive --stdout`, the
123
+ * non-mutating preview the console's read-only model viewer uses. */
124
+ export async function previewModels(opts) {
125
+ const { root, io } = opts;
126
+ const manifestPath = join(root, opts.manifestFile ?? "nano.app.json");
127
+ const manifest = JSON.parse(await io.readText(manifestPath));
128
+ return deriveModels(root, io, manifest);
129
+ }
@@ -1,16 +1,30 @@
1
1
  export type { DerivedArtifact, Deriver } from "./artifact.ts";
2
2
  export { GENERATED_DIR, sortArtifacts } from "./artifact.ts";
3
- export { deriveMigrations, migrationsDeriver, sqlType, createTableSql, } from "./derivers/migrations.ts";
4
- export type { ToolkitManifest, ToolkitType, ToolkitField } from "./derivers/migrations.ts";
3
+ export { deriveMigrations, migrationsDeriver, sqlType, createTableSql, tableSchemaForType, groupTypesBySource, } from "./derivers/migrations.ts";
4
+ export type { ToolkitManifest, ToolkitType, ToolkitField, DerivedColumn, DerivedTable, } from "./derivers/migrations.ts";
5
+ export { deriveDomain, emitDomain, emitDomainModel, emitDomainDts, emitDomainDtsForSources, emitDomainTypeRegistry, domainDeriver, sourcesFromManifest, registryFromManifest, sqliteAffinityToTs, interfaceName, } from "./derivers/domain.ts";
6
+ export type { SourceSchema, DomainFieldDef, DomainTypeDef, DomainTypeRegistry, } from "./derivers/domain.ts";
5
7
  export { deriveWorkerBindings, emitWorkerBindings, workerIoDeriver, scanModelWorkers, WORKER_BINDINGS_DTS, DOMAIN_DTS, } from "./derivers/worker-io.ts";
6
8
  export type { ModelSource, WorkerIo, WorkerBindingDecl } from "./derivers/worker-io.ts";
7
- export declare const DERIVERS: readonly [import("./artifact.ts").Deriver<import("./derivers/migrations.ts").ToolkitManifest>, import("./artifact.ts").Deriver<{
9
+ export { byModelPath, typeRefFor } from "./derivers/worker-io.ts";
10
+ export { deriveMeta, emitMeta, foldMeta, metaDeriver, scanModelMeta, META_TS, } from "./derivers/meta.ts";
11
+ export type { MetaDecl } from "./derivers/meta.ts";
12
+ export { deriveMessageBindings, emitMessageBindings, messagesDeriver, scanModelMessages, MESSAGE_BINDINGS_DTS, } from "./derivers/messages.ts";
13
+ export type { MessageIo, MessageBindingDecl } from "./derivers/messages.ts";
14
+ export declare const DERIVERS: readonly [import("./artifact.ts").Deriver<import("./derivers/migrations.ts").ToolkitManifest>, import("./artifact.ts").Deriver<import("./derivers/migrations.ts").ToolkitManifest>, import("./artifact.ts").Deriver<{
15
+ models: import("./derivers/worker-io.ts").ModelSource[];
16
+ declaredTypeIds: Iterable<string>;
17
+ }>, import("./artifact.ts").Deriver<{
18
+ models: import("./derivers/worker-io.ts").ModelSource[];
19
+ }>, import("./artifact.ts").Deriver<{
8
20
  models: import("./derivers/worker-io.ts").ModelSource[];
9
21
  declaredTypeIds: Iterable<string>;
10
22
  }>];
11
- export { collectArtifacts, runGen, joinPath, readModels } from "./gen.ts";
23
+ export { collectArtifacts, runGen, joinPath, readModels, expandPattern, previewModels } from "./gen.ts";
12
24
  export type { GenIO, GenOptions, GenResult } from "./gen.ts";
13
25
  export { createNodeGenIO } from "./fsio.ts";
26
+ export { deriveModels, isWorkflow, bpmnFilename, processesOutDir, DEFAULT_WORKFLOW_PATTERNS, PROCESSES_DIR, MODEL_PROVENANCE, } from "./models.ts";
27
+ export type { DerivedModels, DerivedModelInfo, ModelError, ModelsManifest } from "./models.ts";
14
28
  export { planWorkerScaffold, renderWorkerStub, slugifyTaskType, } from "./scaffold/workers.ts";
15
29
  export type { ScaffoldWorker, SkippedWorker, SkipReason, StubManifestEntry, WorkerScaffoldPlan, WorkerStubPlan, } from "./scaffold/workers.ts";
16
30
  export { scaffoldWorkers } from "./scaffold.ts";
@@ -4,15 +4,24 @@
4
4
  // (ADR 0054) — the toolkit derives migrations and worker I/O from the model.
5
5
  export { GENERATED_DIR, sortArtifacts } from "./artifact.js";
6
6
  // Derivers
7
- export { deriveMigrations, migrationsDeriver, sqlType, createTableSql, } from "./derivers/migrations.js";
7
+ export { deriveMigrations, migrationsDeriver, sqlType, createTableSql, tableSchemaForType, groupTypesBySource, } from "./derivers/migrations.js";
8
+ export { deriveDomain, emitDomain, emitDomainModel, emitDomainDts, emitDomainDtsForSources, emitDomainTypeRegistry, domainDeriver, sourcesFromManifest, registryFromManifest, sqliteAffinityToTs, interfaceName, } from "./derivers/domain.js";
8
9
  export { deriveWorkerBindings, emitWorkerBindings, workerIoDeriver, scanModelWorkers, WORKER_BINDINGS_DTS, DOMAIN_DTS, } from "./derivers/worker-io.js";
10
+ export { byModelPath, typeRefFor } from "./derivers/worker-io.js";
11
+ export { deriveMeta, emitMeta, foldMeta, metaDeriver, scanModelMeta, META_TS, } from "./derivers/meta.js";
12
+ export { deriveMessageBindings, emitMessageBindings, messagesDeriver, scanModelMessages, MESSAGE_BINDINGS_DTS, } from "./derivers/messages.js";
9
13
  // The registry of all derivers (for discovery / IDE migration).
10
14
  import { migrationsDeriver } from "./derivers/migrations.js";
15
+ import { domainDeriver } from "./derivers/domain.js";
11
16
  import { workerIoDeriver } from "./derivers/worker-io.js";
12
- export const DERIVERS = [migrationsDeriver, workerIoDeriver];
17
+ import { metaDeriver } from "./derivers/meta.js";
18
+ import { messagesDeriver } from "./derivers/messages.js";
19
+ export const DERIVERS = [migrationsDeriver, domainDeriver, workerIoDeriver, metaDeriver, messagesDeriver];
13
20
  // Gen orchestrator + IO
14
- export { collectArtifacts, runGen, joinPath, readModels } from "./gen.js";
21
+ export { collectArtifacts, runGen, joinPath, readModels, expandPattern, previewModels } from "./gen.js";
15
22
  export { createNodeGenIO } from "./fsio.js";
23
+ // Code-first model derivation (workflows/*.ts → BPMN)
24
+ export { deriveModels, isWorkflow, bpmnFilename, processesOutDir, DEFAULT_WORKFLOW_PATTERNS, PROCESSES_DIR, MODEL_PROVENANCE, } from "./models.js";
16
25
  // Worker-stub scaffolder (ADR 0056): write-once handler stubs from the model.
17
26
  export { planWorkerScaffold, renderWorkerStub, slugifyTaskType, } from "./scaffold/workers.js";
18
27
  export { scaffoldWorkers } from "./scaffold.js";
@@ -0,0 +1,63 @@
1
+ import type { Workflow } from "@nanobpm/workflow";
2
+ import type { DerivedArtifact } from "./artifact.ts";
3
+ import type { GenIO } from "./gen.ts";
4
+ import type { ModelSource } from "./derivers/worker-io.ts";
5
+ /** Default source glob for code-first flows (matches the code-first scaffold layout). */
6
+ export declare const DEFAULT_WORKFLOW_PATTERNS: string[];
7
+ /** Default output dir for derived models (matches the model-first scaffold, so a derived
8
+ * model is ejectable to model-first and picked up by `models.processes: ["processes/*.bpmn"]`). */
9
+ export declare const PROCESSES_DIR = "processes";
10
+ /** Marker stamped into every derived `.bpmn` (an XML comment) so re-gen overwrites and the
11
+ * stale sweep deletes ONLY derived output — never an authored, human-owned model. */
12
+ export declare const MODEL_PROVENANCE = "Generated by @nanobpm/urban from workflows/*.ts \u2014 do not edit; edit the flow and re-run `urban gen`.";
13
+ /** Minimal shape a code-first manifest declares for model derivation. */
14
+ export interface ModelsManifest {
15
+ models?: {
16
+ processes?: string[];
17
+ workflows?: string[];
18
+ };
19
+ }
20
+ /** A `defineFlow` export: has a string `id` and an `imperative`/`declarative` `kind`. */
21
+ export declare function isWorkflow(v: unknown): v is Workflow;
22
+ /** Sanitize a flow id into a safe `.bpmn` filename (mirrors the host generator). */
23
+ export declare function bpmnFilename(id: string): string;
24
+ /** The directory derived models are written to: the dir of the first `models.processes`
25
+ * glob if the manifest declares one, else the `processes/` default. */
26
+ export declare function processesOutDir(manifest: ModelsManifest): string;
27
+ export interface ModelError {
28
+ path: string;
29
+ message: string;
30
+ }
31
+ export interface DerivedModels {
32
+ /** `processes/<id>.bpmn` artifacts (path + provenance-stamped content). */
33
+ artifacts: DerivedArtifact[];
34
+ /** The same models, in-memory, for feeding the type-contract derivers (worker I/O, meta, messages). */
35
+ models: ModelSource[];
36
+ /** Derived flow ids (used for the provenance-scoped stale sweep). */
37
+ ids: string[];
38
+ /** Per-flow `{ id, kind, xml }` for the non-writing preview (`urban derive --stdout`). */
39
+ list: DerivedModelInfo[];
40
+ /** The output directory the artifacts were written into. */
41
+ outDir: string;
42
+ /** True once we attempted derivation (loader present AND at least one workflow file matched). */
43
+ attempted: boolean;
44
+ /** True if any workflow module failed to import/derive — suppresses the destructive sweep. */
45
+ incomplete: boolean;
46
+ /** Per-file derivation errors (empty on full success). */
47
+ errors: ModelError[];
48
+ }
49
+ export interface DerivedModelInfo {
50
+ id: string;
51
+ kind: Workflow["kind"];
52
+ xml: string;
53
+ }
54
+ /**
55
+ * Derive executable BPMN models from an app's code-first `workflows/*.ts`.
56
+ *
57
+ * Returns everything in-memory (artifacts + `ModelSource`s + ids). The caller decides whether
58
+ * to write the `.bpmn` to disk (`urban gen` / `urban derive`) or keep them in memory to feed
59
+ * the type-contract derivers only (`urban gen --no-models`) or preview them (`urban derive
60
+ * --stdout`). Never throws on a bad workflow: a failing module is recorded in `errors`,
61
+ * `incomplete` is set, and the sweep is suppressed so a partial failure can't delete good models.
62
+ */
63
+ export declare function deriveModels(root: string, io: GenIO, manifest: ModelsManifest): Promise<DerivedModels>;