@nanobpm/nano-workforce 0.118.2 → 0.120.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.118.2",
3
+ "version": "0.120.0",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",
@@ -82,6 +82,51 @@
82
82
  "variant": "sub"
83
83
  }
84
84
  },
85
+ {
86
+ "type": "prose",
87
+ "id": "wave-banner",
88
+ "props": {
89
+ "refreshMs": 5000,
90
+ "measure": 80,
91
+ "empty": "No plan recorded for this epic yet.",
92
+ "data": {
93
+ "kind": "datasource",
94
+ "source": "app",
95
+ "table": "plan_read_model",
96
+ "orderBy": { "field": "updated_at", "dir": "desc" },
97
+ "filter": [{ "field": "plan_key", "eqParam": true }]
98
+ },
99
+ "header": "Wave {{wave_label}} · {{epic_phase}}",
100
+ "body": "delivery_label"
101
+ }
102
+ },
103
+ {
104
+ "type": "dataGrid",
105
+ "id": "wave-summary",
106
+ "props": {
107
+ "title": "Wave progress",
108
+ "rowKey": "wave",
109
+ "refreshMs": 5000,
110
+ "empty": "No waves yet — this epic hasn't been levelized into waves.",
111
+ "data": {
112
+ "kind": "datasource",
113
+ "source": "app",
114
+ "table": "plan_wave_summary",
115
+ "orderBy": { "field": "wave", "dir": "asc" },
116
+ "filter": [{ "field": "plan_key", "eqParam": true }]
117
+ },
118
+ "columns": [
119
+ { "field": "wave", "header": "Wave" },
120
+ { "field": "bar", "header": "Progress", "width": "40%" },
121
+ { "field": "merged", "header": "Merged" },
122
+ { "field": "in_flight", "header": "In flight" },
123
+ { "field": "blocked", "header": "Blocked" },
124
+ { "field": "escalated", "header": "Escalated" },
125
+ { "field": "skipped", "header": "Skipped" },
126
+ { "field": "total", "header": "Total" }
127
+ ]
128
+ }
129
+ },
85
130
  {
86
131
  "type": "dataGrid",
87
132
  "id": "epic-plan",
@@ -92,7 +137,7 @@
92
137
  "data": {
93
138
  "kind": "datasource",
94
139
  "source": "app",
95
- "table": "plans",
140
+ "table": "plan_read_model",
96
141
  "orderBy": { "field": "updated_at", "dir": "desc" },
97
142
  "filter": [{ "field": "plan_key", "eqParam": true }]
98
143
  },
@@ -161,7 +206,7 @@
161
206
  "data": {
162
207
  "kind": "datasource",
163
208
  "source": "app",
164
- "table": "plan_tasks",
209
+ "table": "plan_wave_tasks",
165
210
  "orderBy": { "field": "wave", "dir": "asc" },
166
211
  "filter": [
167
212
  { "field": "plan_key", "eqParam": true },
@@ -192,8 +237,8 @@
192
237
  { "field": "wave", "header": "Wave" },
193
238
  { "field": "task_id", "header": "Task" },
194
239
  { "field": "title", "header": "Title" },
195
- { "field": "status", "header": "Status" },
196
- { "field": "pr_key", "header": "PR" },
240
+ { "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
241
+ { "field": "pr_key", "header": "PR", "linkField": "pr_url" },
197
242
  { "field": "summary", "header": "Summary" },
198
243
  { "field": "updated_at", "header": "Updated", "format": "datetime" }
199
244
  ],
@@ -87,7 +87,7 @@
87
87
  "data": {
88
88
  "kind": "datasource",
89
89
  "source": "app",
90
- "table": "lineage_threads",
90
+ "table": "lineage_thread_view",
91
91
  "orderBy": { "field": "updated_at", "dir": "desc" },
92
92
  "filter": [{ "field": "active", "in": [1] }]
93
93
  },
@@ -140,7 +140,7 @@
140
140
  "data": {
141
141
  "kind": "datasource",
142
142
  "source": "app",
143
- "table": "plans",
143
+ "table": "plan_read_model",
144
144
  "orderBy": { "field": "updated_at", "dir": "desc" },
145
145
  "filter": [{ "field": "list_bucket", "in": ["active"] }]
146
146
  },
@@ -85,7 +85,7 @@
85
85
  "data": {
86
86
  "kind": "datasource",
87
87
  "source": "app",
88
- "table": "merges_per_day",
88
+ "table": "merges_per_day_view",
89
89
  "orderBy": { "field": "day", "dir": "asc" }
90
90
  },
91
91
  "header": "{{day}} \u00b7 {{merged}} merged \u00b7 \u03a3 {{cumulative}}",
@@ -49,6 +49,54 @@ function parseSchema(sql: string, schema: Map<string, Set<string>>): void {
49
49
  cols.add(m[2]);
50
50
  schema.set(m[1], cols);
51
51
  }
52
+ // CREATE VIEW [IF NOT EXISTS] <name> AS SELECT <select-list> FROM …
53
+ //
54
+ // A page datasource can bind a VIEW as well as a base table (nano-ide#424), so a view's OUTPUT
55
+ // columns must join the whitelist exactly like a table's. A view has no `PRAGMA`-visible column
56
+ // list in this static parse, so we read them off its SELECT projection: take the select-list up
57
+ // to the first top-level `FROM`, split it on top-level commas, and record each item's output name
58
+ // (its `AS <alias>`, or the trailing identifier of a bare `t.col` / `col`). This is why the
59
+ // repo's views (see 059_plan_wave_summary.sql) alias every projected column and avoid select-list
60
+ // subqueries — it keeps this purely-textual contract guard able to see their columns.
61
+ const viewRe = /CREATE\s+VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?\s+AS\s+SELECT\s+/gi;
62
+ while ((m = viewRe.exec(sql)) !== null) {
63
+ const selectList = topLevelSelectList(sql, viewRe.lastIndex);
64
+ if (selectList === null) continue;
65
+ const cols = schema.get(m[1]) ?? new Set<string>();
66
+ for (const frag of splitTopLevel(selectList)) {
67
+ const col = viewColumnName(frag);
68
+ if (col) cols.add(col);
69
+ }
70
+ schema.set(m[1], cols);
71
+ }
72
+ }
73
+
74
+ // The select-list of a view: the text between `SELECT` (its end at `start`) and the first top-level
75
+ // `FROM` keyword (one at paren depth 0 — a `FROM` inside a function/subquery doesn't end the list).
76
+ function topLevelSelectList(s: string, start: number): string | null {
77
+ let depth = 0;
78
+ for (let i = start; i < s.length; i++) {
79
+ const c = s[i];
80
+ if (c === "(") depth++;
81
+ else if (c === ")") depth--;
82
+ else if (depth === 0 && (c === "F" || c === "f")) {
83
+ // whole-word FROM with a non-identifier char (or string start) on each side
84
+ if (/from/i.test(s.slice(i, i + 4)) && !/\w/.test(s[i - 1] ?? " ") && !/\w/.test(s[i + 4] ?? " ")) {
85
+ return s.slice(start, i);
86
+ }
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+
92
+ // The output name of one select-list item: its `AS <alias>` if present, else the trailing
93
+ // identifier of a bare `table.column` / `column` reference.
94
+ function viewColumnName(frag: string): string | null {
95
+ const trimmed = frag.trim();
96
+ const asMatch = trimmed.match(/\bAS\s+["`]?(\w+)["`]?\s*$/i);
97
+ if (asMatch) return asMatch[1];
98
+ const bare = trimmed.match(/(\w+)\s*$/);
99
+ return bare ? bare[1] : null;
52
100
  }
53
101
 
54
102
  // Return the text inside the parentheses whose opener is at `openIdx`, honouring nesting.
@@ -275,12 +323,14 @@ test("issue #205: overview is the landing page and first nav item", async () =>
275
323
  // EPIC surface buckets on the DERIVED `list_bucket` (issue #298) — NOT raw `status` — so a `done`
276
324
  // epic still converging, or landed-but-unpromoted, does not vanish from the in-flight Epics section
277
325
  // the instant `status=done`. Guarding the field here is the regression guard for that defect class.
326
+ // The epic surface binds the derived `plan_read_model` VIEW (epic #412 — the single source of truth
327
+ // for the wave/delivery projections it also renders), not the raw `plans` table.
278
328
  const expected: Record<string, { field: string; in: string[] }> = {
279
329
  pull_requests: {
280
330
  field: "status",
281
331
  in: ["converging", "waiting_review", "escalated", "waiting_deps", "waiting_merge", "queued", "merging"],
282
332
  },
283
- plans: { field: "list_bucket", in: ["active"] },
333
+ plan_read_model: { field: "list_bucket", in: ["active"] },
284
334
  feature_runs: { field: "status", in: ["running", "escalated", "awaiting_operator"] },
285
335
  // The 4th dispatch surface (issue #386) — active delivery graphs. Both in-flight statuses
286
336
  // (`awaiting-approval` parked at the gate, `running` dispatched) show here.