/** * Parse `taskDefinition` leaves out of a deployed BPMN model. * * The demand side of the demand×supply model (S4) is the set of routing tokens * the deployed processes ask for. In a Nano/Camunda-8 model each demand is a * service task's `` — the job type the engine * matches 1:1 against a worker's routing token. This scanner reads those leaves * out of the raw BPMN XML so the model can bucket them by network prefix and * diff them against live supply. * * It is deliberately a small, dependency-free regex scan over the same surface * the Urban toolkit's worker-io deriver reads, so it stays app-tier and never * touches the engine internals — the XML arrives from the C8 REST API * ({@link ./c8-rest.ts}). */ /** One deployed `taskDefinition` leaf: a job type demanded by a model element. */ export interface TaskDefinitionLeaf { /** The `zeebe:taskDefinition` `type` — the routing token the engine matches. */ readonly taskType: string; /** The `bpmn:process` id the leaf was declared in (best-effort, may be empty). */ readonly process: string; /** The service-task element id carrying the leaf (best-effort, may be empty). */ readonly elementId: string; /** * True when the service task carries a `` * — its base-prompt side-car. This is the *structural* agentic signal (per nwf * `SPEC.md` §"Agent job contract"): every external agent task delivers its base * prompt through a `linkName="prompt"` linked resource, and no in-process * (deterministic) worker task does. Agentic-ness is read from this flag, NOT * from the `taskType` string — the deployed models use arbitrary type strings * (colon-form `senior:retro`, dot-form, bare) that a routing-token grammar * would mis-classify. */ readonly agentic: boolean; } function attr(tag: string, name: string): string { const match = tag.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`)); return match ? match[1] : ""; } function processId(xml: string): string { const match = xml.match(/]*\bid\s*=\s*"([^"]*)"/); return match ? match[1] : ""; } /** * True when a service-task body carries a `linkName="prompt"` linked resource — * the base-prompt side-car that marks a task as external agentic work. * * Scans every `` in the body (tolerating attribute * ordering and self-closing tags, consistent with {@link scanTaskDefinitions}) * and returns true as soon as one carries `linkName="prompt"`. */ function hasPromptLink(body: string): boolean { const linkRe = /]*?\/?>/g; let link: RegExpExecArray | null; while ((link = linkRe.exec(body)) !== null) { if (attr(link[0], "linkName") === "prompt") return true; } return false; } /** * Scan one BPMN document for its service-task `taskDefinition` leaves. * * Every `` carrying a `` with a * non-empty type yields a leaf; tasks without a task definition (or with an empty * type) are skipped. The scan is order-preserving and tolerant of attribute * ordering and self-closing task-definition tags. Each leaf also records whether * the task carries a `linkName="prompt"` linked resource (its `agentic` flag). */ export function scanTaskDefinitions(xml: string): TaskDefinitionLeaf[] { const proc = processId(xml); const out: TaskDefinitionLeaf[] = []; const blockRe = /]*)>([\s\S]*?)<\/bpmn:serviceTask>/g; let block: RegExpExecArray | null; while ((block = blockRe.exec(xml)) !== null) { const openAttrs = block[1]; const body = block[2]; const elementId = attr(``, "id"); const tdMatch = body.match(/]*>/); if (!tdMatch) continue; const taskType = attr(tdMatch[0], "type"); if (!taskType) continue; out.push({ taskType, elementId, process: proc, agentic: hasPromptLink(body) }); } return out; } /** * Scan many BPMN documents and return the distinct demanded job types, in first * occurrence order. The demand×supply model only needs the distinct set of * routing tokens; {@link scanTaskDefinitions} keeps the full leaves for callers * that want element provenance. */ export function distinctTaskTypes(leaves: readonly TaskDefinitionLeaf[]): string[] { const seen = new Set(); const out: string[] = []; for (const leaf of leaves) { if (seen.has(leaf.taskType)) continue; seen.add(leaf.taskType); out.push(leaf.taskType); } return out; }