@nanobpm/nano-workforce 0.83.0 → 0.84.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 +7 -0
- package/package.json +2 -2
- package/pages/epic-detail.page.json +16 -31
- package/scripts/pages-contract.test.ts +22 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [0.84.0](https://github.com/nanobpm/nano-workforce/compare/v0.83.0...v0.84.0) (2026-08-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **ui:** render narrative epic-detail sections with urban prose renderer ([#270](https://github.com/nanobpm/nano-workforce/issues/270)) ([#271](https://github.com/nanobpm/nano-workforce/issues/271)) ([38c67a4](https://github.com/nanobpm/nano-workforce/commit/38c67a479693730b9a79f3a7b0f88ef574d3a456)), closes [nano-ide#274](https://github.com/nano-ide/issues/274) [#87](https://github.com/nanobpm/nano-workforce/issues/87) [274/#275](https://github.com/nanobpm/nano-workforce/issues/275)
|
|
7
|
+
|
|
1
8
|
# [0.83.0](https://github.com/nanobpm/nano-workforce/compare/v0.82.1...v0.83.0) (2026-08-17)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.84.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",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@nanobpm/agentic": "^0.1.0",
|
|
56
|
-
"@nanobpm/urban": "^0.
|
|
56
|
+
"@nanobpm/urban": "^0.54.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@biomejs/biome": "^2.4.11",
|
|
@@ -137,38 +137,36 @@
|
|
|
137
137
|
}
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
|
-
"type": "
|
|
140
|
+
"type": "prose",
|
|
141
141
|
"id": "plan-reviews",
|
|
142
142
|
"props": {
|
|
143
143
|
"title": "Plan review trace",
|
|
144
144
|
"refreshMs": 5000,
|
|
145
145
|
"collapsible": true,
|
|
146
146
|
"defaultCollapsed": true,
|
|
147
|
+
"measure": 80,
|
|
148
|
+
"empty": "No plan reviews recorded yet.",
|
|
147
149
|
"data": {
|
|
148
150
|
"kind": "datasource",
|
|
149
151
|
"source": "app",
|
|
150
152
|
"table": "plan_reviews",
|
|
151
|
-
"orderBy": { "field": "
|
|
153
|
+
"orderBy": { "field": "created_at", "dir": "asc" },
|
|
152
154
|
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
153
155
|
},
|
|
154
|
-
"
|
|
155
|
-
|
|
156
|
-
{ "field": "epoch", "header": "Epoch" },
|
|
157
|
-
{ "field": "approved", "header": "Approved? (1/0)" },
|
|
158
|
-
{ "field": "findings", "header": "Reviewer findings" },
|
|
159
|
-
{ "field": "created_at", "header": "Recorded" }
|
|
160
|
-
]
|
|
156
|
+
"header": "Round {{round}} · epoch {{epoch}} · approved {{approved}} · {{created_at}}",
|
|
157
|
+
"body": "findings"
|
|
161
158
|
}
|
|
162
159
|
},
|
|
163
160
|
{
|
|
164
|
-
"type": "
|
|
161
|
+
"type": "prose",
|
|
165
162
|
"id": "plan-review-escalations",
|
|
166
163
|
"props": {
|
|
167
164
|
"title": "Plan-review escalations",
|
|
168
|
-
"rowKey": "id",
|
|
169
165
|
"refreshMs": 5000,
|
|
170
166
|
"collapsible": true,
|
|
171
167
|
"defaultCollapsed": true,
|
|
168
|
+
"measure": 80,
|
|
169
|
+
"empty": "No plan-review escalations.",
|
|
172
170
|
"data": {
|
|
173
171
|
"kind": "datasource",
|
|
174
172
|
"source": "app",
|
|
@@ -176,16 +174,8 @@
|
|
|
176
174
|
"orderBy": { "field": "id", "dir": "desc" },
|
|
177
175
|
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
178
176
|
},
|
|
179
|
-
"
|
|
180
|
-
|
|
181
|
-
{ "field": "round", "header": "Round" },
|
|
182
|
-
{ "field": "findings", "header": "Findings" },
|
|
183
|
-
{ "field": "status", "header": "Status" },
|
|
184
|
-
{ "field": "directive", "header": "Directive" },
|
|
185
|
-
{ "field": "note", "header": "Human note" },
|
|
186
|
-
{ "field": "asked_at", "header": "Asked" },
|
|
187
|
-
{ "field": "answered_at", "header": "Answered" }
|
|
188
|
-
]
|
|
177
|
+
"header": "Round {{round}} · epoch {{epoch}} · {{status}} · directive {{directive}} · asked {{asked_at}} · answered {{answered_at}} · note {{note}}",
|
|
178
|
+
"body": "findings"
|
|
189
179
|
}
|
|
190
180
|
},
|
|
191
181
|
{
|
|
@@ -214,14 +204,15 @@
|
|
|
214
204
|
}
|
|
215
205
|
},
|
|
216
206
|
{
|
|
217
|
-
"type": "
|
|
207
|
+
"type": "prose",
|
|
218
208
|
"id": "coordination-notes",
|
|
219
209
|
"props": {
|
|
220
210
|
"title": "Coordination notes",
|
|
221
|
-
"rowKey": "id",
|
|
222
211
|
"refreshMs": 5000,
|
|
223
212
|
"collapsible": true,
|
|
224
213
|
"defaultCollapsed": true,
|
|
214
|
+
"measure": 80,
|
|
215
|
+
"empty": "No coordination notes posted yet.",
|
|
225
216
|
"data": {
|
|
226
217
|
"kind": "datasource",
|
|
227
218
|
"source": "app",
|
|
@@ -229,14 +220,8 @@
|
|
|
229
220
|
"orderBy": { "field": "id", "dir": "asc" },
|
|
230
221
|
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
231
222
|
},
|
|
232
|
-
"
|
|
233
|
-
|
|
234
|
-
{ "field": "author_task", "header": "Agent" },
|
|
235
|
-
{ "field": "kind", "header": "Kind" },
|
|
236
|
-
{ "field": "files", "header": "Files" },
|
|
237
|
-
{ "field": "body", "header": "Note" },
|
|
238
|
-
{ "field": "created_at", "header": "Posted" }
|
|
239
|
-
]
|
|
223
|
+
"header": "Wave {{wave}} · {{author_task}} · {{kind}} · files {{files}} · {{created_at}}",
|
|
224
|
+
"body": "body"
|
|
240
225
|
}
|
|
241
226
|
},
|
|
242
227
|
{
|
|
@@ -112,6 +112,12 @@ function filterFields(filter: Json): string[] {
|
|
|
112
112
|
return filter.map((f: Json) => f?.field).filter(Boolean);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// Pull `{{field}}` interpolation names out of a prose renderer's header template (nano-ide#274).
|
|
116
|
+
function templateFields(tpl: Json): string[] {
|
|
117
|
+
if (typeof tpl !== "string") return [];
|
|
118
|
+
return [...tpl.matchAll(/\{\{([^{}]+)\}\}/g)].map((m) => m[1].trim()).filter(Boolean);
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
function collectRefs(page: string, node: Json, out: Ref[]): void {
|
|
116
122
|
if (Array.isArray(node)) {
|
|
117
123
|
for (const v of node) collectRefs(page, v, out);
|
|
@@ -120,10 +126,17 @@ function collectRefs(page: string, node: Json, out: Ref[]): void {
|
|
|
120
126
|
if (!node || typeof node !== "object") return;
|
|
121
127
|
|
|
122
128
|
// Top-level datasource grid: the datasource lives at `node.data`, while `columns`, `rowKey`,
|
|
123
|
-
// `filter`/`tabs`, and `detail` are siblings on the same `node` (the grid props).
|
|
129
|
+
// `filter`/`tabs`, and `detail` are siblings on the same `node` (the grid props). A `prose`
|
|
130
|
+
// renderer (nano-ide#274) binds the same `node.data` but has no `columns`: its displayed content
|
|
131
|
+
// is the header template's `{{field}}` refs plus the single `body` field, so fold those in as
|
|
132
|
+
// "columns" so the field-existence + surface guards below apply to prose sections too.
|
|
124
133
|
const data = node.data;
|
|
125
134
|
if (data && data.kind === "datasource" && typeof data.table === "string") {
|
|
126
|
-
const columns: string[] =
|
|
135
|
+
const columns: string[] = [
|
|
136
|
+
...(node.columns ?? []).map((c: Json) => c.field),
|
|
137
|
+
...templateFields(node.header),
|
|
138
|
+
...(typeof node.body === "string" ? [node.body] : []),
|
|
139
|
+
].filter(Boolean);
|
|
127
140
|
// Every reference that resolves to a column on this table — the runtime 400s on any of them if
|
|
128
141
|
// it names a column the migrations never created, so all must be guarded, not just displayed
|
|
129
142
|
// columns. `detail.fields`/`detail.linkField` render columns of the same top-level row.
|
|
@@ -215,17 +228,19 @@ test("issue #87: plan_reviews is surfaced on the per-epic detail page", async ()
|
|
|
215
228
|
const onEpicDetail = refs.some(
|
|
216
229
|
(r) => r.page === "epic-detail.page.json" && r.table === "plan_reviews",
|
|
217
230
|
);
|
|
218
|
-
assert(onEpicDetail, "epic-detail.page.json must bind
|
|
231
|
+
assert(onEpicDetail, "epic-detail.page.json must bind the plan-review trace to plan_reviews");
|
|
219
232
|
|
|
220
|
-
// The trace is only useful with the verdict + critique
|
|
221
|
-
// visibly displayed `columns` (not `fields`, which also holds binding refs like orderBy.field)
|
|
222
|
-
//
|
|
233
|
+
// The trace is only useful with the verdict + critique surfaced, so pin them. Assert against the
|
|
234
|
+
// visibly displayed `columns` (not `fields`, which also holds binding refs like orderBy.field) —
|
|
235
|
+
// for the `prose` renderer (nano-ide#274) these are the header template's `{{round}}`/`{{approved}}`
|
|
236
|
+
// refs plus the `findings` body — so a field silently dropped from the UI can't pass by being
|
|
237
|
+
// referenced elsewhere.
|
|
223
238
|
const required = ["round", "approved", "findings"];
|
|
224
239
|
for (const r of refs.filter((x) => x.table === "plan_reviews")) {
|
|
225
240
|
for (const col of required) {
|
|
226
241
|
assert(
|
|
227
242
|
r.columns.includes(col),
|
|
228
|
-
`${r.page}: plan_reviews
|
|
243
|
+
`${r.page}: plan_reviews trace must surface the "${col}" field`,
|
|
229
244
|
);
|
|
230
245
|
}
|
|
231
246
|
}
|