@pattern-stack/codegen 0.24.0 → 0.26.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 (81) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/{chunk-2VHZ7EKC.js → chunk-5AAA4LTE.js} +2 -2
  3. package/dist/{chunk-CKLM57IE.js → chunk-AC6T2JUX.js} +14 -14
  4. package/dist/{chunk-AS3NAZB6.js → chunk-B7SC2V45.js} +2 -2
  5. package/dist/{chunk-4MVGAMUA.js → chunk-BK5ICA2F.js} +4 -4
  6. package/dist/{chunk-W72PRNJY.js → chunk-BPYZCEHS.js} +2 -2
  7. package/dist/{chunk-J7JMVS2B.js → chunk-CZQUOIDY.js} +4 -4
  8. package/dist/{chunk-KYR3B3OW.js → chunk-DAHWN63L.js} +26 -5
  9. package/dist/chunk-DAHWN63L.js.map +1 -0
  10. package/dist/{chunk-37PILMIT.js → chunk-EEGVDRZE.js} +4 -4
  11. package/dist/{chunk-RFH7N6EP.js → chunk-FCPTHS42.js} +2 -2
  12. package/dist/chunk-H6QRQUAF.js +54 -0
  13. package/dist/chunk-H6QRQUAF.js.map +1 -0
  14. package/dist/{chunk-ENAR3F5S.js → chunk-HEOISQ6W.js} +6 -6
  15. package/dist/{chunk-CDLWYZVQ.js → chunk-IN3EWFB4.js} +4 -4
  16. package/dist/{chunk-WZOPWQN2.js → chunk-IXAE6BN6.js} +3 -3
  17. package/dist/{chunk-YULGWXCY.js → chunk-MVKW2BCR.js} +4 -4
  18. package/dist/{chunk-KFXXOFDC.js → chunk-N43D57AP.js} +2 -2
  19. package/dist/{chunk-JYBFPNBJ.js → chunk-SJGEBMJT.js} +2 -2
  20. package/dist/{chunk-HN5HT5WL.js → chunk-VI2VNA6Y.js} +4 -4
  21. package/dist/runtime/base-classes/activity-entity-service.js +3 -3
  22. package/dist/runtime/base-classes/base-service.js +2 -2
  23. package/dist/runtime/base-classes/index.js +6 -6
  24. package/dist/runtime/base-classes/integrated-entity-service.js +3 -3
  25. package/dist/runtime/base-classes/knowledge-entity-service.js +3 -3
  26. package/dist/runtime/base-classes/lifecycle-events.js +1 -1
  27. package/dist/runtime/base-classes/metadata-entity-service.js +3 -3
  28. package/dist/runtime/shared/logging/compact-console-logger.d.ts +55 -0
  29. package/dist/runtime/shared/logging/compact-console-logger.js +12 -0
  30. package/dist/runtime/shared/logging/compact-console-logger.js.map +1 -0
  31. package/dist/runtime/shared/logging/index.d.ts +2 -0
  32. package/dist/runtime/shared/logging/index.js +12 -0
  33. package/dist/runtime/shared/logging/index.js.map +1 -0
  34. package/dist/runtime/subsystems/analytics/analytics.module.js +2 -2
  35. package/dist/runtime/subsystems/analytics/index.js +4 -4
  36. package/dist/runtime/subsystems/auth/auth.module.js +1 -1
  37. package/dist/runtime/subsystems/auth/index.js +3 -3
  38. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +2 -2
  39. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +1 -1
  40. package/dist/runtime/subsystems/bridge/bridge.module.js +11 -11
  41. package/dist/runtime/subsystems/bridge/index.js +13 -13
  42. package/dist/runtime/subsystems/index.js +49 -49
  43. package/dist/runtime/subsystems/integration/index.js +12 -12
  44. package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
  45. package/dist/runtime/subsystems/integration/integration.module.js +3 -3
  46. package/dist/runtime/subsystems/jobs/index.js +24 -24
  47. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +4 -4
  48. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +2 -2
  49. package/dist/runtime/subsystems/jobs/job-worker.module.js +8 -8
  50. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +7 -7
  51. package/dist/runtime/subsystems/observability/index.js +2 -2
  52. package/dist/runtime/subsystems/observability/observability.module.js +2 -2
  53. package/dist/runtime/subsystems/storage/index.js +4 -4
  54. package/dist/runtime/subsystems/storage/storage.module.js +2 -2
  55. package/dist/src/cli/index.js +101 -31
  56. package/dist/src/cli/index.js.map +1 -1
  57. package/dist/src/index.js +4 -4
  58. package/package.json +5 -1
  59. package/runtime/base-classes/lifecycle-events.ts +39 -6
  60. package/runtime/shared/logging/compact-console-logger.ts +102 -0
  61. package/runtime/shared/logging/index.ts +10 -0
  62. package/templates/subsystem/jobs/job-orchestration.schema.ejs.t +1 -0
  63. package/templates/subsystem/jobs/main-hook.ejs.t +1 -1
  64. package/templates/subsystem/jobs/prompt.js +40 -2
  65. package/templates/subsystem/jobs/worker.ejs.t +47 -35
  66. package/dist/chunk-KYR3B3OW.js.map +0 -1
  67. /package/dist/{chunk-2VHZ7EKC.js.map → chunk-5AAA4LTE.js.map} +0 -0
  68. /package/dist/{chunk-CKLM57IE.js.map → chunk-AC6T2JUX.js.map} +0 -0
  69. /package/dist/{chunk-AS3NAZB6.js.map → chunk-B7SC2V45.js.map} +0 -0
  70. /package/dist/{chunk-4MVGAMUA.js.map → chunk-BK5ICA2F.js.map} +0 -0
  71. /package/dist/{chunk-W72PRNJY.js.map → chunk-BPYZCEHS.js.map} +0 -0
  72. /package/dist/{chunk-J7JMVS2B.js.map → chunk-CZQUOIDY.js.map} +0 -0
  73. /package/dist/{chunk-37PILMIT.js.map → chunk-EEGVDRZE.js.map} +0 -0
  74. /package/dist/{chunk-RFH7N6EP.js.map → chunk-FCPTHS42.js.map} +0 -0
  75. /package/dist/{chunk-ENAR3F5S.js.map → chunk-HEOISQ6W.js.map} +0 -0
  76. /package/dist/{chunk-CDLWYZVQ.js.map → chunk-IN3EWFB4.js.map} +0 -0
  77. /package/dist/{chunk-WZOPWQN2.js.map → chunk-IXAE6BN6.js.map} +0 -0
  78. /package/dist/{chunk-YULGWXCY.js.map → chunk-MVKW2BCR.js.map} +0 -0
  79. /package/dist/{chunk-KFXXOFDC.js.map → chunk-N43D57AP.js.map} +0 -0
  80. /package/dist/{chunk-JYBFPNBJ.js.map → chunk-SJGEBMJT.js.map} +0 -0
  81. /package/dist/{chunk-HN5HT5WL.js.map → chunk-VI2VNA6Y.js.map} +0 -0
