@nanobpm/urban 0.39.0 → 0.40.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 (44) hide show
  1. package/README.md +37 -0
  2. package/dist/runtime/adapters/deno.js +18 -2
  3. package/dist/runtime/adapters/node.js +17 -5
  4. package/dist/runtime/core/context.d.ts +10 -1
  5. package/dist/runtime/core/host.d.ts +9 -2
  6. package/dist/runtime/core/logger.d.ts +69 -0
  7. package/dist/runtime/core/logger.js +134 -0
  8. package/dist/runtime/core/modules/api.js +7 -1
  9. package/dist/runtime/core/modules/pages.js +52 -4
  10. package/dist/runtime/core/modules/workers.js +16 -1
  11. package/dist/runtime/core/runtime.d.ts +5 -0
  12. package/dist/runtime/core/runtime.js +7 -1
  13. package/dist/runtime/engine/nanosdk.d.ts +1 -1
  14. package/dist/runtime/index.d.ts +1 -0
  15. package/dist/toolkit/scaffold/operations.js +3 -1
  16. package/dist/toolkit/scaffold/workers.js +2 -0
  17. package/dist/worker.d.ts +1 -0
  18. package/package.json +2 -2
  19. package/src/runtime/adapters/deno.ts +17 -4
  20. package/src/runtime/adapters/node.ts +17 -7
  21. package/src/runtime/core/context.ts +10 -1
  22. package/src/runtime/core/host.ts +9 -2
  23. package/src/runtime/core/logger.test.ts +221 -0
  24. package/src/runtime/core/logger.ts +177 -0
  25. package/src/runtime/core/modules/actions.test.ts +2 -1
  26. package/src/runtime/core/modules/api.test.ts +38 -2
  27. package/src/runtime/core/modules/api.ts +10 -1
  28. package/src/runtime/core/modules/cancel.test.ts +4 -1
  29. package/src/runtime/core/modules/connectors.test.ts +2 -1
  30. package/src/runtime/core/modules/instance-tracking.test.ts +4 -1
  31. package/src/runtime/core/modules/pages.test.ts +31 -0
  32. package/src/runtime/core/modules/pages.ts +52 -4
  33. package/src/runtime/core/modules/surfaces.test.ts +2 -1
  34. package/src/runtime/core/modules/triggers.test.ts +4 -1
  35. package/src/runtime/core/modules/workers.test.ts +59 -2
  36. package/src/runtime/core/modules/workers.ts +15 -1
  37. package/src/runtime/core/runtime.test.ts +22 -0
  38. package/src/runtime/core/runtime.ts +12 -1
  39. package/src/runtime/devserver.test.ts +2 -0
  40. package/src/runtime/engine/nanosdk.ts +1 -1
  41. package/src/runtime/index.ts +2 -0
  42. package/src/toolkit/scaffold/operations.ts +3 -1
  43. package/src/toolkit/scaffold/workers.ts +2 -0
  44. package/src/worker.ts +1 -0
@@ -1,4 +1,5 @@
1
1
  import { test } from "node:test";
2
+ import { createLogger } from "../logger.ts";
2
3
  import assert from "node:assert/strict";
3
4
  import { mkdtemp, rm } from "node:fs/promises";
4
5
  import { tmpdir } from "node:os";
@@ -148,7 +149,9 @@ async function withHarness(
148
149
  data,
149
150
  engine,
150
151
  env: () => undefined,
151
- log: (level, msg) => logs.push({ level, msg }),
152
+ log: createLogger((level, msg) => {
153
+ logs.push({ level, msg });
154
+ }),
152
155
  };
