@nanobpm/urban 0.95.0 → 0.96.1
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/dist/cli.d.ts +8 -0
- package/dist/cli.js +24 -12
- package/dist/runtime/core/host.d.ts +33 -11
- package/dist/runtime/core/modules/mcp.js +5 -1
- package/dist/runtime/core/process-identity.d.ts +21 -0
- package/dist/runtime/core/process-identity.js +56 -0
- package/dist/runtime/engine/nanosdk.d.ts +3 -5
- package/dist/runtime/engine/nanosdk.js +12 -16
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.js +5 -0
- package/package.json +2 -2
- package/src/bin.mjs +13 -6
- package/src/bin.test.ts +74 -0
- package/src/cli.ts +25 -12
- package/src/runtime/core/host.ts +35 -8
- package/src/runtime/core/modules/mcp.test.ts +24 -0
- package/src/runtime/core/modules/mcp.ts +7 -2
- package/src/runtime/core/process-identity.test.ts +72 -0
- package/src/runtime/core/process-identity.ts +66 -0
- package/src/runtime/engine/nanosdk.test.ts +82 -0
- package/src/runtime/engine/nanosdk.ts +18 -16
- package/src/runtime/index.ts +11 -0
package/dist/cli.d.ts
CHANGED
|
@@ -23,4 +23,12 @@ interface Flags {
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function cmdData(f: Flags, readInput?: () => Promise<string>): Promise<number>;
|
|
25
25
|
export declare function main(argv: string[]): Promise<number>;
|
|
26
|
+
/**
|
|
27
|
+
* Run the CLI as the process entrypoint: `main(argv)`, then drain stdio and exit with its code.
|
|
28
|
+
* A negative code means a long-running command (`run`, `dev`) owns the process, so it is left
|
|
29
|
+
* alive. Exported so the `urban` bin (`bin.mjs`) can run the CLI **in-process** via `import()`
|
|
30
|
+
* instead of spawning a second Node (nano-ide#589): every `urban` command, notably each
|
|
31
|
+
* gateway `urban data` op, used to pay two Node start-ups.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runCli(argv: string[]): Promise<void>;
|
|
26
34
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -457,24 +457,23 @@ async function flushStdio() {
|
|
|
457
457
|
return;
|
|
458
458
|
await Promise.all([flushStream(p.stdout), flushStream(p.stderr)]);
|
|
459
459
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
const
|
|
470
|
-
const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
|
|
460
|
+
/**
|
|
461
|
+
* Run the CLI as the process entrypoint: `main(argv)`, then drain stdio and exit with its code.
|
|
462
|
+
* A negative code means a long-running command (`run`, `dev`) owns the process, so it is left
|
|
463
|
+
* alive. Exported so the `urban` bin (`bin.mjs`) can run the CLI **in-process** via `import()`
|
|
464
|
+
* instead of spawning a second Node (nano-ide#589): every `urban` command, notably each
|
|
465
|
+
* gateway `urban data` op, used to pay two Node start-ups.
|
|
466
|
+
*/
|
|
467
|
+
export function runCli(argv) {
|
|
468
|
+
const proc = processGlobal();
|
|
469
|
+
const deno = denoGlobal();
|
|
471
470
|
const exit = (code) => {
|
|
472
471
|
if (proc?.exit)
|
|
473
472
|
proc.exit(code);
|
|
474
473
|
else if (deno?.exit)
|
|
475
474
|
deno.exit(code);
|
|
476
475
|
};
|
|
477
|
-
main(argv).then(async (code) => {
|
|
476
|
+
return main(argv).then(async (code) => {
|
|
478
477
|
if (code >= 0) {
|
|
479
478
|
await flushStdio();
|
|
480
479
|
exit(code);
|
|
@@ -485,3 +484,16 @@ if (importMetaMain === true || nodeMain) {
|
|
|
485
484
|
exit(1);
|
|
486
485
|
});
|
|
487
486
|
}
|
|
487
|
+
const proc = processGlobal();
|
|
488
|
+
const deno = denoGlobal();
|
|
489
|
+
const importMetaMain = Reflect.get(import.meta, "main");
|
|
490
|
+
const argv1 = proc?.argv?.[1];
|
|
491
|
+
const nodeMain = argv1 ? import.meta.url === pathToFileURL(argv1).href : false;
|
|
492
|
+
if (importMetaMain === true || nodeMain) {
|
|
493
|
+
// Node passes args via process.argv (slice off exec+script); Deno (run directly)
|
|
494
|
+
// exposes them on Deno.args. Prefer whichever actually carries args.
|
|
495
|
+
const fromNode = proc?.argv?.slice(2);
|
|
496
|
+
const fromDeno = deno?.args;
|
|
497
|
+
const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
|
|
498
|
+
void runCli(argv);
|
|
499
|
+
}
|
|
@@ -216,14 +216,33 @@ export type ProcessInstanceState = "ACTIVE" | "COMPLETED" | "TERMINATED";
|
|
|
216
216
|
* never surface in the `taskInbox` as if it were actionable.
|
|
217
217
|
*/
|
|
218
218
|
export type UserTaskState = "CREATED" | "COMPLETED" | "CANCELED" | "FAILED";
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Which deployed process definition an instance runs — shared by the instance a
|
|
221
|
+
* {@link EngineClient.createInstance} starts and the {@link ProcessInstanceSnapshot} a
|
|
222
|
+
* {@link EngineClient.searchProcessInstances} reports, so the two describe the same instance
|
|
223
|
+
* identically. Each field is absent when the engine omits it (or reports it blank/malformed).
|
|
224
|
+
*/
|
|
225
|
+
export interface ProcessDefinitionIdentity {
|
|
223
226
|
/** The key of the deployed process definition this instance runs — the handle a caller feeds to
|
|
224
227
|
* {@link EngineClient.getProcessDefinitionXml} to fetch the deployed BPMN (the instance →
|
|
225
|
-
* `processDefinitionKey` → deployed XML path, no repo checkout).
|
|
228
|
+
* `processDefinitionKey` → deployed XML path, no repo checkout). Unique per deployed version. */
|
|
226
229
|
readonly processDefinitionKey?: string;
|
|
230
|
+
/** The BPMN process id (`<bpmn:process id>`) — stable across versions; the selector
|
|
231
|
+
* {@link EngineClient.searchProcessInstances} accepts as `processDefinitionId`. */
|
|
232
|
+
readonly processDefinitionId?: string;
|
|
233
|
+
/** The deployed version of `processDefinitionId` this instance runs (1-based). */
|
|
234
|
+
readonly processDefinitionVersion?: number;
|
|
235
|
+
}
|
|
236
|
+
/** The instance {@link EngineClient.createInstance} started: its key, the exact deployed
|
|
237
|
+
* definition version it runs, and — when `awaitCompletion` was requested — its final variables. */
|
|
238
|
+
export interface CreatedProcessInstance extends ProcessDefinitionIdentity {
|
|
239
|
+
readonly processInstanceKey: string;
|
|
240
|
+
readonly variables?: Record<string, unknown>;
|
|
241
|
+
}
|
|
242
|
+
/** A single process instance's lifecycle snapshot returned by {@link EngineClient.searchProcessInstances}. */
|
|
243
|
+
export interface ProcessInstanceSnapshot extends ProcessDefinitionIdentity {
|
|
244
|
+
readonly processInstanceKey: string;
|
|
245
|
+
readonly state: ProcessInstanceState;
|
|
227
246
|
/** The key of this instance's *immediate* parent process instance — the caller that instantiated
|
|
228
247
|
* it as a native child (a `<bpmn:callActivity>`). `undefined` for a top-level (root) instance,
|
|
229
248
|
* and absent when the engine omits it (same optional/engine-provided convention as
|
|
@@ -664,15 +683,14 @@ export interface EngineClient {
|
|
|
664
683
|
}[]): Promise<{
|
|
665
684
|
deployed: number;
|
|
666
685
|
}>;
|
|
667
|
-
/** Start a process instance
|
|
686
|
+
/** Start a process instance of the latest deployed version of `processDefinitionId`. Returns
|
|
687
|
+
* the new instance's key plus the {@link ProcessDefinitionIdentity} (key, id, version) of the
|
|
688
|
+
* definition it actually runs. */
|
|
668
689
|
createInstance(input: {
|
|
669
690
|
processDefinitionId: string;
|
|
670
691
|
variables?: Record<string, unknown>;
|
|
671
692
|
awaitCompletion?: boolean;
|
|
672
|
-
}): Promise<
|
|
673
|
-
processInstanceKey: string;
|
|
674
|
-
variables?: Record<string, unknown>;
|
|
675
|
-
}>;
|
|
693
|
+
}): Promise<CreatedProcessInstance>;
|
|
676
694
|
/** Cancel a running process instance (the pages surface's row-cancel action). */
|
|
677
695
|
cancelInstance(input: {
|
|
678
696
|
processInstanceKey: string;
|
|
@@ -725,7 +743,8 @@ export interface EngineClient {
|
|
|
725
743
|
/** Complete a user task. */
|
|
726
744
|
completeUserTask(userTaskKey: string, variables?: Record<string, unknown>): Promise<void>;
|
|
727
745
|
/**
|
|
728
|
-
* Search process instances by key
|
|
746
|
+
* Search process instances by key, BPMN process id (`processDefinitionId`, any version),
|
|
747
|
+
* and/or lifecycle state. The instance-tracking
|
|
729
748
|
* reconciler uses this to detect a tracked instance reaching a terminal state
|
|
730
749
|
* (`TERMINATED`/`COMPLETED`) even though no completion worker ran for it — the
|
|
731
750
|
* read-model row it backs would otherwise stay "active" forever. An eventually
|
|
@@ -735,6 +754,9 @@ export interface EngineClient {
|
|
|
735
754
|
searchProcessInstances(filter?: {
|
|
736
755
|
processInstanceKeys?: string[];
|
|
737
756
|
state?: ProcessInstanceState;
|
|
757
|
+
/** Select only instances of this BPMN process id (every deployed version). A blank or
|
|
758
|
+
* whitespace-only value is treated as absent (no filter). */
|
|
759
|
+
processDefinitionId?: string;
|
|
738
760
|
/** Select only instances whose *immediate* parent is this process instance — the reduced-path
|
|
739
761
|
* query for "the native children spawned by this subject". */
|
|
740
762
|
parentProcessInstanceKey?: string;
|
|
@@ -428,11 +428,12 @@ function buildDebugTools(app) {
|
|
|
428
428
|
return [
|
|
429
429
|
{
|
|
430
430
|
name: `${DEBUG_PREFIX}search_process_instances`,
|
|
431
|
-
description: "Engine truth: search process instances by key and/or lifecycle state.",
|
|
431
|
+
description: "Engine truth: search process instances by key, BPMN process id (processDefinitionId, any version), and/or lifecycle state.",
|
|
432
432
|
inputSchema: {
|
|
433
433
|
type: "object",
|
|
434
434
|
properties: {
|
|
435
435
|
processInstanceKeys: { type: "array", items: OPTIONAL_STRING },
|
|
436
|
+
processDefinitionId: OPTIONAL_STRING,
|
|
436
437
|
state: { type: "string", enum: ["ACTIVE", "COMPLETED", "TERMINATED"] },
|
|
437
438
|
},
|
|
438
439
|
},
|
|
@@ -444,6 +445,9 @@ function buildDebugTools(app) {
|
|
|
444
445
|
const state = readProcessInstanceState(args, "state");
|
|
445
446
|
if (state)
|
|
446
447
|
filter.state = state;
|
|
448
|
+
const processDefinitionId = readPresentString(args, "processDefinitionId");
|
|
449
|
+
if (processDefinitionId)
|
|
450
|
+
filter.processDefinitionId = processDefinitionId;
|
|
447
451
|
return app.engine.searchProcessInstances(filter);
|
|
448
452
|
},
|
|
449
453
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ProcessDefinitionIdentity } from "./host.ts";
|
|
2
|
+
export type { ProcessDefinitionIdentity };
|
|
3
|
+
/** A non-empty string form of an engine key/id, or `undefined` when absent/blank (including a
|
|
4
|
+
* whitespace-only string). Coerces a *finite* numeric key to a string (the engine may serialize a
|
|
5
|
+
* key either way) but never `String(...)`-coerces an arbitrary object into a garbage
|
|
6
|
+
* `"[object Object]"` id, nor a non-finite number into `"NaN"`/`"Infinity"`. The blank check trims,
|
|
7
|
+
* matching the read-path blank-key guards, so a `" "` key can never leak into a result. */
|
|
8
|
+
export declare function presentEngineKey(value: unknown): string | undefined;
|
|
9
|
+
/** A present string-only identifier (e.g. a BPMN process id) under the shared trim rule; a
|
|
10
|
+
* non-string is absent and never `String(...)`-coerced. */
|
|
11
|
+
export declare function presentString(value: unknown): string | undefined;
|
|
12
|
+
/** The {@link ProcessDefinitionIdentity} an engine row (a create response or a process-instance
|
|
13
|
+
* search row) reports — the single mapping both `createInstance` and `searchProcessInstances` use,
|
|
14
|
+
* so a created instance and its later snapshot describe the same definition identically. Each field
|
|
15
|
+
* passes the shared presence rule; a version that is not a positive integer number is omitted
|
|
16
|
+
* (never coerced). */
|
|
17
|
+
export declare function pickProcessDefinitionIdentity(row: {
|
|
18
|
+
processDefinitionKey?: unknown;
|
|
19
|
+
processDefinitionId?: unknown;
|
|
20
|
+
processDefinitionVersion?: unknown;
|
|
21
|
+
}): ProcessDefinitionIdentity;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The single, transport-agnostic normalizer for a {@link ProcessDefinitionIdentity}. Both engine
|
|
2
|
+
// adapters — the live SDK/REST adapter (`engine/nanosdk.ts`, `SdkEngineClient`) and the in-process
|
|
3
|
+
// WASM test adapter (`@nanobpm/urban-testkit`, `WasmEngineClient`) — report which deployed
|
|
4
|
+
// definition an instance runs on *both* `createInstance` and `searchProcessInstances`. Each used to
|
|
5
|
+
// carry its own hand-mirrored `pickProcessDefinitionIdentity` (plus a re-declared identity type), so
|
|
6
|
+
// the two could silently diverge the moment identity validation changed — exactly the drift class
|
|
7
|
+
// the shared form-contract normalizer exists to kill (`./form-contract.ts`, issue #252). Centralizing
|
|
8
|
+
// the mapper (and reusing the canonical {@link ProcessDefinitionIdentity}) here removes that surface:
|
|
9
|
+
// a created instance and its later snapshot describe the same definition identically wherever this
|
|
10
|
+
// normalizer runs, because they run the *same* code.
|
|
11
|
+
//
|
|
12
|
+
// Ownership today: only the SDK adapter runs this implementation. `WasmEngineClient` still defines
|
|
13
|
+
// and calls its own behaviourally identical copy (`urban-testkit/src/wasm-engine.ts`) — a deliberate,
|
|
14
|
+
// temporary fork: this module is introduced by the same change, so it exists in no published urban
|
|
15
|
+
// release, and a value import would force the testkit's peer floor up to an urban version that does
|
|
16
|
+
// not yet exist (AGENTS.md: depend on an unreleased sibling only after it is published). Follow-up
|
|
17
|
+
// #585 switches the WASM adapter to this canonical import once the urban release carrying it is
|
|
18
|
+
// published; until then the testkit's own identity cases pin its copy to this normalizer's behaviour.
|
|
19
|
+
/** A non-empty string form of an engine key/id, or `undefined` when absent/blank (including a
|
|
20
|
+
* whitespace-only string). Coerces a *finite* numeric key to a string (the engine may serialize a
|
|
21
|
+
* key either way) but never `String(...)`-coerces an arbitrary object into a garbage
|
|
22
|
+
* `"[object Object]"` id, nor a non-finite number into `"NaN"`/`"Infinity"`. The blank check trims,
|
|
23
|
+
* matching the read-path blank-key guards, so a `" "` key can never leak into a result. */
|
|
24
|
+
export function presentEngineKey(value) {
|
|
25
|
+
if (typeof value === "number") {
|
|
26
|
+
return Number.isFinite(value) ? String(value) : undefined;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value !== "string")
|
|
29
|
+
return undefined;
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
return trimmed === "" ? undefined : trimmed;
|
|
32
|
+
}
|
|
33
|
+
/** A present string-only identifier (e.g. a BPMN process id) under the shared trim rule; a
|
|
34
|
+
* non-string is absent and never `String(...)`-coerced. */
|
|
35
|
+
export function presentString(value) {
|
|
36
|
+
if (typeof value !== "string")
|
|
37
|
+
return undefined;
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
return trimmed === "" ? undefined : trimmed;
|
|
40
|
+
}
|
|
41
|
+
/** The {@link ProcessDefinitionIdentity} an engine row (a create response or a process-instance
|
|
42
|
+
* search row) reports — the single mapping both `createInstance` and `searchProcessInstances` use,
|
|
43
|
+
* so a created instance and its later snapshot describe the same definition identically. Each field
|
|
44
|
+
* passes the shared presence rule; a version that is not a positive integer number is omitted
|
|
45
|
+
* (never coerced). */
|
|
46
|
+
export function pickProcessDefinitionIdentity(row) {
|
|
47
|
+
const processDefinitionKey = presentEngineKey(row.processDefinitionKey);
|
|
48
|
+
const processDefinitionId = presentString(row.processDefinitionId);
|
|
49
|
+
const version = row.processDefinitionVersion;
|
|
50
|
+
const processDefinitionVersion = typeof version === "number" && Number.isInteger(version) && version > 0 ? version : undefined;
|
|
51
|
+
return {
|
|
52
|
+
...(processDefinitionKey ? { processDefinitionKey } : {}),
|
|
53
|
+
...(processDefinitionId ? { processDefinitionId } : {}),
|
|
54
|
+
...(processDefinitionVersion !== undefined ? { processDefinitionVersion } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EngineClient, AgentHistoryFilter, AgentHistoryRecord, AgentInstanceFilter, AgentInstanceSummary, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, FormSchema, IncidentFilter, IncidentState, IncidentSummary, JobFilter, JobHandler, JobSummary, ProcessInstanceSnapshot, ProcessInstanceState, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WorkerSubscription } from "../core/host.ts";
|
|
1
|
+
import type { EngineClient, AgentHistoryFilter, AgentHistoryRecord, AgentInstanceFilter, AgentInstanceSummary, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, FormSchema, IncidentFilter, IncidentState, IncidentSummary, JobFilter, JobHandler, JobSummary, CreatedProcessInstance, ProcessInstanceSnapshot, ProcessInstanceState, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WorkerSubscription } from "../core/host.ts";
|
|
2
2
|
import type { EngineSdkClient } from "./sdk.ts";
|
|
3
3
|
/** Coerce an engine response's process-instance key to a non-empty string, or
|
|
4
4
|
* throw — a missing key means a malformed/partial response, not a real instance. */
|
|
@@ -233,10 +233,7 @@ export declare class SdkEngineClient implements EngineClient {
|
|
|
233
233
|
processDefinitionId: string;
|
|
234
234
|
variables?: Record<string, unknown>;
|
|
235
235
|
awaitCompletion?: boolean;
|
|
236
|
-
}): Promise<
|
|
237
|
-
processInstanceKey: string;
|
|
238
|
-
variables?: Record<string, unknown>;
|
|
239
|
-
}>;
|
|
236
|
+
}): Promise<CreatedProcessInstance>;
|
|
240
237
|
cancelInstance(input: {
|
|
241
238
|
processInstanceKey: string;
|
|
242
239
|
}): Promise<void>;
|
|
@@ -260,6 +257,7 @@ export declare class SdkEngineClient implements EngineClient {
|
|
|
260
257
|
searchProcessInstances(filter?: {
|
|
261
258
|
processInstanceKeys?: string[];
|
|
262
259
|
state?: ProcessInstanceState;
|
|
260
|
+
processDefinitionId?: string;
|
|
263
261
|
parentProcessInstanceKey?: string;
|
|
264
262
|
rootProcessInstanceKey?: string;
|
|
265
263
|
}): Promise<ProcessInstanceSnapshot[]>;
|
|
@@ -19,6 +19,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
19
19
|
return path;
|
|
20
20
|
};
|
|
21
21
|
import { assertDeployedWaitStateType, isBpmnError } from "../core/host.js";
|
|
22
|
+
import { pickProcessDefinitionIdentity, presentEngineKey, presentString, } from "../core/process-identity.js";
|
|
22
23
|
import { buildFormSchema, parseFormSchema, pickFormLinkage, resolveFormIdentifier, } from "../core/form-contract.js";
|
|
23
24
|
import { isRecord } from "../core/guards.js";
|
|
24
25
|
import { applyAmbientLineage } from "../core/lineage.js";
|
|
@@ -111,20 +112,11 @@ export function normalizeIncidentState(raw) {
|
|
|
111
112
|
return "UNKNOWN";
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
function presentEngineKey(value) {
|
|
120
|
-
if (typeof value === "number") {
|
|
121
|
-
return Number.isFinite(value) ? String(value) : undefined;
|
|
122
|
-
}
|
|
123
|
-
if (typeof value !== "string")
|
|
124
|
-
return undefined;
|
|
125
|
-
const trimmed = value.trim();
|
|
126
|
-
return trimmed === "" ? undefined : trimmed;
|
|
127
|
-
}
|
|
115
|
+
// Engine-key / process-definition-identity normalization now lives in the shared, adapter-agnostic
|
|
116
|
+
// `../core/process-identity.ts` (imported above), so this adapter's `createInstance` and
|
|
117
|
+
// `searchProcessInstances` run the *same* mapping — see that module's header for the drift class
|
|
118
|
+
// this removes. The WASM test adapter keeps a mirrored local copy until #585 (that module header's
|
|
119
|
+
// "Ownership today" note), so the shared mapping is not yet a cross-adapter guarantee.
|
|
128
120
|
/** The non-empty, trimmed string form of a *required* engine key, or throws when the value is
|
|
129
121
|
* absent/blank (including a whitespace-only string). A mutating seam operation addresses a
|
|
130
122
|
* single entity by key, so a blank key is a caller bug that must fail fast with an actionable
|
|
@@ -632,6 +624,7 @@ export class SdkEngineClient {
|
|
|
632
624
|
const variables = isRecord(body.variables) ? body.variables : undefined;
|
|
633
625
|
return {
|
|
634
626
|
processInstanceKey: requireProcessInstanceKey(key),
|
|
627
|
+
...pickProcessDefinitionIdentity(body),
|
|
635
628
|
variables,
|
|
636
629
|
};
|
|
637
630
|
}
|
|
@@ -749,6 +742,10 @@ export class SdkEngineClient {
|
|
|
749
742
|
const f = {};
|
|
750
743
|
if (filter?.state)
|
|
751
744
|
f.state = filter.state;
|
|
745
|
+
// A blank BPMN process-id selector is absent, not an empty filter that matches nothing.
|
|
746
|
+
const processDefinitionId = presentString(filter?.processDefinitionId);
|
|
747
|
+
if (processDefinitionId)
|
|
748
|
+
f.processDefinitionId = processDefinitionId;
|
|
752
749
|
// Normalize each requested key through the shared presence rule (drop blank/whitespace-only
|
|
753
750
|
// entries, trim padded ones like `" 123 "`) so the `$in` filter carries only present keys that
|
|
754
751
|
// match the normalized keys we surface — mirroring `WasmEngineClient` and the selector
|
|
@@ -792,13 +789,12 @@ export class SdkEngineClient {
|
|
|
792
789
|
});
|
|
793
790
|
return [];
|
|
794
791
|
}
|
|
795
|
-
const processDefinitionKey = presentEngineKey(it.processDefinitionKey);
|
|
796
792
|
const parentProcessInstanceKey = presentEngineKey(it.parentProcessInstanceKey);
|
|
797
793
|
const rootProcessInstanceKey = presentEngineKey(it.rootProcessInstanceKey);
|
|
798
794
|
return [{
|
|
799
795
|
processInstanceKey,
|
|
800
796
|
state,
|
|
801
|
-
...(
|
|
797
|
+
...pickProcessDefinitionIdentity(it),
|
|
802
798
|
...(parentProcessInstanceKey ? { parentProcessInstanceKey } : {}),
|
|
803
799
|
...(rootProcessInstanceKey ? { rootProcessInstanceKey } : {}),
|
|
804
800
|
}];
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -8,9 +8,10 @@ export { bindModeToHost, expandEnv, expandEnvString, isBindMode, loadManifest, p
|
|
|
8
8
|
export type { ActionDecl, AppManifest, BindMode, ChatSurface, DataSource, DomainField, DomainType, InstanceTracking, LlmBinding, NetworkConfig, PagesSurface, Surfaces, TaskInboxSurface, Trigger, Worker, } from "./core/manifest.ts";
|
|
9
9
|
export { collectManifestIssues, ManifestValidationError, validateManifest, } from "./core/validate.ts";
|
|
10
10
|
export type { ValidationIssue } from "./core/validate.ts";
|
|
11
|
-
export type { EngineClient, AgentHistoryFilter, AgentHistoryRecord, AgentInstanceFilter, AgentInstanceMetrics, AgentInstanceSummary, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, EngineJob, FormSchema, HostContext, HttpHandler, HttpRequest, HttpResponse, HttpServer, IncidentState, EngineIncidentState, IncidentFilter, IncidentSummary, JobFilter, JobHandler, JobSummary, ProcessInstanceSnapshot, ProcessInstanceState, SqliteDb, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WatchHandle, WorkerSubscription, } from "./core/host.ts";
|
|
11
|
+
export type { EngineClient, AgentHistoryFilter, AgentHistoryRecord, AgentInstanceFilter, AgentInstanceMetrics, AgentInstanceSummary, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, EngineJob, FormSchema, HostContext, HttpHandler, HttpRequest, HttpResponse, HttpServer, IncidentState, EngineIncidentState, IncidentFilter, IncidentSummary, JobFilter, JobHandler, JobSummary, CreatedProcessInstance, ProcessDefinitionIdentity, ProcessInstanceSnapshot, ProcessInstanceState, SqliteDb, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WatchHandle, WorkerSubscription, } from "./core/host.ts";
|
|
12
12
|
export type { TranscriptContentBlock, TranscriptContentType, TranscriptToolCall, TranscriptTurnMetrics, TranscriptTurnRole, } from "@nanobpm/agentic/transcript";
|
|
13
13
|
export { buildFormSchema, parseFormSchema, pickFormLinkage, presentFormIdentifier, resolveFormIdentifier, } from "./core/form-contract.ts";
|
|
14
|
+
export { pickProcessDefinitionIdentity, presentEngineKey, presentString, } from "./core/process-identity.ts";
|
|
14
15
|
export type { AppApi, Mounted, RuntimeContext } from "./core/context.ts";
|
|
15
16
|
export { createLogger } from "./core/logger.ts";
|
|
16
17
|
export type { Logger, LogLevel, LogFields, LogSink } from "./core/logger.ts";
|
package/dist/runtime/index.js
CHANGED
|
@@ -12,6 +12,11 @@ export { collectManifestIssues, ManifestValidationError, validateManifest, } fro
|
|
|
12
12
|
// both engine adapters (SDK/REST + WASM test double) call, so their form-identifier,
|
|
13
13
|
// form-schema, and user-task form-linkage handling cannot drift.
|
|
14
14
|
export { buildFormSchema, parseFormSchema, pickFormLinkage, presentFormIdentifier, resolveFormIdentifier, } from "./core/form-contract.js";
|
|
15
|
+
// The adapter-agnostic process-definition-identity normalizer (the single mapping both
|
|
16
|
+
// `createInstance` and `searchProcessInstances` use), owned today by the live SDK adapter; the WASM
|
|
17
|
+
// test adapter keeps a mirrored local copy until #585, so cross-adapter drift-freedom is not yet
|
|
18
|
+
// claimed. `ProcessDefinitionIdentity` itself is re-exported from `./core/host.ts`.
|
|
19
|
+
export { pickProcessDefinitionIdentity, presentEngineKey, presentString, } from "./core/process-identity.js";
|
|
15
20
|
// Structured logging surface (see core/logger.ts): the shape of `AppApi.log`, plus `createLogger`
|
|
16
21
|
// so consumers can build a Logger for a custom sink or a no-op test double (`createLogger(() => {})`).
|
|
17
22
|
export { createLogger } from "./core/logger.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.96.1",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"@nanobpm/agentic": "^0.15.0",
|
|
107
107
|
"@nanobpm/nano-app-schema": "^0.11.0",
|
|
108
108
|
"@nanobpm/nano-sdk": "^1.2.5",
|
|
109
|
-
"@nanobpm/workflow": "^0.
|
|
109
|
+
"@nanobpm/workflow": "^0.15.0",
|
|
110
110
|
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
111
111
|
"create-urban-app": "^0.15.2",
|
|
112
112
|
"yaml": "^2.9.0"
|
package/src/bin.mjs
CHANGED
|
@@ -3,16 +3,23 @@
|
|
|
3
3
|
// published as compiled JS + .d.ts because Node cannot strip types for files under
|
|
4
4
|
// node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so no type-stripping flag
|
|
5
5
|
// is needed here.
|
|
6
|
+
//
|
|
7
|
+
// The CLI is loaded IN-PROCESS (dynamic import + runCli) rather than spawned as a second
|
|
8
|
+
// Node: a spawn doubled the start-up cost of every command, and the gateway runs `urban
|
|
9
|
+
// data` once per data op (nano-ide#589, nano-bpm#1340). dist/cli.js only auto-runs when it
|
|
10
|
+
// is argv[1] itself, so importing it here does not start it twice.
|
|
6
11
|
|
|
7
|
-
import {
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
13
|
import { dirname, join } from "node:path";
|
|
10
14
|
|
|
11
15
|
const cli = join(dirname(fileURLToPath(import.meta.url)), "..", "dist", "cli.js");
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
let runCli;
|
|
18
|
+
try {
|
|
19
|
+
({ runCli } = await import(pathToFileURL(cli).href));
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(String(err?.message ?? err));
|
|
16
22
|
process.exit(1);
|
|
17
23
|
}
|
|
18
|
-
|
|
24
|
+
// runCli owns the exit code, stdio flushing and long-running commands (run/dev).
|
|
25
|
+
await runCli(process.argv.slice(2));
|
package/src/bin.test.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The `urban` bin (`bin.mjs`) must run the compiled CLI in the SAME Node process (nano-ide#589):
|
|
2
|
+
// the gateway spawns `urban data` per data op, and the old spawnSync bootstrap paid two Node
|
|
3
|
+
// start-ups for every one. Exercises the real bin against dist/ (CI builds before testing).
|
|
4
|
+
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
+
|
|
13
|
+
const bin = fileURLToPath(new URL("./bin.mjs", import.meta.url));
|
|
14
|
+
const dist = fileURLToPath(new URL("../dist/cli.js", import.meta.url));
|
|
15
|
+
|
|
16
|
+
function runBin(
|
|
17
|
+
args: string[],
|
|
18
|
+
stdin: string,
|
|
19
|
+
env: NodeJS.ProcessEnv,
|
|
20
|
+
): Promise<{ stdout: string; code: number | null }> {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const child = spawn(process.execPath, [bin, ...args], {
|
|
23
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
24
|
+
env: { ...process.env, ...env },
|
|
25
|
+
});
|
|
26
|
+
const chunks: Buffer[] = [];
|
|
27
|
+
child.stdout.on("data", (c: Buffer) => chunks.push(c));
|
|
28
|
+
child.on("error", reject);
|
|
29
|
+
child.on("close", (code) => resolve({ stdout: Buffer.concat(chunks).toString("utf8"), code }));
|
|
30
|
+
child.stdin.end(stdin);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// `bin.mjs` is the npm bin, so it always runs under Node: skip under Deno (`test:deno`), whose
|
|
35
|
+
// process.execPath is not Node (same as generated-lint.test.ts).
|
|
36
|
+
const runtimeIsNode = !("Deno" in globalThis);
|
|
37
|
+
|
|
38
|
+
test("bin.mjs runs the CLI in-process: one Node per command, reply + exit code intact", {
|
|
39
|
+
skip: runtimeIsNode ? false : "Node-only: spawns bin.mjs via process.execPath (Deno's execPath is not Node)",
|
|
40
|
+
}, async (t) => {
|
|
41
|
+
if (!existsSync(dist)) {
|
|
42
|
+
t.skip("dist/cli.js not built (run `npm run build`); CI builds before testing");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const dir = mkdtempSync(join(tmpdir(), "urban-bin-"));
|
|
46
|
+
try {
|
|
47
|
+
// A preload every Node process runs (NODE_OPTIONS is inherited by child Nodes), recording
|
|
48
|
+
// its pid. The old spawnSync bootstrap would record two.
|
|
49
|
+
const pids = join(dir, "pids.txt");
|
|
50
|
+
const preload = join(dir, "preload.mjs");
|
|
51
|
+
writeFileSync(
|
|
52
|
+
preload,
|
|
53
|
+
`import { appendFileSync } from "node:fs";\nappendFileSync(${JSON.stringify(pids)}, process.pid + "\\n");\n`,
|
|
54
|
+
);
|
|
55
|
+
const env = { NODE_OPTIONS: `--import=${pathToFileURL(preload).href}` };
|
|
56
|
+
|
|
57
|
+
// An unreadable request: `urban data` still answers with a parseable envelope and exits 0.
|
|
58
|
+
const { stdout, code } = await runBin(["data", "--root", dir], "not json", env);
|
|
59
|
+
assert.equal(code, 0, `bin exited ${code}, expected 0`);
|
|
60
|
+
const reply: unknown = JSON.parse(stdout);
|
|
61
|
+
assert.ok(reply !== null && typeof reply === "object", "reply is not an object");
|
|
62
|
+
assert.equal(Reflect.get(reply, "ok"), false);
|
|
63
|
+
assert.match(String(Reflect.get(reply, "error")), /^bad request:/);
|
|
64
|
+
|
|
65
|
+
const started = readFileSync(pids, "utf8").trim().split("\n").filter(Boolean);
|
|
66
|
+
assert.equal(started.length, 1, `expected 1 Node process, got ${started.length}`);
|
|
67
|
+
|
|
68
|
+
// A failing command's non-zero exit code still propagates through the bin.
|
|
69
|
+
const unknown = await runBin(["no-such-command"], "", {});
|
|
70
|
+
assert.equal(unknown.code, 1);
|
|
71
|
+
} finally {
|
|
72
|
+
rmSync(dir, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -472,22 +472,21 @@ async function flushStdio(): Promise<void> {
|
|
|
472
472
|
await Promise.all([flushStream(p.stdout), flushStream(p.stderr)]);
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
|
|
475
|
+
/**
|
|
476
|
+
* Run the CLI as the process entrypoint: `main(argv)`, then drain stdio and exit with its code.
|
|
477
|
+
* A negative code means a long-running command (`run`, `dev`) owns the process, so it is left
|
|
478
|
+
* alive. Exported so the `urban` bin (`bin.mjs`) can run the CLI **in-process** via `import()`
|
|
479
|
+
* instead of spawning a second Node (nano-ide#589): every `urban` command, notably each
|
|
480
|
+
* gateway `urban data` op, used to pay two Node start-ups.
|
|
481
|
+
*/
|
|
482
|
+
export function runCli(argv: string[]): Promise<void> {
|
|
483
|
+
const proc = processGlobal();
|
|
484
|
+
const deno = denoGlobal();
|
|
486
485
|
const exit = (code: number) => {
|
|
487
486
|
if (proc?.exit) proc.exit(code);
|
|
488
487
|
else if (deno?.exit) deno.exit(code);
|
|
489
488
|
};
|
|
490
|
-
main(argv).then(
|
|
489
|
+
return main(argv).then(
|
|
491
490
|
async (code) => {
|
|
492
491
|
if (code >= 0) {
|
|
493
492
|
await flushStdio();
|
|
@@ -501,3 +500,17 @@ if (importMetaMain === true || nodeMain) {
|
|
|
501
500
|
},
|
|
502
501
|
);
|
|
503
502
|
}
|
|
503
|
+
|
|
504
|
+
const proc = processGlobal();
|
|
505
|
+
const deno = denoGlobal();
|
|
506
|
+
const importMetaMain: unknown = Reflect.get(import.meta, "main");
|
|
507
|
+
const argv1 = proc?.argv?.[1];
|
|
508
|
+
const nodeMain = argv1 ? import.meta.url === pathToFileURL(argv1).href : false;
|
|
509
|
+
if (importMetaMain === true || nodeMain) {
|
|
510
|
+
// Node passes args via process.argv (slice off exec+script); Deno (run directly)
|
|
511
|
+
// exposes them on Deno.args. Prefer whichever actually carries args.
|
|
512
|
+
const fromNode = proc?.argv?.slice(2);
|
|
513
|
+
const fromDeno = deno?.args;
|
|
514
|
+
const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
|
|
515
|
+
void runCli(argv);
|
|
516
|
+
}
|
package/src/runtime/core/host.ts
CHANGED
|
@@ -257,14 +257,35 @@ export type ProcessInstanceState = "ACTIVE" | "COMPLETED" | "TERMINATED";
|
|
|
257
257
|
*/
|
|
258
258
|
export type UserTaskState = "CREATED" | "COMPLETED" | "CANCELED" | "FAILED";
|
|
259
259
|
|
|
260
|
-
/**
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Which deployed process definition an instance runs — shared by the instance a
|
|
262
|
+
* {@link EngineClient.createInstance} starts and the {@link ProcessInstanceSnapshot} a
|
|
263
|
+
* {@link EngineClient.searchProcessInstances} reports, so the two describe the same instance
|
|
264
|
+
* identically. Each field is absent when the engine omits it (or reports it blank/malformed).
|
|
265
|
+
*/
|
|
266
|
+
export interface ProcessDefinitionIdentity {
|
|
264
267
|
/** The key of the deployed process definition this instance runs — the handle a caller feeds to
|
|
265
268
|
* {@link EngineClient.getProcessDefinitionXml} to fetch the deployed BPMN (the instance →
|
|
266
|
-
* `processDefinitionKey` → deployed XML path, no repo checkout).
|
|
269
|
+
* `processDefinitionKey` → deployed XML path, no repo checkout). Unique per deployed version. */
|
|
267
270
|
readonly processDefinitionKey?: string;
|
|
271
|
+
/** The BPMN process id (`<bpmn:process id>`) — stable across versions; the selector
|
|
272
|
+
* {@link EngineClient.searchProcessInstances} accepts as `processDefinitionId`. */
|
|
273
|
+
readonly processDefinitionId?: string;
|
|
274
|
+
/** The deployed version of `processDefinitionId` this instance runs (1-based). */
|
|
275
|
+
readonly processDefinitionVersion?: number;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The instance {@link EngineClient.createInstance} started: its key, the exact deployed
|
|
279
|
+
* definition version it runs, and — when `awaitCompletion` was requested — its final variables. */
|
|
280
|
+
export interface CreatedProcessInstance extends ProcessDefinitionIdentity {
|
|
281
|
+
readonly processInstanceKey: string;
|
|
282
|
+
readonly variables?: Record<string, unknown>;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** A single process instance's lifecycle snapshot returned by {@link EngineClient.searchProcessInstances}. */
|
|
286
|
+
export interface ProcessInstanceSnapshot extends ProcessDefinitionIdentity {
|
|
287
|
+
readonly processInstanceKey: string;
|
|
288
|
+
readonly state: ProcessInstanceState;
|
|
268
289
|
/** The key of this instance's *immediate* parent process instance — the caller that instantiated
|
|
269
290
|
* it as a native child (a `<bpmn:callActivity>`). `undefined` for a top-level (root) instance,
|
|
270
291
|
* and absent when the engine omits it (same optional/engine-provided convention as
|
|
@@ -741,12 +762,14 @@ export interface EngineClient {
|
|
|
741
762
|
deployResources(
|
|
742
763
|
resources: { name: string; content: string; contentType: string }[],
|
|
743
764
|
): Promise<{ deployed: number }>;
|
|
744
|
-
/** Start a process instance
|
|
765
|
+
/** Start a process instance of the latest deployed version of `processDefinitionId`. Returns
|
|
766
|
+
* the new instance's key plus the {@link ProcessDefinitionIdentity} (key, id, version) of the
|
|
767
|
+
* definition it actually runs. */
|
|
745
768
|
createInstance(input: {
|
|
746
769
|
processDefinitionId: string;
|
|
747
770
|
variables?: Record<string, unknown>;
|
|
748
771
|
awaitCompletion?: boolean;
|
|
749
|
-
}): Promise<
|
|
772
|
+
}): Promise<CreatedProcessInstance>;
|
|
750
773
|
/** Cancel a running process instance (the pages surface's row-cancel action). */
|
|
751
774
|
cancelInstance(input: { processInstanceKey: string }): Promise<void>;
|
|
752
775
|
/** Publish a message for correlation. */
|
|
@@ -794,7 +817,8 @@ export interface EngineClient {
|
|
|
794
817
|
/** Complete a user task. */
|
|
795
818
|
completeUserTask(userTaskKey: string, variables?: Record<string, unknown>): Promise<void>;
|
|
796
819
|
/**
|
|
797
|
-
* Search process instances by key
|
|
820
|
+
* Search process instances by key, BPMN process id (`processDefinitionId`, any version),
|
|
821
|
+
* and/or lifecycle state. The instance-tracking
|
|
798
822
|
* reconciler uses this to detect a tracked instance reaching a terminal state
|
|
799
823
|
* (`TERMINATED`/`COMPLETED`) even though no completion worker ran for it — the
|
|
800
824
|
* read-model row it backs would otherwise stay "active" forever. An eventually
|
|
@@ -804,6 +828,9 @@ export interface EngineClient {
|
|
|
804
828
|
searchProcessInstances(filter?: {
|
|
805
829
|
processInstanceKeys?: string[];
|
|
806
830
|
state?: ProcessInstanceState;
|
|
831
|
+
/** Select only instances of this BPMN process id (every deployed version). A blank or
|
|
832
|
+
* whitespace-only value is treated as absent (no filter). */
|
|
833
|
+
processDefinitionId?: string;
|
|
807
834
|
/** Select only instances whose *immediate* parent is this process instance — the reduced-path
|
|
808
835
|
* query for "the native children spawned by this subject". */
|
|
809
836
|
parentProcessInstanceKey?: string;
|
|
@@ -617,6 +617,30 @@ test("the framework debug tools appear and return engine truth", async () => {
|
|
|
617
617
|
assert.deepEqual(JSON.parse(text), [snapshot]);
|
|
618
618
|
});
|
|
619
619
|
|
|
620
|
+
test("urban_debug_search_process_instances forwards a processDefinitionId selector", async () => {
|
|
621
|
+
const seen: unknown[] = [];
|
|
622
|
+
const engine = fakeEngine({
|
|
623
|
+
searchProcessInstances: async (filter) => {
|
|
624
|
+
seen.push(filter);
|
|
625
|
+
return [];
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
const { router } = buildHarness({ engine });
|
|
629
|
+
const session = await connect(router);
|
|
630
|
+
const list = await rpc(router, session, "tools/list", {});
|
|
631
|
+
const tools = list.result?.tools;
|
|
632
|
+
assert.ok(Array.isArray(tools));
|
|
633
|
+
const tool = tools.find((t) => Reflect.get(t, "name") === "urban_debug_search_process_instances");
|
|
634
|
+
const props = Reflect.get(Reflect.get(tool ?? {}, "inputSchema") ?? {}, "properties");
|
|
635
|
+
assert.ok(props && typeof props === "object" && "processDefinitionId" in props);
|
|
636
|
+
|
|
637
|
+
await rpc(router, session, "tools/call", {
|
|
638
|
+
name: "urban_debug_search_process_instances",
|
|
639
|
+
arguments: { processDefinitionId: "order", state: "ACTIVE" },
|
|
640
|
+
});
|
|
641
|
+
assert.deepEqual(seen, [{ state: "ACTIVE", processDefinitionId: "order" }]);
|
|
642
|
+
});
|
|
643
|
+
|
|
620
644
|
test("urban_debug_search_variables / search_jobs / get_process_definition_xml are read tools returning engine truth", async () => {
|
|
621
645
|
const variable = {
|
|
622
646
|
variableKey: "v-1",
|
|
@@ -70,6 +70,7 @@ import type {
|
|
|
70
70
|
HttpResponse,
|
|
71
71
|
IncidentFilter,
|
|
72
72
|
JobFilter,
|
|
73
|
+
EngineClient,
|
|
73
74
|
ProcessInstanceState,
|
|
74
75
|
SqliteDb,
|
|
75
76
|
UserTaskFilter,
|
|
@@ -544,20 +545,24 @@ function buildDebugTools(app: AppApi): DebugTool[] {
|
|
|
544
545
|
return [
|
|
545
546
|
{
|
|
546
547
|
name: `${DEBUG_PREFIX}search_process_instances`,
|
|
547
|
-
description:
|
|
548
|
+
description:
|
|
549
|
+
"Engine truth: search process instances by key, BPMN process id (processDefinitionId, any version), and/or lifecycle state.",
|
|
548
550
|
inputSchema: {
|
|
549
551
|
type: "object",
|
|
550
552
|
properties: {
|
|
551
553
|
processInstanceKeys: { type: "array", items: OPTIONAL_STRING },
|
|
554
|
+
processDefinitionId: OPTIONAL_STRING,
|
|
552
555
|
state: { type: "string", enum: ["ACTIVE", "COMPLETED", "TERMINATED"] },
|
|
553
556
|
},
|
|
554
557
|
},
|
|
555
558
|
run: (args) => {
|
|
556
|
-
const filter:
|
|
559
|
+
const filter: NonNullable<Parameters<EngineClient["searchProcessInstances"]>[0]> = {};
|
|
557
560
|
const keys = readStringArray(args, "processInstanceKeys");
|
|
558
561
|
if (keys) filter.processInstanceKeys = keys;
|
|
559
562
|
const state = readProcessInstanceState(args, "state");
|
|
560
563
|
if (state) filter.state = state;
|
|
564
|
+
const processDefinitionId = readPresentString(args, "processDefinitionId");
|
|
565
|
+
if (processDefinitionId) filter.processDefinitionId = processDefinitionId;
|
|
561
566
|
return app.engine.searchProcessInstances(filter);
|
|
562
567
|
},
|
|
563
568
|
},
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
pickProcessDefinitionIdentity,
|
|
5
|
+
presentEngineKey,
|
|
6
|
+
presentString,
|
|
7
|
+
} from "./process-identity.ts";
|
|
8
|
+
|
|
9
|
+
// The canonical, adapter-agnostic process-definition-identity normalizer. These cases pin the
|
|
10
|
+
// class the shared helper exists to keep consistent (No Drift Surfaces): a padded/numeric key is
|
|
11
|
+
// normalized, a blank/whitespace/non-string/non-finite value is *absent* and never coerced, and a
|
|
12
|
+
// version is kept only when a positive integer number. Scope today: only the SDK adapter
|
|
13
|
+
// (`engine/nanosdk.ts`) routes through this mapping; the WASM test adapter
|
|
14
|
+
// (`@nanobpm/urban-testkit`) runs a deliberately mirrored local copy until #585, pinned to this
|
|
15
|
+
// behaviour by its own identity cases (`wasm-engine.test.ts`).
|
|
16
|
+
|
|
17
|
+
test("presentEngineKey coerces a finite numeric key, trims a string, and drops blank/non-finite/non-string", () => {
|
|
18
|
+
assert.equal(presentEngineKey(7), "7");
|
|
19
|
+
assert.equal(presentEngineKey(" order "), "order");
|
|
20
|
+
assert.equal(presentEngineKey(" "), undefined);
|
|
21
|
+
assert.equal(presentEngineKey(""), undefined);
|
|
22
|
+
assert.equal(presentEngineKey(Number.NaN), undefined);
|
|
23
|
+
assert.equal(presentEngineKey(Number.POSITIVE_INFINITY), undefined);
|
|
24
|
+
assert.equal(presentEngineKey({}), undefined);
|
|
25
|
+
assert.equal(presentEngineKey(null), undefined);
|
|
26
|
+
assert.equal(presentEngineKey(undefined), undefined);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("presentString keeps a trimmed non-empty string and drops blank/non-string (never coerces)", () => {
|
|
30
|
+
assert.equal(presentString(" human "), "human");
|
|
31
|
+
assert.equal(presentString(" "), undefined);
|
|
32
|
+
assert.equal(presentString(""), undefined);
|
|
33
|
+
// A number id is absent — never String(...)-coerced into a garbage id.
|
|
34
|
+
assert.equal(presentString(5), undefined);
|
|
35
|
+
assert.equal(presentString({}), undefined);
|
|
36
|
+
assert.equal(presentString(undefined), undefined);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("pickProcessDefinitionIdentity normalizes present fields and omits blank/malformed ones", () => {
|
|
40
|
+
assert.deepEqual(
|
|
41
|
+
pickProcessDefinitionIdentity({
|
|
42
|
+
processDefinitionKey: 7,
|
|
43
|
+
processDefinitionId: " order ",
|
|
44
|
+
processDefinitionVersion: 2,
|
|
45
|
+
}),
|
|
46
|
+
{ processDefinitionKey: "7", processDefinitionId: "order", processDefinitionVersion: 2 },
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("pickProcessDefinitionIdentity drops a blank key, a non-string id, and a stringly version", () => {
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
pickProcessDefinitionIdentity({
|
|
53
|
+
processDefinitionKey: " ",
|
|
54
|
+
processDefinitionId: 5,
|
|
55
|
+
processDefinitionVersion: "2",
|
|
56
|
+
}),
|
|
57
|
+
{},
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("pickProcessDefinitionIdentity keeps a version only when a positive integer number", () => {
|
|
62
|
+
assert.deepEqual(pickProcessDefinitionIdentity({ processDefinitionVersion: 1.5 }), {});
|
|
63
|
+
assert.deepEqual(pickProcessDefinitionIdentity({ processDefinitionVersion: 0 }), {});
|
|
64
|
+
assert.deepEqual(pickProcessDefinitionIdentity({ processDefinitionVersion: -1 }), {});
|
|
65
|
+
assert.deepEqual(pickProcessDefinitionIdentity({ processDefinitionVersion: 3 }), {
|
|
66
|
+
processDefinitionVersion: 3,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("pickProcessDefinitionIdentity on an empty row returns an empty identity (fully absent)", () => {
|
|
71
|
+
assert.deepEqual(pickProcessDefinitionIdentity({}), {});
|
|
72
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The single, transport-agnostic normalizer for a {@link ProcessDefinitionIdentity}. Both engine
|
|
2
|
+
// adapters — the live SDK/REST adapter (`engine/nanosdk.ts`, `SdkEngineClient`) and the in-process
|
|
3
|
+
// WASM test adapter (`@nanobpm/urban-testkit`, `WasmEngineClient`) — report which deployed
|
|
4
|
+
// definition an instance runs on *both* `createInstance` and `searchProcessInstances`. Each used to
|
|
5
|
+
// carry its own hand-mirrored `pickProcessDefinitionIdentity` (plus a re-declared identity type), so
|
|
6
|
+
// the two could silently diverge the moment identity validation changed — exactly the drift class
|
|
7
|
+
// the shared form-contract normalizer exists to kill (`./form-contract.ts`, issue #252). Centralizing
|
|
8
|
+
// the mapper (and reusing the canonical {@link ProcessDefinitionIdentity}) here removes that surface:
|
|
9
|
+
// a created instance and its later snapshot describe the same definition identically wherever this
|
|
10
|
+
// normalizer runs, because they run the *same* code.
|
|
11
|
+
//
|
|
12
|
+
// Ownership today: only the SDK adapter runs this implementation. `WasmEngineClient` still defines
|
|
13
|
+
// and calls its own behaviourally identical copy (`urban-testkit/src/wasm-engine.ts`) — a deliberate,
|
|
14
|
+
// temporary fork: this module is introduced by the same change, so it exists in no published urban
|
|
15
|
+
// release, and a value import would force the testkit's peer floor up to an urban version that does
|
|
16
|
+
// not yet exist (AGENTS.md: depend on an unreleased sibling only after it is published). Follow-up
|
|
17
|
+
// #585 switches the WASM adapter to this canonical import once the urban release carrying it is
|
|
18
|
+
// published; until then the testkit's own identity cases pin its copy to this normalizer's behaviour.
|
|
19
|
+
|
|
20
|
+
import type { ProcessDefinitionIdentity } from "./host.ts";
|
|
21
|
+
|
|
22
|
+
export type { ProcessDefinitionIdentity };
|
|
23
|
+
|
|
24
|
+
/** A non-empty string form of an engine key/id, or `undefined` when absent/blank (including a
|
|
25
|
+
* whitespace-only string). Coerces a *finite* numeric key to a string (the engine may serialize a
|
|
26
|
+
* key either way) but never `String(...)`-coerces an arbitrary object into a garbage
|
|
27
|
+
* `"[object Object]"` id, nor a non-finite number into `"NaN"`/`"Infinity"`. The blank check trims,
|
|
28
|
+
* matching the read-path blank-key guards, so a `" "` key can never leak into a result. */
|
|
29
|
+
export function presentEngineKey(value: unknown): string | undefined {
|
|
30
|
+
if (typeof value === "number") {
|
|
31
|
+
return Number.isFinite(value) ? String(value) : undefined;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== "string") return undefined;
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
return trimmed === "" ? undefined : trimmed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A present string-only identifier (e.g. a BPMN process id) under the shared trim rule; a
|
|
39
|
+
* non-string is absent and never `String(...)`-coerced. */
|
|
40
|
+
export function presentString(value: unknown): string | undefined {
|
|
41
|
+
if (typeof value !== "string") return undefined;
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
return trimmed === "" ? undefined : trimmed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The {@link ProcessDefinitionIdentity} an engine row (a create response or a process-instance
|
|
47
|
+
* search row) reports — the single mapping both `createInstance` and `searchProcessInstances` use,
|
|
48
|
+
* so a created instance and its later snapshot describe the same definition identically. Each field
|
|
49
|
+
* passes the shared presence rule; a version that is not a positive integer number is omitted
|
|
50
|
+
* (never coerced). */
|
|
51
|
+
export function pickProcessDefinitionIdentity(row: {
|
|
52
|
+
processDefinitionKey?: unknown;
|
|
53
|
+
processDefinitionId?: unknown;
|
|
54
|
+
processDefinitionVersion?: unknown;
|
|
55
|
+
}): ProcessDefinitionIdentity {
|
|
56
|
+
const processDefinitionKey = presentEngineKey(row.processDefinitionKey);
|
|
57
|
+
const processDefinitionId = presentString(row.processDefinitionId);
|
|
58
|
+
const version = row.processDefinitionVersion;
|
|
59
|
+
const processDefinitionVersion =
|
|
60
|
+
typeof version === "number" && Number.isInteger(version) && version > 0 ? version : undefined;
|
|
61
|
+
return {
|
|
62
|
+
...(processDefinitionKey ? { processDefinitionKey } : {}),
|
|
63
|
+
...(processDefinitionId ? { processDefinitionId } : {}),
|
|
64
|
+
...(processDefinitionVersion !== undefined ? { processDefinitionVersion } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -200,6 +200,43 @@ test("createInstance routes through the SDK and coerces the key", async () => {
|
|
|
200
200
|
assert.ok(client.calls.includes("createProcessInstance"));
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
test("createInstance returns the started definition's identity (key, id, version)", async () => {
|
|
204
|
+
// The create response names exactly which deployed definition version the instance runs; the
|
|
205
|
+
// seam must surface it (normalized through the same presence rule as the search snapshot) so a
|
|
206
|
+
// caller need not race an eventually consistent search to learn it.
|
|
207
|
+
const client = fakeSdkClient({
|
|
208
|
+
createProcessInstance: async () => ({
|
|
209
|
+
processInstanceKey: 99,
|
|
210
|
+
processDefinitionKey: 2251799813685249,
|
|
211
|
+
processDefinitionId: " order ",
|
|
212
|
+
processDefinitionVersion: 3,
|
|
213
|
+
}),
|
|
214
|
+
});
|
|
215
|
+
const engine = new SdkEngineClient(client);
|
|
216
|
+
const res = await engine.createInstance({ processDefinitionId: "order" });
|
|
217
|
+
assert.deepEqual(res, {
|
|
218
|
+
processInstanceKey: "99",
|
|
219
|
+
processDefinitionKey: "2251799813685249",
|
|
220
|
+
processDefinitionId: "order",
|
|
221
|
+
processDefinitionVersion: 3,
|
|
222
|
+
variables: undefined,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("createInstance omits definition identity fields the engine leaves blank or malformed", async () => {
|
|
227
|
+
const client = fakeSdkClient({
|
|
228
|
+
createProcessInstance: async () => ({
|
|
229
|
+
processInstanceKey: "99",
|
|
230
|
+
processDefinitionKey: " ",
|
|
231
|
+
processDefinitionId: "",
|
|
232
|
+
processDefinitionVersion: "3", // not a number → omitted, never coerced
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
const engine = new SdkEngineClient(client);
|
|
236
|
+
const res = await engine.createInstance({ processDefinitionId: "order" });
|
|
237
|
+
assert.deepEqual(res, { processInstanceKey: "99", variables: undefined });
|
|
238
|
+
});
|
|
239
|
+
|
|
203
240
|
test("createInstance throws when the SDK response omits the instance key", async () => {
|
|
204
241
|
const client = fakeSdkClient({
|
|
205
242
|
createProcessInstance: async () => ({ variables: { ok: true } }),
|
|
@@ -622,6 +659,51 @@ test("searchProcessInstances surfaces the parent/root linkage keys and drops bla
|
|
|
622
659
|
]);
|
|
623
660
|
});
|
|
624
661
|
|
|
662
|
+
test("searchProcessInstances forwards a processDefinitionId selector (trimmed, blank dropped)", async () => {
|
|
663
|
+
const seen: unknown[] = [];
|
|
664
|
+
const client = fakeSdkClient({
|
|
665
|
+
searchProcessInstances: async (input) => {
|
|
666
|
+
seen.push(input);
|
|
667
|
+
return { items: [] };
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
const engine = new SdkEngineClient(client);
|
|
671
|
+
await engine.searchProcessInstances({ processDefinitionId: " order ", state: "ACTIVE" });
|
|
672
|
+
await engine.searchProcessInstances({ processDefinitionId: " " });
|
|
673
|
+
assert.deepEqual(seen, [
|
|
674
|
+
{ filter: { state: "ACTIVE", processDefinitionId: "order" } },
|
|
675
|
+
{ filter: {} },
|
|
676
|
+
]);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("searchProcessInstances surfaces each instance's definition identity", async () => {
|
|
680
|
+
const client = fakeSdkClient({
|
|
681
|
+
searchProcessInstances: async () => ({
|
|
682
|
+
items: [
|
|
683
|
+
{
|
|
684
|
+
processInstanceKey: 10,
|
|
685
|
+
state: "ACTIVE",
|
|
686
|
+
processDefinitionKey: 5,
|
|
687
|
+
processDefinitionId: "order",
|
|
688
|
+
processDefinitionVersion: 2,
|
|
689
|
+
},
|
|
690
|
+
{ processInstanceKey: 11, state: "ACTIVE", processDefinitionId: " ", processDefinitionVersion: null },
|
|
691
|
+
],
|
|
692
|
+
}),
|
|
693
|
+
});
|
|
694
|
+
const engine = new SdkEngineClient(client);
|
|
695
|
+
assert.deepEqual(await engine.searchProcessInstances(), [
|
|
696
|
+
{
|
|
697
|
+
processInstanceKey: "10",
|
|
698
|
+
state: "ACTIVE",
|
|
699
|
+
processDefinitionKey: "5",
|
|
700
|
+
processDefinitionId: "order",
|
|
701
|
+
processDefinitionVersion: 2,
|
|
702
|
+
},
|
|
703
|
+
{ processInstanceKey: "11", state: "ACTIVE" },
|
|
704
|
+
]);
|
|
705
|
+
});
|
|
706
|
+
|
|
625
707
|
test("normalizeProcessInstanceState maps the terminal set and rejects others", () => {
|
|
626
708
|
assert.equal(normalizeProcessInstanceState("ACTIVE"), "ACTIVE");
|
|
627
709
|
assert.equal(normalizeProcessInstanceState("completed"), "COMPLETED");
|
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
JobFilter,
|
|
33
33
|
JobHandler,
|
|
34
34
|
JobSummary,
|
|
35
|
+
CreatedProcessInstance,
|
|
35
36
|
ProcessInstanceSnapshot,
|
|
36
37
|
ProcessInstanceState,
|
|
37
38
|
UserTaskState,
|
|
@@ -49,6 +50,11 @@ import type {
|
|
|
49
50
|
TranscriptTurnRole,
|
|
50
51
|
} from "@nanobpm/agentic/transcript";
|
|
51
52
|
import { assertDeployedWaitStateType, isBpmnError } from "../core/host.ts";
|
|
53
|
+
import {
|
|
54
|
+
pickProcessDefinitionIdentity,
|
|
55
|
+
presentEngineKey,
|
|
56
|
+
presentString,
|
|
57
|
+
} from "../core/process-identity.ts";
|
|
52
58
|
import {
|
|
53
59
|
buildFormSchema,
|
|
54
60
|
parseFormSchema,
|
|
@@ -153,19 +159,11 @@ export function normalizeIncidentState(raw: unknown): IncidentState {
|
|
|
153
159
|
}
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
*
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
function presentEngineKey(value: unknown): string | undefined {
|
|
162
|
-
if (typeof value === "number") {
|
|
163
|
-
return Number.isFinite(value) ? String(value) : undefined;
|
|
164
|
-
}
|
|
165
|
-
if (typeof value !== "string") return undefined;
|
|
166
|
-
const trimmed = value.trim();
|
|
167
|
-
return trimmed === "" ? undefined : trimmed;
|
|
168
|
-
}
|
|
162
|
+
// Engine-key / process-definition-identity normalization now lives in the shared, adapter-agnostic
|
|
163
|
+
// `../core/process-identity.ts` (imported above), so this adapter's `createInstance` and
|
|
164
|
+
// `searchProcessInstances` run the *same* mapping — see that module's header for the drift class
|
|
165
|
+
// this removes. The WASM test adapter keeps a mirrored local copy until #585 (that module header's
|
|
166
|
+
// "Ownership today" note), so the shared mapping is not yet a cross-adapter guarantee.
|
|
169
167
|
|
|
170
168
|
/** The non-empty, trimmed string form of a *required* engine key, or throws when the value is
|
|
171
169
|
* absent/blank (including a whitespace-only string). A mutating seam operation addresses a
|
|
@@ -858,7 +856,7 @@ export class SdkEngineClient implements EngineClient {
|
|
|
858
856
|
processDefinitionId: string;
|
|
859
857
|
variables?: Record<string, unknown>;
|
|
860
858
|
awaitCompletion?: boolean;
|
|
861
|
-
}): Promise<
|
|
859
|
+
}): Promise<CreatedProcessInstance> {
|
|
862
860
|
// Auto-thread the `_urban.lineage` envelope (issue #254): a handler that spawns an
|
|
863
861
|
// instance propagates its `rootRequestKey` and stamps `causedByInstanceKey`; a genuine
|
|
864
862
|
// top-level request mints a fresh root. An explicit caller-supplied envelope wins.
|
|
@@ -873,6 +871,7 @@ export class SdkEngineClient implements EngineClient {
|
|
|
873
871
|
const variables = isRecord(body.variables) ? body.variables : undefined;
|
|
874
872
|
return {
|
|
875
873
|
processInstanceKey: requireProcessInstanceKey(key),
|
|
874
|
+
...pickProcessDefinitionIdentity(body),
|
|
876
875
|
variables,
|
|
877
876
|
};
|
|
878
877
|
}
|
|
@@ -1003,11 +1002,15 @@ export class SdkEngineClient implements EngineClient {
|
|
|
1003
1002
|
async searchProcessInstances(filter?: {
|
|
1004
1003
|
processInstanceKeys?: string[];
|
|
1005
1004
|
state?: ProcessInstanceState;
|
|
1005
|
+
processDefinitionId?: string;
|
|
1006
1006
|
parentProcessInstanceKey?: string;
|
|
1007
1007
|
rootProcessInstanceKey?: string;
|
|
1008
1008
|
}): Promise<ProcessInstanceSnapshot[]> {
|
|
1009
1009
|
const f: Record<string, unknown> = {};
|
|
1010
1010
|
if (filter?.state) f.state = filter.state;
|
|
1011
|
+
// A blank BPMN process-id selector is absent, not an empty filter that matches nothing.
|
|
1012
|
+
const processDefinitionId = presentString(filter?.processDefinitionId);
|
|
1013
|
+
if (processDefinitionId) f.processDefinitionId = processDefinitionId;
|
|
1011
1014
|
// Normalize each requested key through the shared presence rule (drop blank/whitespace-only
|
|
1012
1015
|
// entries, trim padded ones like `" 123 "`) so the `$in` filter carries only present keys that
|
|
1013
1016
|
// match the normalized keys we surface — mirroring `WasmEngineClient` and the selector
|
|
@@ -1051,13 +1054,12 @@ export class SdkEngineClient implements EngineClient {
|
|
|
1051
1054
|
});
|
|
1052
1055
|
return [];
|
|
1053
1056
|
}
|
|
1054
|
-
const processDefinitionKey = presentEngineKey(it.processDefinitionKey);
|
|
1055
1057
|
const parentProcessInstanceKey = presentEngineKey(it.parentProcessInstanceKey);
|
|
1056
1058
|
const rootProcessInstanceKey = presentEngineKey(it.rootProcessInstanceKey);
|
|
1057
1059
|
return [{
|
|
1058
1060
|
processInstanceKey,
|
|
1059
1061
|
state,
|
|
1060
|
-
...(
|
|
1062
|
+
...pickProcessDefinitionIdentity(it),
|
|
1061
1063
|
...(parentProcessInstanceKey ? { parentProcessInstanceKey } : {}),
|
|
1062
1064
|
...(rootProcessInstanceKey ? { rootProcessInstanceKey } : {}),
|
|
1063
1065
|
}];
|
package/src/runtime/index.ts
CHANGED
|
@@ -108,6 +108,8 @@ export type {
|
|
|
108
108
|
JobFilter,
|
|
109
109
|
JobHandler,
|
|
110
110
|
JobSummary,
|
|
111
|
+
CreatedProcessInstance,
|
|
112
|
+
ProcessDefinitionIdentity,
|
|
111
113
|
ProcessInstanceSnapshot,
|
|
112
114
|
ProcessInstanceState,
|
|
113
115
|
SqliteDb,
|
|
@@ -140,6 +142,15 @@ export {
|
|
|
140
142
|
presentFormIdentifier,
|
|
141
143
|
resolveFormIdentifier,
|
|
142
144
|
} from "./core/form-contract.ts";
|
|
145
|
+
// The adapter-agnostic process-definition-identity normalizer (the single mapping both
|
|
146
|
+
// `createInstance` and `searchProcessInstances` use), owned today by the live SDK adapter; the WASM
|
|
147
|
+
// test adapter keeps a mirrored local copy until #585, so cross-adapter drift-freedom is not yet
|
|
148
|
+
// claimed. `ProcessDefinitionIdentity` itself is re-exported from `./core/host.ts`.
|
|
149
|
+
export {
|
|
150
|
+
pickProcessDefinitionIdentity,
|
|
151
|
+
presentEngineKey,
|
|
152
|
+
presentString,
|
|
153
|
+
} from "./core/process-identity.ts";
|
|
143
154
|
export type { AppApi, Mounted, RuntimeContext } from "./core/context.ts";
|
|
144
155
|
// Structured logging surface (see core/logger.ts): the shape of `AppApi.log`, plus `createLogger`
|
|
145
156
|
// so consumers can build a Logger for a custom sink or a no-op test double (`createLogger(() => {})`).
|