package/dist/src/index.js CHANGED
@@ -47,14 +47,15 @@ import {
47
47
  validatePatternProject
48
48
  } from "../chunk-K4BQQ2NN.js";
49
49
  import "../chunk-KVOWSC5S.js";
50
+ import "../chunk-N43D57AP.js";
50
51
  import "../chunk-PRWIX6UW.js";
51
- import "../chunk-KFXXOFDC.js";
52
- import "../chunk-HN5HT5WL.js";
52
+ import "../chunk-VI2VNA6Y.js";
53
53
  import "../chunk-EO2QPOKH.js";
54
+ import "../chunk-SQDOBLBP.js";
55
+ import "../chunk-LG57S2SC.js";
54
56
  import "../chunk-LLDJS7PJ.js";
55
57
  import "../chunk-HNWZFNKP.js";
56
58
  import "../chunk-AHV4GDYM.js";
57
- import "../chunk-SQDOBLBP.js";
58
59
  import "../chunk-43SBT72G.js";
59
60
  import "../chunk-4MF3HKJA.js";
60
61
  import "../chunk-TIZXQU26.js";
@@ -65,7 +66,6 @@ import "../chunk-3CJFPU6Q.js";
65
66
  import "../chunk-YIVQ7KLS.js";
66
67
  import "../chunk-S5G3HO7N.js";
67
68
  import "../chunk-MZ6GV4YF.js";
68
- import "../chunk-LG57S2SC.js";
69
69
  import "../chunk-U64T4YZE.js";
70
70
  import "../chunk-2E224ZSN.js";
71
71
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pattern-stack/codegen",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Entity-driven code generation for full-stack TypeScript applications",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,6 +35,10 @@
35
35
  "types": "./dist/runtime/shared/openapi/index.d.ts",
36
36
  "default": "./dist/runtime/shared/openapi/index.js"
37
37
  },