153
156
  return {
154
157
  api,
@@ -262,6 +262,37 @@ test("renderer wires a column's processExplorer link to the console explorer", a
262
262
  assert.match(js, /"\/console\/explorer\?instance="\s*\+\s*encodeURIComponent\(keyStr\)/);
263
263
  assert.match(js, /target:\s*"_blank"/);
264
264
  assert.match(js, /rel:\s*"noopener noreferrer"/);
265
+ // Embedded in the console, a plain click routes the host explorer in place via
266
+ // hostNavigate("processExplorer", {instance}) + preventDefault; a modified
267
+ // click (new-tab intent) short-circuits so the native _blank anchor wins.
268
+ assert.match(js, /hostNavigate\("processExplorer",\s*\{\s*instance:\s*keyStr\s*\}\)/);
269
+ // Non-primary buttons (middle/right) and modified clicks keep the native
270
+ // new-tab behavior; only a plain primary click routes in-host.
271
+ assert.match(js, /ev\.button !== 0/);
272
+ assert.match(js, /ev\.metaKey \|\| ev\.ctrlKey \|\| ev\.shiftKey \|\| ev\.altKey/);
273
+ assert.match(js, /ev\.preventDefault\(\)/);
274
+ });
275
+
276
+ test("renderer exposes an embed-gated host-navigation bridge", async () => {
277
+ const res = await dispatch("GET", "/app/runtime.js");
278
+ const js = res.body ?? "";
279
+ // In-host navigation is gated on being embedded in a SAME-ORIGIN console
280
+ // iframe: standalone (or a cross-origin framer, where postMessage to our own
281
+ // origin could never be delivered) hostNavigate returns false so callers keep
282
+ // the native new-window anchor. The payload is structured (target + params)
283
+ // and posted to the console origin — the host builds its own route, so row
284
+ // data can't smuggle a path. The same-origin gate reads parent.location.origin
285
+ // in a try/catch (a cross-origin parent throws under the same-origin policy).
286
+ assert.match(js, /window\.parent\.location\.origin === window\.location\.origin/);
287
+ assert.match(js, /const NANO_EMBEDDED =/);
288
+ assert.match(js, /function hostNavigate\(target, params\)/);
289
+ assert.match(js, /if \(!NANO_EMBEDDED\) return false;/);
290
+ assert.match(
291
+ js,
292
+ /window\.parent\.postMessage\(\s*\{\s*type:\s*"nano-navigate",\s*target:\s*target,\s*params:\s*params\s*\}\s*,\s*window\.location\.origin\s*\)/,
293
+ );
294
+ // The theme bridge reuses the same embed flag (no second window.parent probe).
295
+ assert.match(js, /if \(NANO_EMBEDDED\) \{/);
265
296
  });
266
297
 
267
298
  test("the renderer honours numeric actionForm fields", async () => {
@@ -608,7 +608,36 @@ function applyTheme(msg) {
608
608
  }
609
609
  }
610
610
  }
611
- if (window.parent && window.parent !== window) {
611
+ // True when this app runs inside the Nano console's same-origin iframe (vs a
612
+ // standalone CLI run). Gates the theme bridge below AND in-host link navigation:
613
+ // embedded, a processExplorer link routes the console in place; standalone, the
614
+ // link keeps its native new-window behavior. The same-origin probe matters: a
615
+ // cross-origin framer is also window.parent, but postMessage to our own origin
616
+ // would never reach it and hostNavigate would still preventDefault the link —
617
+ // so a cross-origin embed must fall through to the native _blank anchor. Reading
618
+ // parent.location.origin throws under the same-origin policy for a cross-origin
619
+ // parent, so the try/catch is the origin gate.
620
+ const NANO_EMBEDDED = (() => {
621
+ if (!window.parent || window.parent === window) return false;
622
+ try {
623
+ return window.parent.location.origin === window.location.origin;
624
+ } catch {
625
+ return false;
626
+ }
627
+ })();
628
+
629
+ // Ask the framing console to navigate in-host. Returns true when a message was
630
+ // posted (embedded) so the caller can suppress the native anchor; false
631
+ // standalone so it falls through to the new-window <a>. The payload is
632
+ // structured (target + params), never a raw href — the console constructs its
633
+ // own route, so app/row data can't smuggle a path or scheme across the boundary.
634
+ function hostNavigate(target, params) {
635
+ if (!NANO_EMBEDDED) return false;
636
+ window.parent.postMessage({ type: "nano-navigate", target: target, params: params }, window.location.origin);
637
+ return true;
638
+ }
639
+
640
+ if (NANO_EMBEDDED) {
612
641
  window.addEventListener("message", (ev) => {
613
642
  // Same-origin proxy (posture A): only trust a message from the framing
614
643
  // parent AND from our own origin. ev.source alone is insufficient — a
@@ -723,8 +752,12 @@ function renderText(node) {
723
752
  // non-empty key (after trimming surrounding whitespace) produces a link.
724
753
  // Unknown link kinds fall back to plain text so an unrecognised schema
725
754
  // can't render a broken anchor.
726
- // Both open in a new tab with rel=noopener noreferrer so the opened page can't
727
- // reach window.opener. Shared by the top-level grid and child grids.
755
+ // The external linkField anchor always opens in a new tab (rel=noopener
756
+ // noreferrer). The processExplorer anchor does too when the app runs
757
+ // standalone, but when embedded in the console a plain click instead routes the
758
+ // host's own explorer view in place via the nano-navigate postMessage bridge
759
+ // (a modified/middle click keeps the new-tab behavior). Shared by the top-level
760
+ // grid and child grids.
728
761
  function gridCell(col, row) {
729
762
  const text = row[col.field] == null ? "" : String(row[col.field]);
730
763
  if (col.linkField) {
@@ -745,7 +778,22 @@ function gridCell(col, row) {
745
778
  return el(
746
779
  "td",
747
780
  {},
748
- el("a", { class: "pc-link", href, target: "_blank", rel: "noopener noreferrer" }, text),
781
+ el("a", {
782
+ class: "pc-link",
783
+ href,
784
+ target: "_blank",
785
+ rel: "noopener noreferrer",
786
+ // Embedded in the console a plain primary click routes the host's
787
+ // explorer in place (via the nano-navigate bridge) instead of opening
788
+ // a new window. A non-primary button (middle/right = new-tab/context
789
+ // intent), a modified click (⌘/ctrl/shift/alt = new-tab intent) and
790
+ // any standalone run fall through to the native anchor above.
791
+ onclick: (ev) => {
792
+ if (ev.button !== 0) return;
793
+ if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
794
+ if (hostNavigate("processExplorer", { instance: keyStr })) ev.preventDefault();
795
+ },
796
+ }, text),
749
797
  );
750
798
  }
751
799
  }
@@ -6,6 +6,7 @@ import assert from "node:assert/strict";
6
6
  import { mountSurfaces } from "./surfaces.ts";
7
7
  import type { EngineClient, HostContext, HttpRequest, HttpResponse } from "../host.ts";
8
8
  import { DataLayer } from "./datasource.ts";
9
+ import { createLogger } from "../logger.ts";
9
10
 
10
11
  function ctxWith(surfaces: Record<string, unknown>): Parameters<typeof mountSurfaces>[0] {
11
12
  const host: HostContext = {
@@ -53,7 +54,7 @@ const fakeApp: Parameters<typeof mountSurfaces>[1] = {
53
54
  data: new DataLayer(new Map(), undefined, {}),
54
55
  engine: fakeEngine,
55
56
  env: () => undefined,
56
- log: () => {},
57
+ log: createLogger(() => {}),
57
58
  };
58
59
 
59
60
  async function render(route: { handler: (req: HttpRequest) => HttpResponse | Promise<HttpResponse> }): Promise<string> {
@@ -1,4 +1,5 @@
1
1
  import { test } from "node:test";
2
+ import { createLogger } from "../logger.ts";
2
3
  import assert from "node:assert/strict";
3
4
  import type { AppApi, RuntimeContext } from "../context.ts";
4
5
  import type { EngineClient, HostContext, HttpRequest } from "../host.ts";
@@ -46,7 +47,9 @@ function fakeApp(): { app: AppApi; calls: EngineCall[]; logs: Array<{ level: str
46
47
  data: new DataLayer(new Map(), undefined, {}),
47
48
  engine,
48
49
  env: () => undefined,
49
- log: (level: string, msg: string) => logs.push({ level, msg }),
50
+ log: createLogger((level: string, msg: string) => {
51
+ logs.push({ level, msg });
52
+ }),
50
53
  };
51
54
  return { app, calls, logs };
52
55
  }
@@ -1,4 +1,5 @@
1
1
  import { test } from "node:test";
2
+ import { createLogger } from "../logger.ts";
2
3
  import assert from "node:assert/strict";
3
4
  import type { AppApi, RuntimeContext } from "../context.ts";
4
5
  import type { EngineClient, EngineJob, HostContext, JobHandler, WorkerSubscription } from "../host.ts";
@@ -63,7 +64,7 @@ function makeApp(over: Partial<AppApi> = {}): AppApi {
63
64
  data: new DataLayer(new Map(), undefined, {}),
64
65
  engine: new MiniEngine(),
65
66
  env: (n) => env[n],
66
- log: () => {},
67
+ log: createLogger(() => {}),
67
68
  ...over,
68
69
  };
69
70
  }
@@ -194,7 +195,63 @@ test("mountWorkers silently skips a connector-backed worker (mountConnectors own
194
195
  assert.ok(!logs.some((l) => l.msg.includes("neither a handler nor an llm")));
195
196
  });
196
197
 
197
- // ── AppJobHandler generics: optional In/Out type parameters ──────────────────────────────
198
+ test("mountWorkers binds the job's correlation context onto the handler's app.log", async () => {
199
+ const engine = new MiniEngine();
200
+ const { ctx } = makeCtx({ workers: [{ taskType: "charge", handler: "workers/charge.ts" }] }, engine);
201
+ const captured: Array<{ level: string; msg: string; fields?: Record<string, unknown> }> = [];
202
+ // The handler logs through the injected app; assert the runtime pre-bound the job context.
203
+ const handler: AppJobHandler = (_job, app) => {
204
+ app.log.info("charging", { amount: 100 });
205
+ return { ok: true };
206
+ };
207
+ ctx.host.importModule = async () => ({ default: handler });
208
+ const app = makeApp({
209
+ log: createLogger((level, msg, fields) => {
210
+ captured.push({ level, msg, fields });
211
+ }),
212
+ });
213
+ await mountWorkers(ctx, app);
214
+ await engine.deliver("charge", {
215
+ jobKey: "j-7",
216
+ jobType: "charge",
217
+ processInstanceKey: "pi-42",
218
+ elementId: "Task_Charge",
219
+ variables: {},
220
+ });
221
+ assert.equal(captured.length, 1);
222
+ assert.equal(captured[0].level, "info");
223
+ assert.equal(captured[0].msg, "charging");
224
+ assert.deepEqual(
225
+ { ...captured[0].fields },
226
+ {
227
+ jobKey: "j-7",
228
+ jobType: "charge",
229
+ processInstanceKey: "pi-42",
230
+ elementId: "Task_Charge",
231
+ amount: 100,
232
+ },
233
+ );
234
+ });
235
+
236
+ test("mountWorkers omits absent instance/element from the bound log context", async () => {
237
+ const engine = new MiniEngine();
238
+ const { ctx } = makeCtx({ workers: [{ taskType: "ping", handler: "workers/ping.ts" }] }, engine);
239
+ const captured: Array<Record<string, unknown> | undefined> = [];
240
+ const handler: AppJobHandler = (_job, app) => {
241
+ app.log.warn("no instance");
242
+ };
243
+ ctx.host.importModule = async () => ({ default: handler });
244
+ const app = makeApp({
245
+ log: createLogger((_level, _msg, fields) => {
246
+ captured.push(fields);
247
+ }),
248
+ });
249
+ await mountWorkers(ctx, app);
250
+ await engine.deliver("ping", { jobKey: "j-1", jobType: "ping", variables: {} });
251
+ assert.equal(captured.length, 1);
252
+ assert.deepEqual({ ...captured[0] }, { jobKey: "j-1", jobType: "ping" });
253
+ });
254
+
198
255
  // These are primarily compile-time assertions (the suite is typechecked by `tsc --noEmit`),
199
256
  // with a runtime smoke that a typed handler still returns its output when delivered a job.
200
257
  test("AppJobHandler carries optional In/Out variable types", async () => {
@@ -72,6 +72,20 @@ function isAppJobHandler(value: unknown): value is AppJobHandler {
72
72
  return typeof value === "function";
73
73
  }
74
74
 
75
+ /**
76
+ * Hand a handler an {@link AppApi} whose `log` is bound to the job's correlation context, so every
77
+ * line the handler emits self-tags with `{ jobKey, jobType, processInstanceKey, elementId }` without
78
+ * the author threading it. Undefined fields (e.g. a job dispatched without an instance) are omitted
79
+ * so the bound context stays clean. The rest of the API is shared by reference — only `log` is
80
+ * per-job.
81
+ */
82
+ function withJobLog(app: AppApi, job: EngineJob): AppApi {
83
+ const bindings: Record<string, unknown> = { jobKey: job.jobKey, jobType: job.jobType };
84
+ if (job.processInstanceKey !== undefined) bindings.processInstanceKey = job.processInstanceKey;
85
+ if (job.elementId !== undefined) bindings.elementId = job.elementId;
86
+ return { ...app, log: app.log.child(bindings) };
87
+ }
88
+
75
89
  /**
76
90
  * Resolve a handler for `jobType` from a loaded module, in priority order:
77
91
  * 1. `handlers[jobType]` (a map keyed by job type — the multi-type module case)
@@ -167,7 +181,7 @@ export async function mountWorkers(ctx: RuntimeContext, app: AppApi): Promise<Wo
167
181
  const wrapped: JobHandler = (job) =>
168
182
  runInJobContext(
169
183
  { instanceKey: job.processInstanceKey, elementId: job.elementId, jobType },
170
- () => handler(job, app),
184
+ () => handler(job, withJobLog(app, job)),
171
185
  );
172
186
  const sub = await ctx.engine.registerWorker(jobType, wrapped, {
173
187
  workerName: `${ctx.manifest.id}:${jobType}`,
@@ -304,6 +304,28 @@ test("task-inbox /api/complete returns 400 on a malformed JSON body", async () =
304
304
  }
305
305
  });
306
306
 
307
+ test("app.log is an app-level structured logger routed to the host sink", async () => {
308
+ const dir = await makeFixture();
309
+ const logs: Array<{ level: string; msg: string; fields?: Record<string, unknown> }> = [];
310
+ const host = createNodeHost({ cwd: dir, log: (level, msg, fields) => logs.push({ level, msg, fields }) });
311
+ const engine = new FakeEngine();
312
+ const app = await createUrbanApp({ host, engine, root: ".", port: 0 });
313
+ app.log.info("booting", { pid: 1 });
314
+ assert.deepEqual(
315
+ // `fields` is the merged bag, deliberately a null-prototype object (untrusted-key hardening);
316
+ // spread it so the assertion compares contents, not prototype.
317
+ logs.filter((l) => l.msg === "booting").map((l) => ({ ...l, fields: { ...l.fields } })),
318
+ [{ level: "info", msg: "booting", fields: { pid: 1 } }],
319
+ );
320
+ // ergonomic methods + child binding are present on the handle logger
321
+ const bound = app.log.child({ region: "eu" });
322
+ bound.warn("slow");
323
+ assert.deepEqual(
324
+ logs.filter((l) => l.msg === "slow").map((l) => ({ ...l, fields: { ...l.fields } })),
325
+ [{ level: "warn", msg: "slow", fields: { region: "eu" } }],
326
+ );
327
+ });
328
+
307
329
  test("resolvePort prefers explicit, then $PORT, then 8090; rejects bad $PORT", () => {
308
330
  assert.equal(resolvePort(3000, "9999"), 3000);
309
331
  assert.equal(resolvePort(undefined, "9999"), 9999);
@@ -13,6 +13,7 @@ import { deployModels } from "./modules/deploy.ts";
13
13
  import type { TemplateSource } from "./modules/templates.ts";
14
14
  import { provisionData, DataLayer } from "./modules/datasource.ts";
15
15
  import { installExecStore, type JobExecContext } from "./execContext.ts";
16
+ import { createLogger, type Logger } from "./logger.ts";
16
17
  import { mountWorkers } from "./modules/workers.ts";
17
18
  import { mountConnectors } from "./modules/connectors.ts";
18
19
  import { mountSurfaces } from "./modules/surfaces.ts";
@@ -99,6 +100,10 @@ export interface UrbanApp {
99
100
  stop(): Promise<void>;
100
101
  /** A structured snapshot of what is mounted. */
101
102
  inspect(): Record<string, unknown>;
103
+ /** An app-level structured {@link Logger} for the entrypoint (`main.ts`). It carries no
104
+ * per-request/per-job correlation (worker handlers and route delegates get a child logger bound
105
+ * to their job/request context via `AppApi.log`); use it for boot/shutdown lifecycle lines. */
106
+ readonly log: Logger;
102
107
  /** The provisioned data layer (available after start when `data` is mounted). */
103
108
  readonly data: DataLayer | undefined;
104
109
  readonly security: SecurityPolicy | undefined;
@@ -123,6 +128,11 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
123
128
  };
124
129
  const ctx = { manifest, host, engine, root, templates: opts.templates };
125
130
 
131
+ // One app-level structured logger over the host sink, shared by the entrypoint (exposed as
132
+ // `app.log`) and the per-invocation `AppApi.log` (workers/route delegates `child()` it with their
133
+ // correlation context).
134
+ const appLog = createLogger((l, m, f) => host.log(l, m, f));
135
+
126
136
  // Install the ambient job-execution store once per process (idempotent) so worker dispatch can
127
137
  // stamp write-provenance onto DataLayer inserts. Absent-safe: a host without `createAsyncStore`
128
138
  // leaves provenance capture disabled.
@@ -173,6 +183,7 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
173
183
  const app: UrbanApp = {
174
184
  manifest,
175
185
  root,
186
+ log: appLog,
176
187
  get data() {
177
188
  return data;
178
189
  },
@@ -197,7 +208,7 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
197
208
  engine,
198
209
  sdk: engineSdk(engine),
199
210
  env: (n) => host.env(n),
200
- log: (l, m, f) => host.log(l, m, f),
211
+ log: appLog,
201
212
  };
202
213
 
203
214
  if (flags.workers) {
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { runDev, shouldReload, type DevDeps } from "./devserver.ts";
4
4
  import type { HostContext } from "./core/host.ts";
5
5
  import type { UrbanApp } from "./core/runtime.ts";
6
+ import { createLogger } from "./core/logger.ts";
6
7
 
7
8
  const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
8
9
 
@@ -226,6 +227,7 @@ function fakeApp(stops: string[], id: string): UrbanApp {
226
227
  stops.push(id);
227
228
  },
228
229
  inspect: () => ({ app: id }),
230
+ log: createLogger(() => {}),
229
231
  data: undefined,
230
232
  security: undefined,
231
233
  httpPort: undefined,
@@ -138,7 +138,7 @@ export interface NanoSdkClient {
138
138
  close?(): void | Promise<void>;
139
139
  }
140
140
 
141
- type Log = (level: "info" | "warn" | "error", msg: string, fields?: Record<string, unknown>) => void;
141
+ type Log = (level: "debug" | "info" | "warn" | "error", msg: string, fields?: Record<string, unknown>) => void;
142
142
 
143
143
  /**
144
144
  * An `EngineClient` backed entirely by a `@nanobpm/nano-sdk` client. Deploy, create,
@@ -54,6 +54,8 @@ export type {
54
54
  WorkerSubscription,
55
55
  } from "./core/host.ts";
56
56
  export type { AppApi, Mounted, RuntimeContext } from "./core/context.ts";
57
+ // Structured logging surface (see core/logger.ts): the shape of `AppApi.log`.
58
+ export type { Logger, LogLevel, LogFields } from "./core/logger.ts";
57
59
 
58
60
  // BPMN error contract: a handler throws `BpmnError(code)` to raise a modelled
59
61
  // error routed to an error boundary (ADR 0050), instead of a retryable failure.
@@ -50,7 +50,9 @@ export function renderOperationStub(operationId: string, dir: string = DEFAULT_O
50
50
  `\n` +
51
51
  `export default defineOperation(${key}, async (input, app) => {\n` +
52
52
  ` // \`input\` is the validated request (params/query/body/req); reach app state + services\n` +
53
- ` // through the injected \`app\` API. Return \`{ status?, headers?, body? }\` (or nothing → 204).\n` +
53
+ ` // through the injected \`app\` API. Structured logs via \`app.log.info("…", { … })\` are\n` +
54
+ ` // auto-tagged with this request's { method, path, operationId } (ADR 0061).\n` +
55
+ ` // Return \`{ status?, headers?, body? }\` (or nothing → 204).\n` +
54
56
  ` throw new NotImplemented(${key});\n` +
55
57
  `});\n`
56
58
  );
@@ -101,6 +101,8 @@ export function renderWorkerStub(
101
101
  varsNote +
102
102
  `\n` +
103
103
  ` // Reach app state and services through the injected \`app\` API.\n` +
104
+ ` // Structured logs: \`app.log.info("done", { … })\` — auto-tagged with this job's\n` +
105
+ ` // { jobKey, jobType, processInstanceKey, elementId } (ADR 0061).\n` +
104
106
  ` throw new Error(${JSON.stringify(`worker not implemented: ${taskType}`)});\n` +
105
107
  `};\n` +
106
108
  `\n` +
package/src/worker.ts CHANGED
@@ -19,3 +19,4 @@
19
19
  export type { AppJobHandler } from "./runtime/core/modules/workers.ts";
20
20
  export type { EngineJob, JobHandler } from "./runtime/core/host.ts";
21
21
  export type { AppApi } from "./runtime/core/context.ts";
22
+ export type { Logger, LogLevel, LogFields } from "./runtime/core/logger.ts";