@nanobpm/urban 0.51.0 → 0.53.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.
- package/README.md +47 -20
- package/dist/runtime/core/events.d.ts +135 -0
- package/dist/runtime/core/events.js +329 -0
- package/dist/runtime/core/execContext.d.ts +5 -0
- package/dist/runtime/core/extensions.d.ts +131 -0
- package/dist/runtime/core/extensions.js +137 -0
- package/dist/runtime/core/glob.d.ts +7 -6
- package/dist/runtime/core/glob.js +16 -14
- package/dist/runtime/core/host.d.ts +4 -1
- package/dist/runtime/core/lineage.d.ts +116 -0
- package/dist/runtime/core/lineage.js +186 -0
- package/dist/runtime/core/modules/agent.d.ts +17 -0
- package/dist/runtime/core/modules/agent.js +63 -0
- package/dist/runtime/core/modules/datasource.d.ts +6 -0
- package/dist/runtime/core/modules/datasource.js +8 -0
- package/dist/runtime/core/modules/deploy.d.ts +8 -5
- package/dist/runtime/core/modules/deploy.js +62 -16
- package/dist/runtime/core/modules/lineage-store.d.ts +81 -0
- package/dist/runtime/core/modules/lineage-store.js +175 -0
- package/dist/runtime/core/modules/pages.js +397 -20
- package/dist/runtime/core/modules/surfaces.js +8 -0
- package/dist/runtime/core/modules/workers.js +69 -2
- package/dist/runtime/core/phase.d.ts +168 -0
- package/dist/runtime/core/phase.js +367 -0
- package/dist/runtime/core/runtime.d.ts +17 -0
- package/dist/runtime/core/runtime.js +30 -0
- package/dist/runtime/engine/nanosdk.js +8 -2
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.js +12 -0
- package/dist/toolkit/derivers/system-brief.d.ts +62 -0
- package/dist/toolkit/derivers/system-brief.js +156 -0
- package/dist/toolkit/gen.d.ts +5 -5
- package/dist/toolkit/gen.js +20 -14
- package/dist/toolkit/index.d.ts +5 -0
- package/dist/toolkit/index.js +3 -0
- package/dist/toolkit/models.d.ts +4 -3
- package/dist/toolkit/models.js +4 -3
- package/package.json +1 -1
- package/src/runtime/core/events.test.ts +382 -0
- package/src/runtime/core/events.ts +370 -0
- package/src/runtime/core/execContext.ts +5 -0
- package/src/runtime/core/extensions.test.ts +378 -0
- package/src/runtime/core/extensions.ts +276 -0
- package/src/runtime/core/glob.ts +15 -12
- package/src/runtime/core/host.ts +4 -1
- package/src/runtime/core/lineage.test.ts +215 -0
- package/src/runtime/core/lineage.ts +297 -0
- package/src/runtime/core/modules/agent.test.ts +104 -0
- package/src/runtime/core/modules/agent.ts +76 -0
- package/src/runtime/core/modules/datasource.ts +9 -0
- package/src/runtime/core/modules/deploy.test.ts +84 -16
- package/src/runtime/core/modules/deploy.ts +59 -16
- package/src/runtime/core/modules/lineage-store.test.ts +177 -0
- package/src/runtime/core/modules/lineage-store.ts +239 -0
- package/src/runtime/core/modules/pages.test.ts +309 -5
- package/src/runtime/core/modules/pages.ts +398 -20
- package/src/runtime/core/modules/surfaces.ts +9 -0
- package/src/runtime/core/modules/workers.test.ts +136 -1
- package/src/runtime/core/modules/workers.ts +72 -6
- package/src/runtime/core/phase.test.ts +350 -0
- package/src/runtime/core/phase.ts +526 -0
- package/src/runtime/core/runtime.extensions.test.ts +151 -0
- package/src/runtime/core/runtime.ts +49 -0
- package/src/runtime/devserver.test.ts +3 -0
- package/src/runtime/engine/nanosdk.test.ts +97 -3
- package/src/runtime/engine/nanosdk.ts +8 -2
- package/src/runtime/index.ts +92 -0
- package/src/toolkit/derivers/system-brief.test.ts +150 -0
- package/src/toolkit/derivers/system-brief.ts +210 -0
- package/src/toolkit/gen.test.ts +28 -4
- package/src/toolkit/gen.ts +20 -13
- package/src/toolkit/index.ts +14 -0
- package/src/toolkit/models.ts +4 -3
|
@@ -20,6 +20,8 @@ import { mountTriggers } from "./modules/triggers.ts";
|
|
|
20
20
|
import { mountInstanceTracking } from "./modules/instance-tracking.ts";
|
|
21
21
|
import type { SchedulerDeps } from "./modules/scheduler.ts";
|
|
22
22
|
import { mountSecurity, type SecurityPolicy } from "./modules/security.ts";
|
|
23
|
+
import { EventBus } from "./events.ts";
|
|
24
|
+
import { createUrbanEvents, mountExtensions, type UrbanEvents, type UrbanExtension } from "./extensions.ts";
|
|
23
25
|
|
|
24
26
|
/** Resolve the HTTP port: explicit option, else $PORT, else 8090. Throws a clear
|
|
25
27
|
* error when $PORT is set but not a valid integer in 0..65535. */
|
|
@@ -82,6 +84,14 @@ export interface CreateUrbanAppOptions {
|
|
|
82
84
|
* so one injected scheduler drives them both (no per-module wiring, no drift).
|
|
83
85
|
*/
|
|
84
86
|
scheduler?: SchedulerDeps;
|
|
87
|
+
/**
|
|
88
|
+
* Pluggable Urban {@link UrbanExtension}s — the agentic families and connector
|
|
89
|
+
* packs that extend the app lifecycle through the typed extension-event taxonomy
|
|
90
|
+
* (issue #262) instead of editing the runtime. The runtime runs each through the
|
|
91
|
+
* `extension/register` serial checkpoint in a deterministic order and disposes
|
|
92
|
+
* everything they register on `stop()` / HMR. Empty by default.
|
|
93
|
+
*/
|
|
94
|
+
extensions?: readonly UrbanExtension[];
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
export interface UrbanApp {
|
|
@@ -112,6 +122,14 @@ export interface UrbanApp {
|
|
|
112
122
|
* `undefined` before `start()`, after `stop()`, and on hosts that don't surface one (Deno).
|
|
113
123
|
*/
|
|
114
124
|
readonly httpServer: object | undefined;
|
|
125
|
+
/**
|
|
126
|
+
* The app's typed extension-event taxonomy (issue #262): lifecycle notifications,
|
|
127
|
+
* the extension-registration checkpoint, request/security waterfall gates, and
|
|
128
|
+
* the reconcile fan-out — each a channel with a declared dispatch mode. Modules
|
|
129
|
+
* and {@link UrbanExtension}s hook onto these seams instead of wiring directly;
|
|
130
|
+
* every registration is an effect on a dispose ladder that `stop()` unwinds.
|
|
131
|
+
*/
|
|
132
|
+
readonly events: UrbanEvents;
|
|
115
133
|
}
|
|
116
134
|
|
|
117
135
|
export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<UrbanApp> {
|
|
@@ -137,6 +155,20 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
|
|
|
137
155
|
// correlation context).
|
|
138
156
|
const appLog = createLogger((l, m, f) => host.log(l, m, f));
|
|
139
157
|
|
|
158
|
+
// The app-wide extension-event microkernel (issue #262): one typed taxonomy over
|
|
159
|
+
// one dispose ladder. Contained listener throws land in the structured log rather
|
|
160
|
+
// than stranding the pipeline. Modules and extensions hook onto `events`; the
|
|
161
|
+
// whole ladder unwinds on teardown.
|
|
162
|
+
const bus = new EventBus({
|
|
163
|
+
onError: (err, info) =>
|
|
164
|
+
appLog.warn("extension listener threw and was contained", {
|
|
165
|
+
event: info.event,
|
|
166
|
+
mode: info.mode,
|
|
167
|
+
error: String(err),
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
const events = createUrbanEvents(bus);
|
|
171
|
+
|
|
140
172
|
// Install the ambient job-execution store once per process (idempotent) so worker dispatch can
|
|
141
173
|
// stamp write-provenance onto DataLayer inserts. Absent-safe: a host without `createAsyncStore`
|
|
142
174
|
// leaves provenance capture disabled.
|
|
@@ -162,6 +194,8 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
|
|
|
162
194
|
// Release every mounted resource and reset internal state so a subsequent
|
|
163
195
|
// start() begins clean. Used by stop() and by start()'s failure path.
|
|
164
196
|
const teardown = async () => {
|
|
197
|
+
// Notify lifecycle listeners before we start tearing seams down (issue #262).
|
|
198
|
+
events.lifecycle.emit({ app: manifest.id, phase: "stopping" });
|
|
165
199
|
if (server) {
|
|
166
200
|
try {
|
|
167
201
|
await server.stop();
|
|
@@ -189,12 +223,18 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
|
|
|
189
223
|
data = undefined;
|
|
190
224
|
security = undefined;
|
|
191
225
|
started = false;
|
|
226
|
+
// Terminal lifecycle notification, then unwind the whole dispose ladder LIFO:
|
|
227
|
+
// every extension listener/effect (and the lifecycle channel itself) is torn
|
|
228
|
+
// down, so a subsequent start() — or a dev-server HMR reload — leaks nothing.
|
|
229
|
+
events.lifecycle.emit({ app: manifest.id, phase: "stopped" });
|
|
230
|
+
bus.dispose();
|
|
192
231
|
};
|
|
193
232
|
|
|
194
233
|
const app: UrbanApp = {
|
|
195
234
|
manifest,
|
|
196
235
|
root,
|
|
197
236
|
log: appLog,
|
|
237
|
+
events,
|
|
198
238
|
get data() {
|
|
199
239
|
return data;
|
|
200
240
|
},
|
|
@@ -212,6 +252,7 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
|
|
|
212
252
|
async start() {
|
|
213
253
|
if (started) throw new Error("app already started");
|
|
214
254
|
started = true;
|
|
255
|
+
events.lifecycle.emit({ app: manifest.id, phase: "starting" });
|
|
215
256
|
try {
|
|
216
257
|
if (flags.security) security = mountSecurity(ctx);
|
|
217
258
|
if (flags.deploy) describe.deploy = await deployModels(ctx);
|
|
@@ -237,6 +278,13 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
|
|
|
237
278
|
if (c.jobTypes.length > 0) describe.connectors = c.describe?.();
|
|
238
279
|
}
|
|
239
280
|
|
|
281
|
+
// Run the agentic-family / connector-pack extension surface through the
|
|
282
|
+
// typed taxonomy (issue #262). Everything an extension registers rides the
|
|
283
|
+
// shared dispose ladder, so teardown's single `bus.dispose()` unwinds it —
|
|
284
|
+
// the extension host is deliberately NOT pushed onto `mounted`.
|
|
285
|
+
const extHost = await mountExtensions(api, opts.extensions ?? [], { bus, events });
|
|
286
|
+
describe.extensions = extHost.describe?.();
|
|
287
|
+
|
|
240
288
|
const routes: Route[] = [];
|
|
241
289
|
let hasUiSurfaces = false;
|
|
242
290
|
if (flags.surfaces) {
|
|
@@ -308,6 +356,7 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
|
|
|
308
356
|
}
|
|
309
357
|
}
|
|
310
358
|
host.log("info", `urban app "${manifest.id}" started`, {});
|
|
359
|
+
events.lifecycle.emit({ app: manifest.id, phase: "started" });
|
|
311
360
|
} catch (err) {
|
|
312
361
|
// A failed start must not leave the app half-mounted (leaked workers,
|
|
313
362
|
// a bound port) or wedged in the "already started" state.
|
|
@@ -4,6 +4,8 @@ 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
6
|
import { createLogger } from "./core/logger.ts";
|
|
7
|
+
import { createUrbanEvents } from "./core/extensions.ts";
|
|
8
|
+
import { EventBus } from "./core/events.ts";
|
|
7
9
|
|
|
8
10
|
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
9
11
|
|
|
@@ -232,6 +234,7 @@ function fakeApp(stops: string[], id: string): UrbanApp {
|
|
|
232
234
|
security: undefined,
|
|
233
235
|
httpPort: undefined,
|
|
234
236
|
httpServer: undefined,
|
|
237
|
+
events: createUrbanEvents(new EventBus()),
|
|
235
238
|
};
|
|
236
239
|
}
|
|
237
240
|
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
type NanoSdkJobWorkerConfig,
|
|
12
12
|
} from "./nanosdk.ts";
|
|
13
13
|
import { BpmnError } from "../core/host.ts";
|
|
14
|
+
import { __resetExecStoreForTests, installExecStore, runInJobContext } from "../core/execContext.ts";
|
|
15
|
+
import { readLineage } from "../core/lineage.ts";
|
|
16
|
+
import { createNodeHost } from "../adapters/node.ts";
|
|
14
17
|
|
|
15
18
|
/** A fake nano-sdk client that records calls and lets a test drive its job worker. */
|
|
16
19
|
function fakeSdkClient(overrides: Partial<NanoSdkClient> = {}): NanoSdkClient & {
|
|
@@ -133,7 +136,8 @@ test("createInstance routes through the SDK and coerces the key", async () => {
|
|
|
133
136
|
const engine = new SdkEngineClient(client);
|
|
134
137
|
const res = await engine.createInstance({ processDefinitionId: "p", variables: { a: 1 } });
|
|
135
138
|
assert.equal(res.processInstanceKey, "99");
|
|
136
|
-
|
|
139
|
+
// Caller variables are preserved (the SDK also auto-threads a `_urban.lineage` envelope, tested below).
|
|
140
|
+
assert.equal(res.variables?.a, 1);
|
|
137
141
|
assert.ok(client.calls.includes("createProcessInstance"));
|
|
138
142
|
});
|
|
139
143
|
|
|
@@ -162,7 +166,7 @@ test("cancelInstance routes through the SDK", async () => {
|
|
|
162
166
|
assert.ok(client.calls.includes("cancelProcessInstance") || seen !== undefined);
|
|
163
167
|
});
|
|
164
168
|
|
|
165
|
-
test("publishMessage defaults correlationKey
|
|
169
|
+
test("publishMessage defaults correlationKey and auto-threads lineage onto variables", async () => {
|
|
166
170
|
let seen: Record<string, unknown> | undefined;
|
|
167
171
|
const client = fakeSdkClient({
|
|
168
172
|
publishMessage: async (input) => {
|
|
@@ -172,7 +176,12 @@ test("publishMessage defaults correlationKey/variables", async () => {
|
|
|
172
176
|
});
|
|
173
177
|
const engine = new SdkEngineClient(client);
|
|
174
178
|
await engine.publishMessage({ name: "m" });
|
|
175
|
-
assert.
|
|
179
|
+
assert.equal(seen?.name, "m");
|
|
180
|
+
assert.equal(seen?.correlationKey, "");
|
|
181
|
+
// With no ambient job context this is a top-level publish: a fresh root envelope is minted.
|
|
182
|
+
const env = readLineage(seen?.variables);
|
|
183
|
+
assert.ok(env, "a lineage envelope is threaded onto the message variables");
|
|
184
|
+
assert.equal(env?.causedByInstanceKey, undefined);
|
|
176
185
|
});
|
|
177
186
|
|
|
178
187
|
test("searchUserTasks passes zero-wait consistency and maps items", async () => {
|
|
@@ -670,3 +679,88 @@ test("createNanoSdkEngineClient uses an injected client factory with the resolve
|
|
|
670
679
|
assert.deepEqual(seen, { restAddress: "http://x/v2", token: "t", transport: "falcon" });
|
|
671
680
|
assert.ok(engine);
|
|
672
681
|
});
|
|
682
|
+
|
|
683
|
+
// --- Lineage auto-threading (issue #254) ---------------------------------------
|
|
684
|
+
|
|
685
|
+
/** A fake client that captures the variables handed to createProcessInstance/publishMessage. */
|
|
686
|
+
function capturingClient(): NanoSdkClient & { createVars?: Record<string, unknown>; msgVars?: Record<string, unknown> } {
|
|
687
|
+
const base = fakeSdkClient();
|
|
688
|
+
const cap: NanoSdkClient & { createVars?: Record<string, unknown>; msgVars?: Record<string, unknown> } = {
|
|
689
|
+
...base,
|
|
690
|
+
async createProcessInstance(input) {
|
|
691
|
+
cap.createVars = input.variables;
|
|
692
|
+
return { processInstanceKey: 99, variables: input.variables };
|
|
693
|
+
},
|
|
694
|
+
async publishMessage(input) {
|
|
695
|
+
cap.msgVars = input.variables;
|
|
696
|
+
return { key: 1 };
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
return cap;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
test("createInstance mints a fresh root envelope for a top-level request (no ambient)", async () => {
|
|
703
|
+
__resetExecStoreForTests();
|
|
704
|
+
const client = capturingClient();
|
|
705
|
+
const engine = new SdkEngineClient(client);
|
|
706
|
+
await engine.createInstance({ processDefinitionId: "p", variables: { a: 1 } });
|
|
707
|
+
const env = readLineage(client.createVars);
|
|
708
|
+
assert.ok(env, "an envelope was threaded onto the variables");
|
|
709
|
+
assert.notEqual(env?.rootRequestKey, "");
|
|
710
|
+
assert.equal(env?.causedByInstanceKey, undefined, "a fresh root has no cause");
|
|
711
|
+
assert.equal(client.createVars?.a, 1, "caller variables are preserved");
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("createInstance propagates the ambient lineage and stamps causedByInstanceKey", async () => {
|
|
715
|
+
__resetExecStoreForTests();
|
|
716
|
+
const host = createNodeHost({ cwd: process.cwd(), log: () => {} });
|
|
717
|
+
installExecStore(() => host.createAsyncStore?.());
|
|
718
|
+
const client = capturingClient();
|
|
719
|
+
const engine = new SdkEngineClient(client);
|
|
720
|
+
try {
|
|
721
|
+
await runInJobContext({ instanceKey: "pi-parent", rootRequestKey: "root-1", jobType: "w" }, () =>
|
|
722
|
+
engine.createInstance({ processDefinitionId: "child" }),
|
|
723
|
+
);
|
|
724
|
+
} finally {
|
|
725
|
+
__resetExecStoreForTests();
|
|
726
|
+
}
|
|
727
|
+
assert.deepEqual(readLineage(client.createVars), {
|
|
728
|
+
rootRequestKey: "root-1",
|
|
729
|
+
causedByInstanceKey: "pi-parent",
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test("createInstance leaves an explicit caller-supplied envelope untouched (override wins)", async () => {
|
|
734
|
+
__resetExecStoreForTests();
|
|
735
|
+
const host = createNodeHost({ cwd: process.cwd(), log: () => {} });
|
|
736
|
+
installExecStore(() => host.createAsyncStore?.());
|
|
737
|
+
const client = capturingClient();
|
|
738
|
+
const engine = new SdkEngineClient(client);
|
|
739
|
+
try {
|
|
740
|
+
await runInJobContext({ instanceKey: "pi-parent", rootRequestKey: "root-1", jobType: "w" }, () =>
|
|
741
|
+
engine.createInstance({
|
|
742
|
+
processDefinitionId: "child",
|
|
743
|
+
variables: { _urban: { lineage: { rootRequestKey: "explicit" } } },
|
|
744
|
+
}),
|
|
745
|
+
);
|
|
746
|
+
} finally {
|
|
747
|
+
__resetExecStoreForTests();
|
|
748
|
+
}
|
|
749
|
+
assert.deepEqual(readLineage(client.createVars), { rootRequestKey: "explicit", causedByInstanceKey: undefined });
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test("publishMessage threads lineage so message-started instances inherit it", async () => {
|
|
753
|
+
__resetExecStoreForTests();
|
|
754
|
+
const host = createNodeHost({ cwd: process.cwd(), log: () => {} });
|
|
755
|
+
installExecStore(() => host.createAsyncStore?.());
|
|
756
|
+
const client = capturingClient();
|
|
757
|
+
const engine = new SdkEngineClient(client);
|
|
758
|
+
try {
|
|
759
|
+
await runInJobContext({ instanceKey: "pi-9", rootRequestKey: "root-9", jobType: "w" }, () =>
|
|
760
|
+
engine.publishMessage({ name: "start" }),
|
|
761
|
+
);
|
|
762
|
+
} finally {
|
|
763
|
+
__resetExecStoreForTests();
|
|
764
|
+
}
|
|
765
|
+
assert.deepEqual(readLineage(client.msgVars), { rootRequestKey: "root-9", causedByInstanceKey: "pi-9" });
|
|
766
|
+
});
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../core/form-contract.ts";
|
|
32
32
|
import type { EngineSdkClient } from "./sdk.ts";
|
|
33
33
|
import { isRecord } from "../core/guards.ts";
|
|
34
|
+
import { applyAmbientLineage } from "../core/lineage.ts";
|
|
34
35
|
|
|
35
36
|
/** Coerce an engine response's process-instance key to a non-empty string, or
|
|
36
37
|
* throw — a missing key means a malformed/partial response, not a real instance. */
|
|
@@ -193,9 +194,12 @@ export class SdkEngineClient implements EngineClient {
|
|
|
193
194
|
variables?: Record<string, unknown>;
|
|
194
195
|
awaitCompletion?: boolean;
|
|
195
196
|
}): Promise<{ processInstanceKey: string; variables?: Record<string, unknown> }> {
|
|
197
|
+
// Auto-thread the `_urban.lineage` envelope (issue #254): a handler that spawns an
|
|
198
|
+
// instance propagates its `rootRequestKey` and stamps `causedByInstanceKey`; a genuine
|
|
199
|
+
// top-level request mints a fresh root. An explicit caller-supplied envelope wins.
|
|
196
200
|
const body = await this.client.createProcessInstance({
|
|
197
201
|
processDefinitionId: input.processDefinitionId,
|
|
198
|
-
variables: input.variables
|
|
202
|
+
variables: applyAmbientLineage(input.variables),
|
|
199
203
|
awaitCompletion: input.awaitCompletion ?? false,
|
|
200
204
|
});
|
|
201
205
|
const rawKey = body.processInstanceKey ?? body.key;
|
|
@@ -217,10 +221,12 @@ export class SdkEngineClient implements EngineClient {
|
|
|
217
221
|
correlationKey?: string;
|
|
218
222
|
variables?: Record<string, unknown>;
|
|
219
223
|
}): Promise<void> {
|
|
224
|
+
// Message-started instances inherit lineage too (issue #254): thread the envelope onto the
|
|
225
|
+
// message variables so the instance the message starts carries `_urban.lineage`.
|
|
220
226
|
await this.client.publishMessage({
|
|
221
227
|
name: input.name,
|
|
222
228
|
correlationKey: input.correlationKey ?? "",
|
|
223
|
-
variables: input.variables
|
|
229
|
+
variables: applyAmbientLineage(input.variables),
|
|
224
230
|
});
|
|
225
231
|
}
|
|
226
232
|
|
package/src/runtime/index.ts
CHANGED
|
@@ -8,6 +8,37 @@ export type {
|
|
|
8
8
|
UrbanApp,
|
|
9
9
|
} from "./core/runtime.ts";
|
|
10
10
|
|
|
11
|
+
// Extension-event taxonomy + microkernel (issue #262)
|
|
12
|
+
export { EventBus } from "./core/events.ts";
|
|
13
|
+
export type {
|
|
14
|
+
DispatchMode,
|
|
15
|
+
Disposer,
|
|
16
|
+
EmitChannel,
|
|
17
|
+
ErrorSink,
|
|
18
|
+
EventBusOptions,
|
|
19
|
+
Listener,
|
|
20
|
+
Middleware,
|
|
21
|
+
ParallelChannel,
|
|
22
|
+
SerialChannel,
|
|
23
|
+
WaterfallChannel,
|
|
24
|
+
} from "./core/events.ts";
|
|
25
|
+
export { URBAN_EVENT_MODES, createUrbanEvents, mountExtensions, urbanEventMode } from "./core/extensions.ts";
|
|
26
|
+
export type {
|
|
27
|
+
DispatchRequest,
|
|
28
|
+
DispatchResponse,
|
|
29
|
+
ExtensionHost,
|
|
30
|
+
ExtensionRegistered,
|
|
31
|
+
ExtensionSetupContext,
|
|
32
|
+
GateDecision,
|
|
33
|
+
GateRequest,
|
|
34
|
+
LifecycleEvent,
|
|
35
|
+
MountExtensionsOptions,
|
|
36
|
+
ReconcileEvent,
|
|
37
|
+
UrbanEventName,
|
|
38
|
+
UrbanEvents,
|
|
39
|
+
UrbanExtension,
|
|
40
|
+
} from "./core/extensions.ts";
|
|
41
|
+
|
|
11
42
|
// Manifest
|
|
12
43
|
export {
|
|
13
44
|
bindModeToHost,
|
|
@@ -143,6 +174,67 @@ export type {
|
|
|
143
174
|
} from "./core/modules/llm.ts";
|
|
144
175
|
export { evalCorrelation } from "./core/modules/triggers.ts";
|
|
145
176
|
|
|
177
|
+
// Lineage — the framework-level lineage primitive (issue #254): the `_urban.lineage`
|
|
178
|
+
// envelope + auto-threading, and the generic read projection that stitches user intent →
|
|
179
|
+
// progress (a root request → the instances/PRs/tasks it causes).
|
|
180
|
+
export {
|
|
181
|
+
applyAmbientLineage,
|
|
182
|
+
buildLineageTree,
|
|
183
|
+
deriveLineage,
|
|
184
|
+
LINEAGE_KEY,
|
|
185
|
+
LINEAGE_NAMESPACE,
|
|
186
|
+
mintRootRequestKey,
|
|
187
|
+
readLineage,
|
|
188
|
+
writeLineage,
|
|
189
|
+
} from "./core/lineage.ts";
|
|
190
|
+
export type {
|
|
191
|
+
ApplyLineageOptions,
|
|
192
|
+
LineageAttachment,
|
|
193
|
+
LineageEdge,
|
|
194
|
+
LineageEdgeType,
|
|
195
|
+
LineageEnvelope,
|
|
196
|
+
LineageNode,
|
|
197
|
+
LineageTree,
|
|
198
|
+
} from "./core/lineage.ts";
|
|
199
|
+
export {
|
|
200
|
+
LINEAGE_ATTACHMENTS_TABLE,
|
|
201
|
+
LINEAGE_EDGES_TABLE,
|
|
202
|
+
LINEAGE_SCHEMA_SQL,
|
|
203
|
+
LineageStore,
|
|
204
|
+
systemClock as lineageSystemClock,
|
|
205
|
+
} from "./core/modules/lineage-store.ts";
|
|
206
|
+
export type {
|
|
207
|
+
Clock as LineageClock,
|
|
208
|
+
LineageJobLike,
|
|
209
|
+
LineageStoreOptions,
|
|
210
|
+
} from "./core/modules/lineage-store.ts";
|
|
211
|
+
|
|
212
|
+
// Phase — the framework-level *domain phase* primitive (issue #266): derive "which phase is this
|
|
213
|
+
// instance in?" from the BPMN scope hierarchy + write-provenance (Tier 0, zero declaration), an
|
|
214
|
+
// optional `nano:phase` override (Tier 1), and a lineage rollup for the epic frontier (Tier 2).
|
|
215
|
+
export {
|
|
216
|
+
buildScopeIndex,
|
|
217
|
+
derivePhase,
|
|
218
|
+
deriveInstancePhases,
|
|
219
|
+
furthestReached,
|
|
220
|
+
rollupLineagePhase,
|
|
221
|
+
SCOPE_CONTAINERS,
|
|
222
|
+
} from "./core/phase.ts";
|
|
223
|
+
export type {
|
|
224
|
+
CrumbKind,
|
|
225
|
+
CrumbSource,
|
|
226
|
+
DerivePhaseOptions,
|
|
227
|
+
FurthestReached,
|
|
228
|
+
InstancePhase,
|
|
229
|
+
LineagePhaseRollup,
|
|
230
|
+
Phase,
|
|
231
|
+
PhaseCrumb,
|
|
232
|
+
PhaseGranularity,
|
|
233
|
+
ProvenanceProgressRow,
|
|
234
|
+
ScopeElement,
|
|
235
|
+
ScopeIndex,
|
|
236
|
+
} from "./core/phase.ts";
|
|
237
|
+
|
|
146
238
|
// Model deploy (deploy-by-convention under `resources/`, ADR 0062)
|
|
147
239
|
export { deployModels, RESOURCES_DIR } from "./core/modules/deploy.ts";
|
|
148
240
|
export { defaultScheduler, MAX_TIMER_DELAY_MS } from "./core/modules/scheduler.ts";
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
buildSystemBrief,
|
|
5
|
+
deriveSystemBrief,
|
|
6
|
+
emitSystemBriefMd,
|
|
7
|
+
foldOwnership,
|
|
8
|
+
scanModelDecisions,
|
|
9
|
+
SYSTEM_BRIEF_JSON,
|
|
10
|
+
SYSTEM_BRIEF_MD,
|
|
11
|
+
type SystemBrief,
|
|
12
|
+
} from "./system-brief.ts";
|
|
13
|
+
import type { ModelSource } from "./worker-io.ts";
|
|
14
|
+
|
|
15
|
+
// A model with a service task (worker), a business-rule task (decision), and ownership
|
|
16
|
+
// `nano:meta` (single-valued + two repeated `adr`s).
|
|
17
|
+
const ORDER_XML = `
|
|
18
|
+
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
|
|
19
|
+
xmlns:zeebe="http://camunda.org/schema/zeebe/1.0">
|
|
20
|
+
<bpmn:process id="order-fulfilment">
|
|
21
|
+
<bpmn:extensionElements>
|
|
22
|
+
<nano:meta xmlns:nano="urn:nano" key="owner" value="payments-team" />
|
|
23
|
+
<nano:meta xmlns:nano="urn:nano" key="slack" value="#payments" />
|
|
24
|
+
<nano:meta xmlns:nano="urn:nano" key="adr" value="ADR 0040" />
|
|
25
|
+
<nano:meta xmlns:nano="urn:nano" key="adr" value="ADR 0060" />
|
|
26
|
+
</bpmn:extensionElements>
|
|
27
|
+
<bpmn:serviceTask id="charge">
|
|
28
|
+
<bpmn:extensionElements>
|
|
29
|
+
<zeebe:taskDefinition type="charge-card" />
|
|
30
|
+
<zeebe:ioMapping>
|
|
31
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="io.acme.ChargeReq" />
|
|
32
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.out" value="io.acme.ChargeRes" />
|
|
33
|
+
</zeebe:ioMapping>
|
|
34
|
+
</bpmn:extensionElements>
|
|
35
|
+
</bpmn:serviceTask>
|
|
36
|
+
<bpmn:businessRuleTask id="risk">
|
|
37
|
+
<bpmn:extensionElements>
|
|
38
|
+
<zeebe:calledDecision decisionId="fraud-score" resultVariable="score" />
|
|
39
|
+
</bpmn:extensionElements>
|
|
40
|
+
</bpmn:businessRuleTask>
|
|
41
|
+
</bpmn:process>
|
|
42
|
+
</bpmn:definitions>`;
|
|
43
|
+
|
|
44
|
+
const models: ModelSource[] = [{ path: "order.bpmn", xml: ORDER_XML }];
|
|
45
|
+
|
|
46
|
+
test("scanModelDecisions extracts businessRuleTask → calledDecision", () => {
|
|
47
|
+
const decisions = scanModelDecisions(ORDER_XML);
|
|
48
|
+
assert.deepEqual(decisions, [
|
|
49
|
+
{ process: "order-fulfilment", elementId: "risk", decisionId: "fraud-score" },
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("foldOwnership folds reserved meta keys, accumulating repeated `adr`", () => {
|
|
54
|
+
const own = foldOwnership(models);
|
|
55
|
+
assert.equal(own.owner, "payments-team");
|
|
56
|
+
assert.equal(own.slack, "#payments");
|
|
57
|
+
assert.deepEqual(own.adrs, ["ADR 0040", "ADR 0060"]);
|
|
58
|
+
assert.equal(own.team, undefined);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// `nano:meta` keys are user-authored, so a crafted key that collides with an
|
|
62
|
+
// `Object.prototype` member (e.g. `toString`) must NOT be treated as a reserved
|
|
63
|
+
// ownership key. Guards against prototype-chain membership checks (`k in OWNERSHIP_KEYS`).
|
|
64
|
+
test("foldOwnership ignores Object.prototype keys in crafted meta", () => {
|
|
65
|
+
const xml = `
|
|
66
|
+
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL">
|
|
67
|
+
<bpmn:process id="hostile">
|
|
68
|
+
<bpmn:extensionElements>
|
|
69
|
+
<nano:meta xmlns:nano="urn:nano" key="toString" value="pwned" />
|
|
70
|
+
<nano:meta xmlns:nano="urn:nano" key="constructor" value="pwned" />
|
|
71
|
+
<nano:meta xmlns:nano="urn:nano" key="hasOwnProperty" value="pwned" />
|
|
72
|
+
</bpmn:extensionElements>
|
|
73
|
+
</bpmn:process>
|
|
74
|
+
</bpmn:definitions>`;
|
|
75
|
+
const own = foldOwnership([{ path: "hostile.bpmn", xml }]);
|
|
76
|
+
assert.deepEqual(own, { adrs: [] });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("buildSystemBrief folds processes, call graph, decisions, ownership", () => {
|
|
80
|
+
const b: SystemBrief = buildSystemBrief(models, "acme-orders");
|
|
81
|
+
assert.equal(b.app, "acme-orders");
|
|
82
|
+
assert.deepEqual(b.processes, ["order-fulfilment"]);
|
|
83
|
+
assert.equal(b.workers.length, 1);
|
|
84
|
+
assert.equal(b.workers[0].taskType, "charge-card");
|
|
85
|
+
assert.equal(b.workers[0].in, "io.acme.ChargeReq");
|
|
86
|
+
assert.equal(b.decisions[0].decisionId, "fraud-score");
|
|
87
|
+
assert.equal(b.ownership.owner, "payments-team");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("emitSystemBriefMd renders owner, call graph, and decisions", () => {
|
|
91
|
+
const md = emitSystemBriefMd(buildSystemBrief(models, "acme-orders"));
|
|
92
|
+
assert.match(md, /# acme-orders — system brief/);
|
|
93
|
+
assert.match(md, /payments-team/);
|
|
94
|
+
assert.match(md, /ADR 0040, ADR 0060/);
|
|
95
|
+
assert.match(md, /charge-card/);
|
|
96
|
+
assert.match(md, /fraud-score/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("emitSystemBriefMd degrades gracefully with no ownership meta", () => {
|
|
100
|
+
const bare = `<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL">
|
|
101
|
+
<bpmn:process id="p1"></bpmn:process></bpmn:definitions>`;
|
|
102
|
+
const md = emitSystemBriefMd(buildSystemBrief([{ path: "p.bpmn", xml: bare }]));
|
|
103
|
+
assert.match(md, /No ownership metadata declared/);
|
|
104
|
+
assert.match(md, /## Processes/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("emitSystemBriefMd escapes `|` and newlines in model-authored ownership cells", () => {
|
|
108
|
+
const evil = `<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL">
|
|
109
|
+
<bpmn:process id="p1">
|
|
110
|
+
<bpmn:extensionElements>
|
|
111
|
+
<nano:meta xmlns:nano="urn:nano" key="owner" value="a | b" />
|
|
112
|
+
</bpmn:extensionElements>
|
|
113
|
+
</bpmn:process></bpmn:definitions>`;
|
|
114
|
+
const md = emitSystemBriefMd(buildSystemBrief([{ path: "p.bpmn", xml: evil }]));
|
|
115
|
+
const ownerLine = md.split("\n").find((l) => l.startsWith("| Owner |"));
|
|
116
|
+
assert.ok(ownerLine, "owner row rendered");
|
|
117
|
+
// The literal `|` from the meta value must be escaped so it can't add a phantom column.
|
|
118
|
+
assert.match(ownerLine!, /a \\\| b/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("processId only recognises `bpmn:process` (consistent with scanModelWorkers)", () => {
|
|
122
|
+
// A non-`bpmn:`-prefixed process must not be listed, otherwise the brief would show a process
|
|
123
|
+
// id while every worker/decision row carried an empty Process column.
|
|
124
|
+
const other = `<definitions xmlns:foo="urn:foo">
|
|
125
|
+
<foo:process id="not-bpmn"></foo:process></definitions>`;
|
|
126
|
+
const b = buildSystemBrief([{ path: "o.bpmn", xml: other }]);
|
|
127
|
+
assert.deepEqual(b.processes, []);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("deriveSystemBrief emits both artifacts with valid JSON", () => {
|
|
131
|
+
const arts = deriveSystemBrief(models, "acme-orders");
|
|
132
|
+
const md = arts.find((a) => a.path.endsWith(SYSTEM_BRIEF_MD));
|
|
133
|
+
const json = arts.find((a) => a.path.endsWith(SYSTEM_BRIEF_JSON));
|
|
134
|
+
assert.ok(md && json);
|
|
135
|
+
const parsed: unknown = JSON.parse(json!.content);
|
|
136
|
+
assertSystemBriefShape(parsed);
|
|
137
|
+
assert.equal(parsed.app, "acme-orders");
|
|
138
|
+
assert.equal(parsed.workers[0].taskType, "charge-card");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/** Runtime-validate the parsed JSON shape (no `as` cast — banned in this repo) and narrow it to
|
|
142
|
+
* `SystemBrief` for the assertions that follow. */
|
|
143
|
+
function assertSystemBriefShape(v: unknown): asserts v is SystemBrief {
|
|
144
|
+
assert.ok(typeof v === "object" && v !== null, "brief is an object");
|
|
145
|
+
assert.ok(!("app" in v) || typeof v.app === "string", "app is absent or a string");
|
|
146
|
+
assert.ok("workers" in v && Array.isArray(v.workers), "workers is an array");
|
|
147
|
+
assert.ok("processes" in v && Array.isArray(v.processes), "processes is an array");
|
|
148
|
+
assert.ok("decisions" in v && Array.isArray(v.decisions), "decisions is an array");
|
|
149
|
+
assert.ok("ownership" in v && typeof v.ownership === "object" && v.ownership !== null, "ownership is an object");
|
|
150
|
+
}
|