38
+ "./runtime/shared/logging": {
39
+ "types": "./dist/runtime/shared/logging/index.d.ts",
40
+ "default": "./dist/runtime/shared/logging/index.js"
41
+ },
38
42
  "./runtime/*": {
39
43
  "types": "./dist/runtime/*.d.ts",
40
44
  "default": "./dist/runtime/*.js"
@@ -23,8 +23,17 @@
23
23
  */
24
24
 
25
25
  import { randomUUID } from 'crypto';
26
+ import { Logger } from '@nestjs/common';
26
27
  import type { IEventBus, DomainEvent } from '../subsystems/events/event-bus.protocol';
27
28
 
29
+ /**
30
+ * Module-level logger for fire-and-forget emission failures. Routed through the
31
+ * Nest `Logger` (not bare `console`) so consumers configuring `app.useLogger`
32
+ * or the factory `logger:` option can format and filter it like any other
33
+ * framework log line.
34
+ */
35
+ const logger = new Logger('LifecycleEvents');
36
+
28
37
  // ============================================================================
29
38
  // Event categories (subset of pattern-stack's EventCategory)
30
39
  // ============================================================================
@@ -99,7 +108,17 @@ export function buildLifecycleEvent(
99
108
  aggregateType: entityName,
100
109
  payload: snapshot ? { snapshot } : {},
101
110
  occurredAt: new Date(),
102
- metadata: { category: 'lifecycle' as EventCategory },
111
+ // AUDIT tier: lifecycle/change events are untyped audit-trail records —
112
+ // never bridge-routed, no pool/direction. The `domain_events`
113
+ // `domain_events_tier_routing_check` CHECK requires `tier='audit' ⇔
114
+ // (pool IS NULL AND direction IS NULL)`; the DEFAULT `tier='domain'`
115
+ // (applied by toInsertValues when absent) requires non-null routing
116
+ // fields, so an un-tiered lifecycle row violates the constraint and the
117
+ // INSERT is rejected — silently, pre-fix, by emitSafely's catch. Stamp
118
+ // `tier:'audit'` so these rows land (and surface under the
119
+ // observability viewer's audit-tier toggle). The bridge guard keeps
120
+ // audit-tier events out of job routing.
121
+ metadata: { category: 'lifecycle' as EventCategory, tier: 'audit' },
103
122
  };
104
123
  }
105
124
 
@@ -119,7 +138,9 @@ export function buildChangeEvents(
119
138
  newValue: c.newValue,
120
139
  },
121
140
  occurredAt: new Date(),
122
- metadata: { category: 'change' as EventCategory },
141
+ // AUDIT tier see buildLifecycleEvent. Change events are audit-trail
142
+ // records; tier:'audit' satisfies the tier-routing CHECK constraint.
143
+ metadata: { category: 'change' as EventCategory, tier: 'audit' },
123
144
  }));
124
145
  }
125
146
 
@@ -144,9 +165,21 @@ export async function emitSafely(
144
165
  } else {
145
166
  await eventBus.publishMany(events);
146
167
  }
147
- } catch {
148
- // Log but never fail the CRUD operation.
149
- // In production, this would use a structured logger.
150
- console.warn(`[lifecycle-events] failed to emit ${events.length} event(s)`);
168
+ } catch (err) {
169
+ // Never fail the CRUD operation — but surface the cause. The bare
170
+ // `catch` that used to live here swallowed the error entirely, so a
171
+ // failing bus printed `failed to emit N event(s)` with zero
172
+ // diagnosability. Route through the Nest Logger (not bare console) at
173
+ // warn level, including the distinct event types and the error message;
174
+ // the stack follows at debug so it's available without noising the
175
+ // default-threshold output.
176
+ const message = err instanceof Error ? err.message : String(err);
177
+ const types = [...new Set(events.map((e) => e.type))].join(', ');
178
+ logger.warn(
179
+ `failed to emit ${events.length} event(s) [${types}]: ${message}`,
180
+ );
181
+ if (err instanceof Error && err.stack) {
182
+ logger.debug(err.stack);
183
+ }
151
184
  }
152
185
  }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Compact console logger + `LOG_LEVEL` threshold.
