@nanobpm/urban 0.50.0 → 0.52.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/dist/runtime/core/execContext.d.ts +5 -0
- package/dist/runtime/core/lineage.d.ts +116 -0
- package/dist/runtime/core/lineage.js +186 -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/lineage-store.d.ts +81 -0
- package/dist/runtime/core/modules/lineage-store.js +175 -0
- package/dist/runtime/core/modules/pages.js +165 -33
- package/dist/runtime/core/modules/workers.js +69 -2
- package/dist/runtime/engine/nanosdk.js +8 -2
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +5 -0
- package/package.json +1 -1
- package/src/runtime/core/execContext.ts +5 -0
- package/src/runtime/core/lineage.test.ts +215 -0
- package/src/runtime/core/lineage.ts +297 -0
- package/src/runtime/core/modules/datasource.ts +9 -0
- 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 +85 -0
- package/src/runtime/core/modules/pages.ts +165 -33
- package/src/runtime/core/modules/workers.test.ts +136 -1
- package/src/runtime/core/modules/workers.ts +72 -6
- package/src/runtime/engine/nanosdk.test.ts +97 -3
- package/src/runtime/engine/nanosdk.ts +8 -2
- package/src/runtime/index.ts +35 -0
|
@@ -498,6 +498,16 @@ table.pc-grid { width:100%; border-collapse:collapse; font-size:.9rem; table-lay
|
|
|
498
498
|
neighbours. */
|
|
499
499
|
table.pc-grid th, table.pc-grid td { text-align:left; padding:.4rem .6rem; border-bottom:1px solid var(--nano-edge); overflow-wrap:anywhere; word-wrap:break-word; vertical-align:top; }
|
|
500
500
|
table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
|
|
501
|
+
/* A cell's optional muted second line (col.subtitleField / subtitleTemplate):
|
|
502
|
+
the identity key rendered under the title, GitHub/Linear style. Plain DOM
|
|
503
|
+
text (never a link/href source), smaller and muted so the primary line leads. */
|
|
504
|
+
.pc-cell-sub { font-size:.8rem; color:var(--nano-text-muted); margin-top:.1rem; }
|
|
505
|
+
/* One-line clamp (col.truncate): stop a bounded column wrapping and show an
|
|
506
|
+
ellipsis, with the full value carried in a title=/aria-label tooltip. Needs a
|
|
507
|
+
bounded column (per-column width/weight) to clip against; a native <col> width
|
|
508
|
+
is honoured under table-layout:fixed. Applied to the primary and, when present,
|
|
509
|
+
the subtitle line so both stay single-line. */
|
|
510
|
+
.pc-truncate { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
501
511
|
.pc-tabs { display:flex; gap:.5rem; margin-bottom:.75rem; }
|
|
502
512
|
.pc-tab { padding:.35rem .8rem; border:1px solid var(--nano-edge); border-radius:.4rem; background:transparent; color:inherit; font:inherit; cursor:pointer; }
|
|
503
513
|
.pc-tab.active { background:var(--nano-accent); color:var(--nano-on-accent); border-color:var(--nano-accent); }
|
|
@@ -987,6 +997,15 @@ function renderButton(node) {
|
|
|
987
997
|
// tooltip stay keyed to the raw 'field' value (a badge shows a fixed glyph, not
|
|
988
998
|
// the field text), so a template does not change badge behaviour. Sorting,
|
|
989
999
|
// groupBy and orderBy also key off real fields, never the rendered template.
|
|
1000
|
+
//
|
|
1001
|
+
// Two-line identity + one-line clamp (additive, opt-in, backward-compatible):
|
|
1002
|
+
// a column may also declare 'subtitleField'/'subtitleTemplate' to render a muted
|
|
1003
|
+
// second line under the primary text (the identity key beneath a title, GitHub/
|
|
1004
|
+
// Linear style) and 'truncate: true' to clamp each line to one row with an
|
|
1005
|
+
// ellipsis and a full-text title/aria-label tooltip (pairs with a per-column
|
|
1006
|
+
// width/weight — see buildColgroup — to give the clip a bounded column). The
|
|
1007
|
+
// subtitle is ALWAYS plain DOM text, never an href source. With none of these
|
|
1008
|
+
// set the cell is a single inline node exactly as before (see cellTd).
|
|
990
1009
|
function gridCell(col, row) {
|
|
991
1010
|
// Raw single-field value: drives the badge presence gate + tooltip and any
|
|
992
1011
|
// field-derived defaults. Kept separate from the (possibly templated) display
|
|
@@ -1016,17 +1035,28 @@ function gridCell(col, row) {
|
|
|
1016
1035
|
el("span", { class: "pc-badge pc-badge-" + tone, title: rawText, "aria-label": rawText }, label),
|
|
1017
1036
|
);
|
|
1018
1037
|
}
|
|
1038
|
+
// Optional second line (col.subtitleField / subtitleTemplate): a muted key
|
|
1039
|
+
// under the primary title. subtitleTemplate wins (interpolated from the row);
|
|
1040
|
+
// else the raw subtitleField value. Absent/null → "" → no extra line. It is
|
|
1041
|
+
// ALWAYS plain DOM text (see cellTd), never a link/href source — a subtitle
|
|
1042
|
+
// cannot smuggle a scheme or path, only linkField/keyField drive hrefs.
|
|
1043
|
+
const subRaw =
|
|
1044
|
+
col.subtitleField != null && row[col.subtitleField] != null ? String(row[col.subtitleField]) : "";
|
|
1045
|
+
const subText = typeof col.subtitleTemplate === "string" ? interpTemplate(col.subtitleTemplate, row) : subRaw;
|
|
1046
|
+
const truncate = col.truncate === true;
|
|
1047
|
+
// Primary inline content: an anchor for a valid link mode (checked in the same
|
|
1048
|
+
// badge < linkField < link(page) < link(processExplorer) precedence as before),
|
|
1049
|
+
// else the plain display text. Each link mode only claims the cell while
|
|
1050
|
+
// 'primary' is still the bare text — an invalid href falls through to the next
|
|
1051
|
+
// mode and ultimately to plain text, exactly as the early-return chain did.
|
|
1052
|
+
let primary = text;
|
|
1019
1053
|
if (col.linkField) {
|
|
1020
1054
|
const href = row[col.linkField] == null ? "" : String(row[col.linkField]);
|
|
1021
1055
|
if (text !== "" && /^https?:\/\//i.test(href)) {
|
|
1022
|
-
|
|
1023
|
-
"td",
|
|
1024
|
-
{},
|
|
1025
|
-
el("a", { class: "pc-link", href, target: "_blank", rel: "noopener noreferrer" }, text),
|
|
1026
|
-
);
|
|
1056
|
+
primary = el("a", { class: "pc-link", href, target: "_blank", rel: "noopener noreferrer" }, text);
|
|
1027
1057
|
}
|
|
1028
1058
|
}
|
|
1029
|
-
if (col.link && col.link.kind === "page" && col.link.page && col.link.keyField) {
|
|
1059
|
+
if (primary === text && col.link && col.link.kind === "page" && col.link.page && col.link.keyField) {
|
|
1030
1060
|
// An in-app link to another page, scoped to this row: #/<page>/<key>. The
|
|
1031
1061
|
// key (e.g. a plan_key "owner/repo#123") is URL-encoded into the route's
|
|
1032
1062
|
// param tail, which parseRoute() decodes back verbatim; the page id is
|
|
@@ -1037,37 +1067,130 @@ function gridCell(col, row) {
|
|
|
1037
1067
|
const keyStr = key == null ? "" : String(key).trim();
|
|
1038
1068
|
if (text !== "" && safePageId(col.link.page) && keyStr !== "") {
|
|
1039
1069
|
const href = "#/" + encodeURIComponent(col.link.page) + "/" + encodeURIComponent(keyStr);
|
|
1040
|
-
|
|
1070
|
+
primary = el("a", { class: "pc-link", href }, text);
|
|
1041
1071
|
}
|
|
1042
1072
|
}
|
|
1043
|
-
if (col.link && col.link.kind === "processExplorer" && col.link.keyField) {
|
|
1073
|
+
if (primary === text && col.link && col.link.kind === "processExplorer" && col.link.keyField) {
|
|
1044
1074
|
const key = row[col.link.keyField];
|
|
1045
1075
|
const keyStr = key == null ? "" : String(key).trim();
|
|
1046
1076
|
if (text !== "" && keyStr !== "") {
|
|
1047
1077
|
const href = "/console/explorer?instance=" + encodeURIComponent(keyStr);
|
|
1048
|
-
|
|
1049
|
-
"
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
if (hostNavigate("processExplorer", { instance: keyStr })) ev.preventDefault();
|
|
1065
|
-
},
|
|
1066
|
-
}, text),
|
|
1067
|
-
);
|
|
1078
|
+
primary = el("a", {
|
|
1079
|
+
class: "pc-link",
|
|
1080
|
+
href,
|
|
1081
|
+
target: "_blank",
|
|
1082
|
+
rel: "noopener noreferrer",
|
|
1083
|
+
// Embedded in the console a plain primary click routes the host's
|
|
1084
|
+
// explorer in place (via the nano-navigate bridge) instead of opening
|
|
1085
|
+
// a new window. A non-primary button (middle/right = new-tab/context
|
|
1086
|
+
// intent), a modified click (⌘/ctrl/shift/alt = new-tab intent) and
|
|
1087
|
+
// any standalone run fall through to the native anchor above.
|
|
1088
|
+
onclick: (ev) => {
|
|
1089
|
+
if (ev.button !== 0) return;
|
|
1090
|
+
if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
|
|
1091
|
+
if (hostNavigate("processExplorer", { instance: keyStr })) ev.preventDefault();
|
|
1092
|
+
},
|
|
1093
|
+
}, text);
|
|
1068
1094
|
}
|
|
1069
1095
|
}
|
|
1070
|
-
return
|
|
1096
|
+
return cellTd(primary, text, subText, truncate);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Compose a grid <td> from its primary inline content (a string or an anchor),
|
|
1100
|
+
// an optional muted subtitle line, and optional one-line truncation. With
|
|
1101
|
+
// NEITHER a subtitle nor truncation the cell is exactly the single inline node
|
|
1102
|
+
// as before, so existing grids render byte-for-byte unchanged (backward
|
|
1103
|
+
// compatible). When either is set the content is wrapped in a .pc-cell-main
|
|
1104
|
+
// line and, if a subtitle is present, a .pc-cell-sub line beneath it. truncate
|
|
1105
|
+
// adds .pc-truncate (nowrap + ellipsis) to each line and mirrors the full text
|
|
1106
|
+
// into title=/aria-label so the clipped value is still recoverable on hover and
|
|
1107
|
+
// to assistive tech. When the primary is an element (a link cell's <a>), title=/
|
|
1108
|
+
// aria-label= are ALSO mirrored onto it: HTML does not inherit those to
|
|
1109
|
+
// descendants, so hovering the link text or naming the anchor for assistive tech
|
|
1110
|
+
// would otherwise miss the full value the wrapper carries. The subtitle is set as
|
|
1111
|
+
// DOM text only — never an href.
|
|
1112
|
+
function cellTd(primary, primaryText, subText, truncate) {
|
|
1113
|
+
if (subText === "" && !truncate) return el("td", {}, primary);
|
|
1114
|
+
const mainAttrs = { class: "pc-cell-main" + (truncate ? " pc-truncate" : "") };
|
|
1115
|
+
if (truncate && primaryText !== "") {
|
|
1116
|
+
mainAttrs.title = primaryText; mainAttrs["aria-label"] = primaryText;
|
|
1117
|
+
if (primary && typeof primary.setAttribute === "function") {
|
|
1118
|
+
primary.setAttribute("title", primaryText); primary.setAttribute("aria-label", primaryText);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
const main = el("div", mainAttrs, primary);
|
|
1122
|
+
if (subText === "") return el("td", {}, main);
|
|
1123
|
+
const subAttrs = { class: "pc-cell-sub" + (truncate ? " pc-truncate" : "") };
|
|
1124
|
+
if (truncate) { subAttrs.title = subText; subAttrs["aria-label"] = subText; }
|
|
1125
|
+
return el("td", {}, main, el("div", subAttrs, subText));
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Parse a "<number>%" width into its numeric percentage, or null if the string
|
|
1129
|
+
// is not a bare percentage (e.g. "22rem", "120px") — used to decide whether the
|
|
1130
|
+
// remainder left for weighted columns can be computed.
|
|
1131
|
+
function colWidthPct(width) {
|
|
1132
|
+
const m = /^\s*([0-9]*\.?[0-9]+)%\s*$/.exec(width);
|
|
1133
|
+
return m ? parseFloat(m[1]) : null;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// A column carries an explicit width only when it is a non-empty string. Single
|
|
1137
|
+
// source of truth so colWidthStyle/buildColgroup can't drift on what "explicit"
|
|
1138
|
+
// means (which columns win verbatim, consume the remainder, or use their weight).
|
|
1139
|
+
function hasExplicitWidth(col) {
|
|
1140
|
+
return typeof col.width === "string" && col.width !== "";
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// A column's <col> width for the grid's <colgroup>: an explicit col.width string
|
|
1144
|
+
// ("40%", "22rem") wins verbatim; else a positive numeric col.weight is
|
|
1145
|
+
// normalised across the weighted columns into a percentage share of the width
|
|
1146
|
+
// left over by explicit columns (remainderPct); else null (no width — the column
|
|
1147
|
+
// shares the remainder equally under table-layout:fixed).
|
|
1148
|
+
function colWidthStyle(col, weightTotal, remainderPct) {
|
|
1149
|
+
if (hasExplicitWidth(col)) return col.width;
|
|
1150
|
+
if (typeof col.weight === "number" && col.weight > 0 && weightTotal > 0) {
|
|
1151
|
+
return Math.round((col.weight / weightTotal) * remainderPct * 100) / 100 + "%";
|
|
1152
|
+
}
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Build a <colgroup> that sizes the grid's columns so table-layout:fixed stops
|
|
1157
|
+
// splitting width equally. Returns null when no column declares width/weight, so
|
|
1158
|
+
// an unsized grid emits no colgroup and lays out exactly as today. 'extraCount'
|
|
1159
|
+
// adds trailing unsized <col>s for the row-actions/detail column so the sized
|
|
1160
|
+
// data columns stay aligned with their headers.
|
|
1161
|
+
function buildColgroup(cols, extraCount) {
|
|
1162
|
+
const hasSizing = cols.some(
|
|
1163
|
+
(c) => hasExplicitWidth(c) || (typeof c.weight === "number" && c.weight > 0),
|
|
1164
|
+
);
|
|
1165
|
+
if (!hasSizing) return null;
|
|
1166
|
+
// Only columns that will actually use their weight (no explicit width — an
|
|
1167
|
+
// explicit width wins verbatim in colWidthStyle and never consumes weight)
|
|
1168
|
+
// contribute to weightTotal; counting a width-bearing column's weight here
|
|
1169
|
+
// would inflate the divisor and shrink the truly-weighted columns' share.
|
|
1170
|
+
let weightTotal = 0;
|
|
1171
|
+
for (const c of cols)
|
|
1172
|
+
if (!hasExplicitWidth(c) && typeof c.weight === "number" && c.weight > 0) weightTotal += c.weight;
|
|
1173
|
+
// Weighted columns share only the width the explicit columns leave behind, so
|
|
1174
|
+
// mixing e.g. width:"40%" with weighted columns can't oversubscribe past 100%.
|
|
1175
|
+
// The remainder is only computable when every explicit width is a percentage;
|
|
1176
|
+
// if any uses an absolute/relative unit (rem/px) we can't subtract it from
|
|
1177
|
+
// 100%, so weighted columns fall back to a share of the full width.
|
|
1178
|
+
let explicitPctTotal = 0;
|
|
1179
|
+
let allExplicitArePct = true;
|
|
1180
|
+
for (const c of cols) {
|
|
1181
|
+
if (hasExplicitWidth(c)) {
|
|
1182
|
+
const pct = colWidthPct(c.width);
|
|
1183
|
+
if (pct === null) allExplicitArePct = false;
|
|
1184
|
+
else explicitPctTotal += pct;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
const remainderPct = allExplicitArePct ? Math.max(0, 100 - explicitPctTotal) : 100;
|
|
1188
|
+
const colEls = cols.map((c) => {
|
|
1189
|
+
const w = colWidthStyle(c, weightTotal, remainderPct);
|
|
1190
|
+
return el("col", w ? { style: "width:" + w } : {});
|
|
1191
|
+
});
|
|
1192
|
+
for (let i = 0; i < extraCount; i++) colEls.push(el("col", {}));
|
|
1193
|
+
return el("colgroup", {}, ...colEls);
|
|
1071
1194
|
}
|
|
1072
1195
|
|
|
1073
1196
|
function renderActionForm(node) {
|
|
@@ -1263,7 +1386,13 @@ function renderDataGrid(node) {
|
|
|
1263
1386
|
if (hasExtra) headCells.push(el("th", {}, ""));
|
|
1264
1387
|
const thead = el("thead", {}, el("tr", {}, ...headCells));
|
|
1265
1388
|
const tbody = el("tbody", {});
|
|
1266
|
-
|
|
1389
|
+
// A <colgroup> (present only when a column declares width/weight) sizes the
|
|
1390
|
+
// columns so table-layout:fixed stops splitting width equally; it must precede
|
|
1391
|
+
// <thead> in the table.
|
|
1392
|
+
const colgroup = buildColgroup(cols, hasExtra ? 1 : 0);
|
|
1393
|
+
const table = colgroup
|
|
1394
|
+
? el("table", { class: "pc-grid" }, colgroup, thead, tbody)
|
|
1395
|
+
: el("table", { class: "pc-grid" }, thead, tbody);
|
|
1267
1396
|
card.append(table);
|
|
1268
1397
|
const span = String((cols.length || 1) + (hasExtra ? 1 : 0));
|
|
1269
1398
|
|
|
@@ -1355,9 +1484,12 @@ function renderDataGrid(node) {
|
|
|
1355
1484
|
if (cg.title) wrap.append(el("div", { class: "pc-child-title" }, cg.title));
|
|
1356
1485
|
const ccols = cg.columns || [];
|
|
1357
1486
|
const cbody = el("tbody", {});
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1487
|
+
const ccolgroup = buildColgroup(ccols, cg.lazyField ? 1 : 0);
|
|
1488
|
+
const cthead = el("thead", {}, el("tr", {}, ...ccols.map((c) => el("th", {}, c.header || c.field)),
|
|
1489
|
+
...(cg.lazyField ? [el("th", {}, "")] : [])));
|
|
1490
|
+
const ctable = ccolgroup
|
|
1491
|
+
? el("table", { class: "pc-grid" }, ccolgroup, cthead, cbody)
|
|
1492
|
+
: el("table", { class: "pc-grid" }, cthead, cbody);
|
|
1361
1493
|
wrap.append(ctable);
|
|
1362
1494
|
try {
|
|
1363
1495
|
const { rows } = await getJSON(dataUrl(cg.source || "app", cg.table,
|
|
@@ -5,6 +5,8 @@ import { isRecord } from "../guards.js";
|
|
|
5
5
|
import { workerJobType } from "../manifest.js";
|
|
6
6
|
import { runLlmJob } from "./llm.js";
|
|
7
7
|
import { runInJobContext } from "../execContext.js";
|
|
8
|
+
import { readLineage } from "../lineage.js";
|
|
9
|
+
import { LineageStore } from "./lineage-store.js";
|
|
8
10
|
/** Adapt an engine SDK client into the LLM module's `DecisionEvaluator`, parsing the
|
|
9
11
|
* decision's JSON-string `output` into the value fed back to the LLM job. Validates the
|
|
10
12
|
* SDK shape and adds decision context to a parse failure rather than surfacing a raw
|
|
@@ -48,6 +50,59 @@ function withJobLog(app, job) {
|
|
|
48
50
|
bindings.elementId = job.elementId;
|
|
49
51
|
return { ...app, log: app.log.child(bindings) };
|
|
50
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the ambient {@link JobExecContext} a worker runs its handler within. Beyond the
|
|
55
|
+
* instance/element/jobType used for write-provenance, it resolves the lineage
|
|
56
|
+
* `rootRequestKey` (issue #254): the job's own `_urban.lineage` envelope root when present,
|
|
57
|
+
* else the instance's own key — so an instance with no envelope is treated as a root and any
|
|
58
|
+
* instance/message its handler spawns inherits that root with `causedByInstanceKey` set.
|
|
59
|
+
*/
|
|
60
|
+
function jobExecContext(job, jobType) {
|
|
61
|
+
return {
|
|
62
|
+
instanceKey: job.processInstanceKey,
|
|
63
|
+
elementId: job.elementId,
|
|
64
|
+
jobType,
|
|
65
|
+
rootRequestKey: readLineage(job.variables)?.rootRequestKey ?? job.processInstanceKey,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build the framework lineage projection store over the app's DEFAULT data source, or `undefined`
|
|
70
|
+
* when the app configures none. The store records the `_urban.lineage` edge every activated job
|
|
71
|
+
* reveals (issue #254), so intent→progress lineage materialises for free — but it is a sidecar on
|
|
72
|
+
* the app's own source (like write-provenance), so an app with no datasource simply records
|
|
73
|
+
* nothing. Absent- and error-safe: any failure to provision degrades to no lineage recording, never
|
|
74
|
+
* a failed worker mount.
|
|
75
|
+
*/
|
|
76
|
+
function tryLineageStore(app, log) {
|
|
77
|
+
if (!app.data.hasDefaultSource()) {
|
|
78
|
+
// No default data source configured at all — lineage is a sidecar on the app's own source, so
|
|
79
|
+
// this is the expected absent case (like write-provenance): record nothing, silently.
|
|
80
|
+
log("debug", "lineage: no default data source; lineage recording disabled");
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const source = app.data.source();
|
|
85
|
+
const store = new LineageStore(source.db);
|
|
86
|
+
store.ensureSchema();
|
|
87
|
+
return store;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// A default IS configured but resolving or provisioning it failed — a missing named source
|
|
91
|
+
// (`no such data source`) or a schema/DB error. That is a genuine fault, not the absent case,
|
|
92
|
+
// so surface it at `warn` rather than disguising it as "no source".
|
|
93
|
+
log("warn", "lineage: failed to provision projection store; lineage recording disabled", { error: String(err) });
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Record an activated job's lineage edge, never letting a projection write break the job. */
|
|
98
|
+
function recordJobLineage(store, job, log) {
|
|
99
|
+
try {
|
|
100
|
+
store.recordFromJob(job);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
log("warn", "lineage: failed to record job edge", { jobType: job.jobType, error: String(err) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
51
106
|
/**
|
|
52
107
|
* Resolve a handler for `jobType` from a loaded module, in priority order:
|
|
53
108
|
* 1. `handlers[jobType]` (a map keyed by job type — the multi-type module case)
|
|
@@ -74,6 +129,10 @@ export async function mountWorkers(ctx, app) {
|
|
|
74
129
|
const decls = ctx.manifest.workers ?? [];
|
|
75
130
|
const subs = [];
|
|
76
131
|
const jobTypes = [];
|
|
132
|
+
// The framework lineage projection (issue #254): each activated job records the `_urban.lineage`
|
|
133
|
+
// edge its instance carries, so intent→progress lineage materialises for free. Absent when the
|
|
134
|
+
// app configures no default data source.
|
|
135
|
+
const lineageStore = tryLineageStore(app, ctx.host.log);
|
|
77
136
|
// Cache module loads so a multi-type handler module is imported once.
|
|
78
137
|
const moduleCache = new Map();
|
|
79
138
|
const joinRoot = (p) => p.startsWith("/") ? p : `${ctx.root.replace(/\/+$/, "")}/${p.replace(/^\/+/, "")}`;
|
|
@@ -114,7 +173,11 @@ export async function mountWorkers(ctx, app) {
|
|
|
114
173
|
env: (n) => app.env(n),
|
|
115
174
|
evaluateDecision: app.sdk ? sdkDecisionEvaluator(app.sdk) : undefined,
|
|
116
175
|
};
|
|
117
|
-
const wrapped = (job) =>
|
|
176
|
+
const wrapped = (job) => {
|
|
177
|
+
if (lineageStore)
|
|
178
|
+
recordJobLineage(lineageStore, job, ctx.host.log);
|
|
179
|
+
return runInJobContext(jobExecContext(job, jobType), () => runLlmJob(job.variables, binding, rt));
|
|
180
|
+
};
|
|
118
181
|
const sub = await ctx.engine.registerWorker(jobType, wrapped, {
|
|
119
182
|
workerName: `${ctx.manifest.id}:${jobType}`,
|
|
120
183
|
});
|
|
@@ -129,7 +192,11 @@ export async function mountWorkers(ctx, app) {
|
|
|
129
192
|
throw new Error(`worker "${jobType}": ${decl.handler} exports no handler for it ` +
|
|
130
193
|
`(expected handlers["${jobType}"], a named export, or a default function)`);
|
|
131
194
|
}
|
|
132
|
-
const wrapped = (job) =>
|
|
195
|
+
const wrapped = (job) => {
|
|
196
|
+
if (lineageStore)
|
|
197
|
+
recordJobLineage(lineageStore, job, ctx.host.log);
|
|
198
|
+
return runInJobContext(jobExecContext(job, jobType), () => handler(job, withJobLog(app, job)));
|
|
199
|
+
};
|
|
133
200
|
const sub = await ctx.engine.registerWorker(jobType, wrapped, {
|
|
134
201
|
workerName: `${ctx.manifest.id}:${jobType}`,
|
|
135
202
|
});
|
|
@@ -21,6 +21,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
21
21
|
import { isBpmnError } from "../core/host.js";
|
|
22
22
|
import { buildFormSchema, parseFormSchema, pickFormLinkage, resolveFormIdentifier, } from "../core/form-contract.js";
|
|
23
23
|
import { isRecord } from "../core/guards.js";
|
|
24
|
+
import { applyAmbientLineage } from "../core/lineage.js";
|
|
24
25
|
/** Coerce an engine response's process-instance key to a non-empty string, or
|
|
25
26
|
* throw — a missing key means a malformed/partial response, not a real instance. */
|
|
26
27
|
export function requireProcessInstanceKey(key) {
|
|
@@ -76,9 +77,12 @@ export class SdkEngineClient {
|
|
|
76
77
|
return { deployed: resources.length };
|
|
77
78
|
}
|
|
78
79
|
async createInstance(input) {
|
|
80
|
+
// Auto-thread the `_urban.lineage` envelope (issue #254): a handler that spawns an
|
|
81
|
+
// instance propagates its `rootRequestKey` and stamps `causedByInstanceKey`; a genuine
|
|
82
|
+
// top-level request mints a fresh root. An explicit caller-supplied envelope wins.
|
|
79
83
|
const body = await this.client.createProcessInstance({
|
|
80
84
|
processDefinitionId: input.processDefinitionId,
|
|
81
|
-
variables: input.variables
|
|
85
|
+
variables: applyAmbientLineage(input.variables),
|
|
82
86
|
awaitCompletion: input.awaitCompletion ?? false,
|
|
83
87
|
});
|
|
84
88
|
const rawKey = body.processInstanceKey ?? body.key;
|
|
@@ -93,10 +97,12 @@ export class SdkEngineClient {
|
|
|
93
97
|
await this.client.cancelProcessInstance({ processInstanceKey: input.processInstanceKey });
|
|
94
98
|
}
|
|
95
99
|
async publishMessage(input) {
|
|
100
|
+
// Message-started instances inherit lineage too (issue #254): thread the envelope onto the
|
|
101
|
+
// message variables so the instance the message starts carries `_urban.lineage`.
|
|
96
102
|
await this.client.publishMessage({
|
|
97
103
|
name: input.name,
|
|
98
104
|
correlationKey: input.correlationKey ?? "",
|
|
99
|
-
variables: input.variables
|
|
105
|
+
variables: applyAmbientLineage(input.variables),
|
|
100
106
|
});
|
|
101
107
|
}
|
|
102
108
|
async searchUserTasks(filter) {
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -31,6 +31,10 @@ export { resolveHandler, sdkDecisionEvaluator } from "./core/modules/workers.ts"
|
|
|
31
31
|
export { buildMessages, callLlm, resolveProvider, runLlmJob, } from "./core/modules/llm.ts";
|
|
32
32
|
export type { ChatMessage, DecisionEvaluator, EnvLookup, LlmRuntime, LlmVars, ProviderConfig, } from "./core/modules/llm.ts";
|
|
33
33
|
export { evalCorrelation } from "./core/modules/triggers.ts";
|
|
34
|
+
export { applyAmbientLineage, buildLineageTree, deriveLineage, LINEAGE_KEY, LINEAGE_NAMESPACE, mintRootRequestKey, readLineage, writeLineage, } from "./core/lineage.ts";
|
|
35
|
+
export type { ApplyLineageOptions, LineageAttachment, LineageEdge, LineageEdgeType, LineageEnvelope, LineageNode, LineageTree, } from "./core/lineage.ts";
|
|
36
|
+
export { LINEAGE_ATTACHMENTS_TABLE, LINEAGE_EDGES_TABLE, LINEAGE_SCHEMA_SQL, LineageStore, systemClock as lineageSystemClock, } from "./core/modules/lineage-store.ts";
|
|
37
|
+
export type { Clock as LineageClock, LineageJobLike, LineageStoreOptions, } from "./core/modules/lineage-store.ts";
|
|
34
38
|
export { deployModels, RESOURCES_DIR } from "./core/modules/deploy.ts";
|
|
35
39
|
export { defaultScheduler, MAX_TIMER_DELAY_MS } from "./core/modules/scheduler.ts";
|
|
36
40
|
export type { SchedulerDeps } from "./core/modules/scheduler.ts";
|
package/dist/runtime/index.js
CHANGED
|
@@ -28,6 +28,11 @@ export { defineOperation, mountApi, resolveOperationHandler, NotImplemented } fr
|
|
|
28
28
|
export { resolveHandler, sdkDecisionEvaluator } from "./core/modules/workers.js";
|
|
29
29
|
export { buildMessages, callLlm, resolveProvider, runLlmJob, } from "./core/modules/llm.js";
|
|
30
30
|
export { evalCorrelation } from "./core/modules/triggers.js";
|
|
31
|
+
// Lineage — the framework-level lineage primitive (issue #254): the `_urban.lineage`
|
|
32
|
+
// envelope + auto-threading, and the generic read projection that stitches user intent →
|
|
33
|
+
// progress (a root request → the instances/PRs/tasks it causes).
|
|
34
|
+
export { applyAmbientLineage, buildLineageTree, deriveLineage, LINEAGE_KEY, LINEAGE_NAMESPACE, mintRootRequestKey, readLineage, writeLineage, } from "./core/lineage.js";
|
|
35
|
+
export { LINEAGE_ATTACHMENTS_TABLE, LINEAGE_EDGES_TABLE, LINEAGE_SCHEMA_SQL, LineageStore, systemClock as lineageSystemClock, } from "./core/modules/lineage-store.js";
|
|
31
36
|
// Model deploy (deploy-by-convention under `resources/`, ADR 0062)
|
|
32
37
|
export { deployModels, RESOURCES_DIR } from "./core/modules/deploy.js";
|
|
33
38
|
export { defaultScheduler, MAX_TIMER_DELAY_MS } from "./core/modules/scheduler.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
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",
|
|
@@ -21,6 +21,11 @@ export interface JobExecContext {
|
|
|
21
21
|
elementId?: string;
|
|
22
22
|
/** The job type (worker) currently executing. */
|
|
23
23
|
jobType?: string;
|
|
24
|
+
/** The lineage `rootRequestKey` the running instance belongs to (its own envelope's root,
|
|
25
|
+
* or — for a root instance with no envelope — its own instance key). Threaded so that when a
|
|
26
|
+
* handler spawns an instance or publishes a message, the SDK propagates the same root and
|
|
27
|
+
* sets `causedByInstanceKey` to {@link instanceKey} (issue #254, see `./lineage.ts`). */
|
|
28
|
+
rootRequestKey?: string;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
let store: AsyncStore<JobExecContext> | undefined;
|