@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/CHANGELOG.md +15 -0
- package/app/lineage.ts +25 -0
- package/app/mergesPerDayView.test.ts +166 -0
- package/app/migration064.test.ts +176 -0
- package/app/planWaveSummary.test.ts +170 -0
- package/app/plansReadModel.test.ts +262 -0
- package/db/migrations/059_plan_wave_summary.sql +93 -0
- package/db/migrations/060_plan_wave_rollup.sql +52 -0
- package/db/migrations/061_plan_delivery_rollup.sql +98 -0
- package/db/migrations/062_merges_per_day_view.sql +72 -0
- package/db/migrations/064_lineage_thread_view.sql +75 -0
- package/package.json +1 -1
- package/pages/epic-detail.page.json +49 -4
- package/pages/lineage.page.json +1 -1
- package/pages/overview.page.json +1 -1
- package/pages/velocity.page.json +1 -1
- package/scripts/pages-contract.test.ts +51 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
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": "
|
|
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": "
|
|
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
|
],
|
package/pages/lineage.page.json
CHANGED
package/pages/overview.page.json
CHANGED
package/pages/velocity.page.json
CHANGED
|
@@ -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
|
-
|
|
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.
|