3
+ *
4
+ * Nest's default `ConsoleLogger` preamble —
5
+ * `[Nest] <pid> - <full locale date> LEVEL [Context] <message>`
6
+ * — is ~55 chars before the message. In a split-pane dev TUI (process-compose,
7
+ * tmux) every line wraps 2–3×, drowning the actual content. And the generated
8
+ * entrypoints pass no `logger:` option, so consumers have no log-level knob:
9
+ * every subsystem `debug` line (e.g. EventScheduler's per-slot `materialised …`)
10
+ * always prints.
11
+ *
12
+ * This module is the proven swe-brain (second-dogfood) consumer pattern lifted
13
+ * into the runtime so consumers stop rebuilding it by hand:
14
+ *
15
+ * - `CompactConsoleLogger` — drops the `[Nest] <pid> - ` prefix (a supervisor
16
+ * pane header already names the process) and shortens the timestamp to
17
+ * `HH:mm:ss`.
18
+ * - `parseLogLevels` / the `LOG_LEVEL` env convention — a single severity
19
+ * threshold enables that level and everything above it.
20
+ * - `createAppLogger` — the factory the entrypoints hand to `NestFactory`.
21
+ */
22
+ import { ConsoleLogger, type LogLevel } from '@nestjs/common';
23
+
24
+ /** Severity-ordered (lowest → highest); a threshold enables its suffix. */
25
+ const LEVELS: LogLevel[] = ['verbose', 'debug', 'log', 'warn', 'error', 'fatal'];
26
+ const DEFAULT_THRESHOLD: LogLevel = 'log';
27
+
28
+ const TIME = new Intl.DateTimeFormat('en-GB', {
29
+ hour: '2-digit',
30
+ minute: '2-digit',
31
+ second: '2-digit',
32
+ hour12: false,
33
+ });
34
+
35
+ /**
36
+ * Resolve a `LOG_LEVEL` threshold into the enabled-levels array Nest's
37
+ * `logLevels` option expects.
38
+ *
39
+ * `LOG_LEVEL=debug` → `['debug','log','warn','error','fatal']`. Unknown input
40
+ * warns and falls back to the default (`'log'` and above).
41
+ */
42
+ export function parseLogLevels(threshold = process.env.LOG_LEVEL): LogLevel[] {
43
+ const idx = LEVELS.indexOf(
44
+ (threshold ?? DEFAULT_THRESHOLD).toLowerCase() as LogLevel,
45
+ );
46
+ if (idx === -1) {
47
+ console.warn(
48
+ `[logging] unknown LOG_LEVEL '${threshold}' — defaulting to '${DEFAULT_THRESHOLD}'`,
49
+ );
50
+ return LEVELS.slice(LEVELS.indexOf(DEFAULT_THRESHOLD));
51
+ }
52
+ return LEVELS.slice(idx);
53
+ }
54
+
55
+ /**
56
+ * A `ConsoleLogger` with a compact one-line format tuned for split-pane TUIs.
57
+ */
58
+ export class CompactConsoleLogger extends ConsoleLogger {
59
+ /** Drop `[Nest] <pid> - ` — a supervisor pane header already identifies the process. */
60
+ protected override formatPid(_pid: number): string {
61
+ return '';
62
+ }
63
+
64
+ /** `12:48:42`, not `06/07/2026, 12:48:42 PM`. */
65
+ protected override getTimestamp(): string {
66
+ return TIME.format(Date.now());
67
+ }
68
+
69
+ protected override formatMessage(
70
+ logLevel: LogLevel,
71
+ message: unknown,
72
+ _pidMessage: string,
73
+ _formattedLogLevel: string,
74
+ contextMessage: string,
75
+ timestampDiff: string,
76
+ ): string {
77
+ const output = this.stringifyMessage(message, logLevel);
78
+ // padStart(5) (vs Nest's 7) aligns LOG/WARN/DEBUG/ERROR; `verbose` may jitter.
79
+ const level = this.colorize(
80
+ logLevel.toUpperCase().padStart(5, ' '),
81
+ logLevel,
82
+ );
83
+ return `${this.getTimestamp()} ${level} ${contextMessage}${output}${timestampDiff}\n`;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Build the app-wide logger that entrypoints hand to `NestFactory`.
89
+ *
90
+ * @param threshold explicit `LOG_LEVEL` override. CLI tools that must stay quiet
91
+ * regardless of the ambient env pass e.g. `'warn'`. When omitted, the
92
+ * `LOG_LEVEL` env var (then the `'log'` default) wins.
93
+ *
94
+ * @example
95
+ * const app = await NestFactory.create(AppModule, { logger: createAppLogger() });
96
+ */
97
+ export function createAppLogger(threshold?: LogLevel): CompactConsoleLogger {
98
+ return new CompactConsoleLogger('', {
99
+ logLevels: parseLogLevels(threshold ?? process.env.LOG_LEVEL),
100
+ timestamp: true,
101
+ });
102
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Logging shared subsystem — public API.
3
+ *
4
+ * Imported by consumers as `@pattern-stack/codegen/runtime/shared/logging`.
5
+ */
6
+ export {
7
+ CompactConsoleLogger,
8
+ createAppLogger,
9
+ parseLogLevels,
10
+ } from './compact-console-logger';
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  to: "<%= schemaPath %>"
3
3
  force: true
4
+ skip_if: "<%= skipSchema %>"
4
5
  ---
5
6
  <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
@@ -7,5 +7,5 @@ skip_if: "<%= mainHookInjected %>"
7
7
  // JOBS — Embedded worker mode (optional)
8
8
  // To run the job worker in-process (single-process deploy), add to AppModule imports:
9
9
  // JobWorkerModule.forRoot({ mode: 'embedded' })
10
- // For standalone worker (separate process), use worker.ts at the project root.
10
+ // For standalone worker (separate process), run src/worker.ts (bun src/worker.ts).
11
11
  // See codegen.config.yaml jobs.worker_mode to toggle the documented default.
@@ -9,9 +9,10 @@
9
9
  * Invoked via:
10
10
  * bunx hygen subsystem jobs \
11
11
  * --workerPath <abs> --workerExists <'true'|''> \
12
+ * --jobWorkerModuleImport <specifier> --workerForRootOpts <ts-literal> \
12
13
  * --mainTsPath <abs> --configPath <abs> --schemaPath <abs> \
13
14
  * --multiTenant <'true'|'false'> --workerMode <embedded|standalone> \
14
- * --appName <string>
15
+ * --skipSchema <'true'|''> --appName <string>
15
16
  */
16
17
 
17
18
  import { renderGeneratedBanner } from "../../_shared/generated-banner.mjs";
@@ -23,6 +24,28 @@ function coerceBool(raw) {
23
24
  return false;
24
25
  }
25
26
 
27
+ // #513: the CLI base64-encodes --workerForRootOpts because Hygen's yargs parser
28
+ // mangles a raw `{ mode: 'standalone', … }` TS literal (the braces/colons are
29
+ // read as nested object syntax). Decode it back to the source string here. A
30
+ // direct hygen invocation that passes a non-encoded value (or omits it) falls
31
+ // back to the plain default below.
32
+ function decodeWorkerForRootOpts(raw) {
33
+ if (typeof raw !== "string" || raw.length === 0) {
34
+ return "{ mode: 'standalone', allPools: true }";
35
+ }
36
+ try {
37
+ const decoded = Buffer.from(raw, "base64").toString("utf-8");
38
+ // Re-encoding round-trips iff `raw` was valid base64 of the decoded bytes;
39
+ // guards against a hand-passed plain literal being treated as base64.
40
+ if (Buffer.from(decoded, "utf-8").toString("base64") === raw) {
41
+ return decoded;
42
+ }
43
+ } catch {
44
+ /* fall through to raw */
45
+ }
46
+ return raw;
47
+ }
48
+
26
49
  export default {
27
50
  prompt: async ({ args }) => {
28
51
  return {
@@ -34,9 +57,24 @@ export default {
34
57
  // Hygen's skip_if treats any non-empty string as truthy, so we send an
35
58
  // empty string when the file doesn't exist (CLI already does this).
36
59
  workerExists: args.workerExists ?? "",
37
- workerPath: args.workerPath ?? "worker.ts",
60
+ // #513: the worker lands at `src/worker.ts` (inside the default tsconfig
61
+ // include, next to `app.module.ts`); the CLI always passes an absolute
62
+ // --workerPath, this fallback only guards a direct hygen invocation.
63
+ workerPath: args.workerPath ?? "src/worker.ts",
64
+ // #513: mode-aware JobWorkerModule import + the pre-serialised
65
+ // forRoot(<opts>) literal (the only mode-dependent import the worker
66
+ // carries — AppModule is imported relatively).
67
+ jobWorkerModuleImport:
68
+ args.jobWorkerModuleImport ??
69
+ "@pattern-stack/codegen/runtime/subsystems/jobs/index",
70
+ workerForRootOpts: decodeWorkerForRootOpts(args.workerForRootOpts),
38
71
  schemaPath:
39
72
  args.schemaPath ?? "shared/subsystems/jobs/job-orchestration.schema.ts",
73
+ // #517: package mode skips the schema template (the schema ships in the
74
+ // package, re-exported via the schema barrel). Hygen's skip_if treats any
75
+ // non-empty string as truthy, so the CLI sends '' in vendored mode and
76
+ // 'true' in package mode. Default '' so a direct hygen invocation renders.
77
+ skipSchema: args.skipSchema ?? "",
40
78
  // @generated DO-NOT-EDIT banner — the jobs subsystem schema is
41
79
  // force-overwritten on every `subsystem install`.
42
80
  generatedBanner: renderGeneratedBanner({
@@ -5,31 +5,42 @@ unless_exists: true
5
5
  /**
6
6
  * Standalone job worker entrypoint — emitted by `codegen subsystem install jobs`.
7
7
  *
8
- * Boots a Nest application context (NO HTTP listener) wiring the full
9
- * subsystem barrel (`SUBSYSTEM_MODULES` events + jobs + bridge + integration, in
10
- * dependency order) plus `JobWorkerModule.forRoot({ mode: 'standalone',
11
- * allPools: true })`. Run with:
8
+ * Boots a Nest application context (NO HTTP listener) that composes the
9
+ * consumer's root `AppModule` plus `JobWorkerModule.forRoot({ mode:
10
+ * 'standalone', allPools: true, … })`. Run with:
12
11
  *
13
- * bun worker.ts
12
+ * bun src/worker.ts
14
13
  *
15
- * Why the barrel + `allPools`:
16
- * - The events subsystem's outbox drain and the bridge's fanout wrappers
17
- * run as `job_run` rows in the RESERVED `events_*` pools. A worker that
18
- * only polls the non-reserved pools (`interactive`, `batch`, …) leaves
19
- * those lanes stranded `BridgeDeliveryHandler` never fires and durable
20
- * event→job fanout silently stops.
14
+ * Why import `AppModule` whole:
15
+ * - Job handlers are Nest providers registered by the consumer's handler
16
+ * modules (and the subsystem barrel). Composing around `AppModule` gives
17
+ * this worker the SAME DI graph as the HTTP process — every `@JobHandler`
18
+ * resolves its dependencies here exactly as it would in the API. A bare
19
+ * `SUBSYSTEM_MODULES`-only worker boots with an empty handler surface.
20
+ * - `AppModule` already wires `DatabaseModule` + `SUBSYSTEM_MODULES` (events +
21
+ * jobs + bridge + integration, dependency-ordered), so the worker needs no
22
+ * mode-aware barrel import — only `JobWorkerModule` itself.
23
+ *
24
+ * Why `allPools: true`:
25
+ * - The events subsystem's outbox drain and the bridge's fanout wrappers run
26
+ * as `job_run` rows in the RESERVED `events_*` pools. A worker that only
27
+ * polls the non-reserved pools (`interactive`, `batch`, …) strands those
28
+ * lanes — `BridgeDeliveryHandler` never fires and durable event→job fanout
29
+ * silently stops.
21
30
  * - `allPools: true` activates every pool in the resolved config, reserved
22
31
  * lanes included, so this single standalone process drains both user work
23
32
  * and the framework's reserved lanes.
24
- * - Importing `SUBSYSTEM_MODULES` (rather than `JobsDomainModule` alone)
25
- * registers `EVENT_BUS` / `JOB_ORCHESTRATOR` / `BRIDGE_*` so the
26
- * framework `@framework/bridge_delivery` handler resolves its DI deps.
27
- * `BridgeModule`'s reserved-pool guard short-circuits to pass because
28
- * `allPools` is set.
29
33
  *
30
- * Embedded mode (single-process) is configured by importing
31
- * `JobWorkerModule.forRoot({ mode: 'embedded' })` inside AppModule instead
32
- * see the commented guidance injected into `src/main.ts`.
34
+ * STANDALONE ONLY: this entrypoint is for `jobs.worker_mode: standalone`. In
35
+ * embedded mode the worker already runs inside `AppModule` (via the
36
+ * `JobWorkerModule.forRoot({ mode: 'embedded' })` the barrel composes), so
37
+ * booting this file too would double-spawn the worker against the same pools.
38
+ *
39
+ * DO NOT boot `AppModule` twice in one process: a consumer `AppModule`
40
+ * registers an OpenAPI document against the per-process `OpenApiRegistry`
41
+ * singleton, which throws `DuplicateSchemaError` on the second registration.
42
+ * Multi-rung boot validation (e.g. "does the worker boot AND does the API
43
+ * boot?") must spawn CHILD PROCESSES, not import both modules into one.
33
44
  *
34
45
  * SIGTERM triggers graceful shutdown bounded by SHUTDOWN_TIMEOUT_MS; after the
35
46
  * timeout the process exits hard so orchestrators (systemd, Kubernetes) can
@@ -39,26 +50,23 @@ import 'reflect-metadata';
39
50
  import { Logger, Module } from '@nestjs/common';
40
51
  import { NestFactory } from '@nestjs/core';
41
52
 
42
- import { DatabaseModule } from '@shared/database/database.module';
43
- import { JobWorkerModule } from '@shared/subsystems/jobs/job-worker.module';
44
- import { SUBSYSTEM_MODULES } from '@generated/subsystems';
53
+ import { AppModule } from './app.module';
54
+ import { JobWorkerModule } from '<%= jobWorkerModuleImport %>';
45
55
 
46
56
  const SHUTDOWN_TIMEOUT_MS = 30_000;
47
57
 
48
58
  @Module({
49
59
  imports: [
50
- DatabaseModule,
51
- // Events + Jobs + Bridge + Integration (dependency-ordered) from the generated
52
- // barrel. This is the same composition AppModule imports — keeping the
53
- // worker's DI graph identical to the HTTP app's so handlers resolve the
54
- // same way in both processes.
55
- ...SUBSYSTEM_MODULES,
60
+ // Consumer root — DatabaseModule + SUBSYSTEM_MODULES + handler modules.
61
+ // Importing it whole keeps the worker's DI graph identical to the HTTP app's
62
+ // so every `@JobHandler` resolves the same way in both processes.
63
+ AppModule,
56
64
  // `allPools: true` drains the reserved `events_*` lanes (events outbox +
57
65
  // bridge wrappers) alongside the user pools.
58
- JobWorkerModule.forRoot({ mode: 'standalone', allPools: true }),
66
+ JobWorkerModule.forRoot(<%- workerForRootOpts %>),
59
67
  ],
60
68
  })
61
- class WorkerAppModule {}
69
+ export class WorkerAppModule {}
62
70
 
63
71
  async function bootstrap(): Promise<void> {
64
72
  const logger = new Logger('JobWorker');
@@ -98,8 +106,12 @@ async function bootstrap(): Promise<void> {
98
106
  logger.log('job worker started (standalone mode, all pools)');
99
107
  }
100
108
 
101
- bootstrap().catch((err) => {
102
- // eslint-disable-next-line no-console
103
- console.error('failed to bootstrap job worker', err);
104
- process.exit(1);
105
- });
109
+ // Gated so the module can be imported by boot-checks / e2e without spawning a
110
+ // worker; `bun src/worker.ts` runs it as the entrypoint.
111
+ if (import.meta.main) {
112
+ bootstrap().catch((err) => {
113
+ // eslint-disable-next-line no-console
114
+ console.error('failed to bootstrap job worker', err);
115
+ process.exit(1);
116
+ });
117
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../runtime/base-classes/lifecycle-events.ts"],"sourcesContent":["/**\n * Lifecycle event emission for BaseService.\n *\n * Ported from pattern-stack/atoms/patterns/services/base.py — the Python\n * BaseService emits LIFECYCLE and CHANGE events on every CRUD operation.\n * This module provides the same capability for the TypeScript codegen stack.\n *\n * Design:\n * - Fire-and-forget: event emission never fails the CRUD operation.\n * - IEventBus is optional: if no EVENT_BUS is injected, emission is silently\n * skipped. This means base classes work in projects that haven't installed\n * the events subsystem.\n * - LIFECYCLE events carry an entity snapshot in payload.\n * - CHANGE events carry per-field old/new diffs.\n * - Controlled per-entity via `emitLifecycleEvents` flag (default: true).\n *\n * @deprecated EVT-7 — Lifecycle events are untyped and emit outside of the\n * CRUD transaction. New work should declare an `emits:` block on the entity\n * and publish typed domain events from use-cases via TYPED_EVENT_BUS inside\n * the same Drizzle transaction. See `docs/specs/EVT-7.md`. This helper is\n * retained for BaseService backward compatibility until all entities have\n * migrated to typed emits.\n */\n\nimport { randomUUID } from 'crypto';\nimport type { IEventBus, DomainEvent } from '../subsystems/events/event-bus.protocol';\n\n// ============================================================================\n// Event categories (subset of pattern-stack's EventCategory)\n// ============================================================================\n\nexport type EventCategory = 'lifecycle' | 'change';\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** System fields excluded from entity snapshots and change diffs. */\nconst SYSTEM_FIELDS = new Set([\n\t'id',\n\t'createdAt',\n\t'updatedAt',\n\t'deletedAt',\n]);\n\n/**\n * Snapshot an entity's field values, excluding system fields.\n * Mirrors pattern-stack's `_get_entity_snapshot()`.\n */\nexport function entitySnapshot(entity: Record<string, unknown>): Record<string, unknown> {\n\tconst snap: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(entity)) {\n\t\tif (!SYSTEM_FIELDS.has(key)) {\n\t\t\tsnap[key] = value;\n\t\t}\n\t}\n\treturn snap;\n}\n\n/**\n * Diff two entity snapshots, returning per-field old/new pairs.\n * Only includes fields that actually changed.\n */\nexport function diffSnapshots(\n\tbefore: Record<string, unknown>,\n\tafter: Record<string, unknown>,\n): Array<{ field: string; oldValue: unknown; newValue: unknown }> {\n\tconst changes: Array<{ field: string; oldValue: unknown; newValue: unknown }> = [];\n\tconst allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);\n\n\tfor (const key of allKeys) {\n\t\tif (SYSTEM_FIELDS.has(key)) continue;\n\t\tconst oldVal = before[key];\n\t\tconst newVal = after[key];\n\t\t// Simple equality — good enough for primitives and nulls.\n\t\t// For deep objects, JSON.stringify comparison.\n\t\tif (oldVal !== newVal && JSON.stringify(oldVal) !== JSON.stringify(newVal)) {\n\t\t\tchanges.push({ field: key, oldValue: oldVal, newValue: newVal });\n\t\t}\n\t}\n\n\treturn changes;\n}\n\n// ============================================================================\n// Event builders\n// ============================================================================\n\nexport function buildLifecycleEvent(\n\tentityName: string,\n\taction: 'created' | 'updated' | 'deleted',\n\tentityId: string,\n\tsnapshot?: Record<string, unknown>,\n): DomainEvent {\n\treturn {\n\t\tid: randomUUID(),\n\t\ttype: `${entityName}.${action}`,\n\t\taggregateId: entityId,\n\t\taggregateType: entityName,\n\t\tpayload: snapshot ? { snapshot } : {},\n\t\toccurredAt: new Date(),\n\t\tmetadata: { category: 'lifecycle' as EventCategory },\n\t};\n}\n\nexport function buildChangeEvents(\n\tentityName: string,\n\tentityId: string,\n\tchanges: Array<{ field: string; oldValue: unknown; newValue: unknown }>,\n): DomainEvent[] {\n\treturn changes.map((c) => ({\n\t\tid: randomUUID(),\n\t\ttype: `${entityName}.field_changed`,\n\t\taggregateId: entityId,\n\t\taggregateType: entityName,\n\t\tpayload: {\n\t\t\tfieldName: c.field,\n\t\t\toldValue: c.oldValue,\n\t\t\tnewValue: c.newValue,\n\t\t},\n\t\toccurredAt: new Date(),\n\t\tmetadata: { category: 'change' as EventCategory },\n\t}));\n}\n\n// ============================================================================\n// Emission helper (fire-and-forget)\n// ============================================================================\n\n/**\n * Emit events to the bus, swallowing errors.\n * Mirrors pattern-stack's `_emit_lifecycle_event()` try/except.\n */\nexport async function emitSafely(\n\teventBus: IEventBus | undefined,\n\tevents: DomainEvent[],\n): Promise<void> {\n\tif (!eventBus || events.length === 0) return;\n\ttry {\n\t\tif (events.length === 1) {\n\t\t\tconst only = events[0];\n\t\t\tif (!only) return;\n\t\t\tawait eventBus.publish(only);\n\t\t} else {\n\t\t\tawait eventBus.publishMany(events);\n\t\t}\n\t} catch {\n\t\t// Log but never fail the CRUD operation.\n\t\t// In production, this would use a structured logger.\n\t\tconsole.warn(`[lifecycle-events] failed to emit ${events.length} event(s)`);\n\t}\n}\n"],"mappings":";AAwBA,SAAS,kBAAkB;AAc3B,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAMM,SAAS,eAAe,QAA0D;AACxF,QAAM,OAAgC,CAAC;AACvC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AAClD,QAAI,CAAC,cAAc,IAAI,GAAG,GAAG;AAC5B,WAAK,GAAG,IAAI;AAAA,IACb;AAAA,EACD;AACA,SAAO;AACR;AAMO,SAAS,cACf,QACA,OACiE;AACjE,QAAM,UAA0E,CAAC;AACjF,QAAM,UAAU,oBAAI,IAAI,CAAC,GAAG,OAAO,KAAK,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,CAAC,CAAC;AAEvE,aAAW,OAAO,SAAS;AAC1B,QAAI,cAAc,IAAI,GAAG,EAAG;AAC5B,UAAM,SAAS,OAAO,GAAG;AACzB,UAAM,SAAS,MAAM,GAAG;AAGxB,QAAI,WAAW,UAAU,KAAK,UAAU,MAAM,MAAM,KAAK,UAAU,MAAM,GAAG;AAC3E,cAAQ,KAAK,EAAE,OAAO,KAAK,UAAU,QAAQ,UAAU,OAAO,CAAC;AAAA,IAChE;AAAA,EACD;AAEA,SAAO;AACR;AAMO,SAAS,oBACf,YACA,QACA,UACA,UACc;AACd,SAAO;AAAA,IACN,IAAI,WAAW;AAAA,IACf,MAAM,GAAG,UAAU,IAAI,MAAM;AAAA,IAC7B,aAAa;AAAA,IACb,eAAe;AAAA,IACf,SAAS,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IACpC,YAAY,oBAAI,KAAK;AAAA,IACrB,UAAU,EAAE,UAAU,YAA6B;AAAA,EACpD;AACD;AAEO,SAAS,kBACf,YACA,UACA,SACgB;AAChB,SAAO,QAAQ,IAAI,CAAC,OAAO;AAAA,IAC1B,IAAI,WAAW;AAAA,IACf,MAAM,GAAG,UAAU;AAAA,IACnB,aAAa;AAAA,IACb,eAAe;AAAA,IACf,SAAS;AAAA,MACR,WAAW,EAAE;AAAA,MACb,UAAU,EAAE;AAAA,MACZ,UAAU,EAAE;AAAA,IACb;AAAA,IACA,YAAY,oBAAI,KAAK;AAAA,IACrB,UAAU,EAAE,UAAU,SAA0B;AAAA,EACjD,EAAE;AACH;AAUA,eAAsB,WACrB,UACA,QACgB;AAChB,MAAI,CAAC,YAAY,OAAO,WAAW,EAAG;AACtC,MAAI;AACH,QAAI,OAAO,WAAW,GAAG;AACxB,YAAM,OAAO,OAAO,CAAC;AACrB,UAAI,CAAC,KAAM;AACX,YAAM,SAAS,QAAQ,IAAI;AAAA,IAC5B,OAAO;AACN,YAAM,SAAS,YAAY,MAAM;AAAA,IAClC;AAAA,EACD,QAAQ;AAGP,YAAQ,KAAK,qCAAqC,OAAO,MAAM,WAAW;AAAA,EAC3E;AACD;","names":[]}