@nanobpm/nano-workforce 0.139.2 → 0.139.3
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
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.139.3](https://github.com/nanobpm/nano-workforce/compare/v0.139.2...v0.139.3) (2026-08-25)
|
|
2
|
+
|
|
3
|
+
### Documentation
|
|
4
|
+
|
|
5
|
+
* **adr:** add §4b derived stepper decision to ADR 0006 ([#540](https://github.com/nanobpm/nano-workforce/issues/540)) ([e85d25b](https://github.com/nanobpm/nano-workforce/commit/e85d25be90fb77cd5ea53b5836df68103c2e2f33)), closes [#464](https://github.com/nanobpm/nano-workforce/issues/464) [nano-ide#473](https://github.com/nanobpm/nano-ide/issues/473) [#473](https://github.com/nanobpm/nano-workforce/issues/473) [#473](https://github.com/nanobpm/nano-workforce/issues/473) [#464](https://github.com/nanobpm/nano-workforce/issues/464)
|
|
6
|
+
|
|
1
7
|
## [0.139.2](https://github.com/nanobpm/nano-workforce/compare/v0.139.1...v0.139.2) (2026-08-24)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
|
@@ -20,8 +20,13 @@ to collapse three bespoke status unions into one),
|
|
|
20
20
|
nano-ide **#424** (datasource can read a SQL VIEW — the *data-level* unlock),
|
|
21
21
|
nano-workforce **#416** (the PR bumping the testkit to engine-wasm 0.7.2, which executes `callActivity`
|
|
22
22
|
— the *process-level* unlock),
|
|
23
|
-
nano-workforce **#464** (the tracking issue
|
|
24
|
-
|
|
23
|
+
nano-workforce **#464** (the tracking issue — its live checklist currently ends at **S5**; **S6 is
|
|
24
|
+
present in this ADR's rollout but not yet listed there**, and the S7/S8 slices below are **added by this
|
|
25
|
+
ADR** — so S6–S8 must all be appended to #464's checklist, which does not yet list them),
|
|
26
|
+
nano-workforce **#305** (consolidate escalations on native `user_tasks` — a natural sub-step of S1/S3),
|
|
27
|
+
nano-ide **#473** (surface the element-instance query on the `EngineClient` binding — the sole upstream
|
|
28
|
+
**binding/platform** dependency of the S8 stepper slice; S8 *additionally* depends on S4's
|
|
29
|
+
inline-vs-child / correlation decision, tracked with #464).
|
|
25
30
|
|
|
26
31
|
## Context
|
|
27
32
|
|
|
@@ -85,7 +90,10 @@ the multi-instance `implement` subprocess in `plan-fanout.bpmn` — plus a **thi
|
|
|
85
90
|
compiler that re-emits it once per graph node. Sibling cells
|
|
86
91
|
(readiness-poll, human-escalation) duplicate the same way. **No `callActivity` exists in any diagram**
|
|
87
92
|
because it did nothing. **Unlocked by #416** (engine-wasm 0.4.0 → **0.7.2**). engine-core executes
|
|
88
|
-
`callActivity`
|
|
93
|
+
`callActivity` natively as a **linked child process instance** (`engine-core/src/model.rs`); inline-expanding
|
|
94
|
+
the called process into one flat instance is the **opt-in** `inline_call_activities` transform, *not* the
|
|
95
|
+
default — the same native-child-vs-opt-in-inline distinction §4b's cross-instance-correlation note below
|
|
96
|
+
depends on.
|
|
89
97
|
**Verified live:** a `callActivity` parent+child model deployed through engine-wasm 0.7.2 runs to
|
|
90
98
|
`COMPLETED`. Caveat: #416 bumps only the **dev-only** `@nanobpm/urban-testkit`; the production
|
|
91
99
|
`@nanobpm/urban` broker does not itself pin `engine-wasm`, so this verification proves the in-process
|
|
@@ -187,6 +195,277 @@ representation. These unions are **not** identical today — features use
|
|
|
187
195
|
separate node contract — plus the per-shape mapping and precedence and the write/`instanceTracking`
|
|
188
196
|
behavior — not merely projecting an existing value.
|
|
189
197
|
|
|
198
|
+
### 4b. One derived **stepper** — the cells are the source positions mapped onto the shared step axis, per-shape correlated
|
|
199
|
+
|
|
200
|
+
§4 collapses the three status *unions* into one derived union; this decision collapses the three
|
|
201
|
+
*progress projections* — the thing an operator actually reads as "where is this unit in its
|
|
202
|
+
lifecycle" — into **one derived stepper**, rendered by **one** primitive across all three surfaces.
|
|
203
|
+
|
|
204
|
+
Today that projection is triplicated the same way the aggregate is, and at three different maturity
|
|
205
|
+
levels for one idea:
|
|
206
|
+
|
|
207
|
+
| Surface | Derivation | Step vocabulary | Render |
|
|
208
|
+
|---|---|---|---|
|
|
209
|
+
| **Feature** | `deriveStage` over the declare-once read-model (`app/stage.ts`; mig. 076 declared it, mig. 081 superseded the VIEW for terminal-folded status — 073 is historical). Its inputs are the feature run's effective `status` / `pr_key` / `converge`+`auto_merge` flags / open user-task projections; `pollFeatureDelivery` separately reconciles PR status **into** that row upstream | `STAGE_KEYS` = Requested → Implementing → PR open → Converging → Merging → Done | the **`pipeline` stepper** kind (`pages/feature.page.json`) |
|
|
210
|
+
| **Epic** | **write-time stamp** by each spine worker (`app/epicPhase.ts`, `ELEMENT_PHASE`) | `EPIC_PHASE` (wave-labelled) | a plain `{{epic_phase}}` **text cell** |
|
|
211
|
+
| **Delivery graph** | engine-truth poll of **process-instance state + open user-task parks** (`pollDeliveryGraphPhase` → `deriveDeliveryPhase`: `COMPLETED`/`TERMINATED` → `done`/`failed`, else the open human-node park) | `DELIVERY_PHASE` + `Parked on human node: <label>` | a plain `phase` **text cell** |
|
|
212
|
+
|
|
213
|
+
Three derivations × three vocabularies × two renderers — and only the feature surface renders an
|
|
214
|
+
actual stepper; epic and delivery-graph project a bare string. A change to "the lifecycle steps of a
|
|
215
|
+
unit of work" is the same by-hand-in-three-places drift this ADR exists to remove.
|
|
216
|
+
|
|
217
|
+
**The step axis is the cells — but the cell→step mapping must be defined, not assumed.** Once §2 makes
|
|
218
|
+
the process a composition of named cells (`implement` → `converge?` → `merge?`, plus `wait` / `human` /
|
|
219
|
+
`escalation`), those ordered cells are the natural step axis. `STAGE_KEYS` is the closest *existing*
|
|
220
|
+
projection of it, but it is **not** literally that sequence: `Requested` / `PR open` / `Done` are
|
|
221
|
+
lifecycle states, while `implement` / `converge` / `merge` are process cells (and today's feature
|
|
222
|
+
derivation additionally treats `Merging` as an upcoming/visual stage and can mark stages *skipped*). So
|
|
223
|
+
§4b's first deliverable is an explicit **cell → step mapping** — which cell entry/exit advances which
|
|
224
|
+
step, how `Requested`/`PR open`/`Done` bracket the cell run, and how an inserted `wait` / `human` /
|
|
225
|
+
`escalation` cell **collapses into an existing `STAGE_KEYS` bracket** — the `pipeline` renderer binds a
|
|
226
|
+
static `stages` array (`pages/feature.page.json:120-126`), so v1 adds no axis entries and needs no
|
|
227
|
+
renderer change — seeded from `STAGE_KEYS` but owning the canonical definition. It
|
|
228
|
+
is not enough to declare one axis *is* the other. **v1 leaves the two existing axis consumers physically
|
|
229
|
+
in place** — the exported `STAGE_KEYS` (`app/stage.ts`) and the static `stages` array in
|
|
230
|
+
`pages/feature.page.json` — and only *seeds* the new mapping from `STAGE_KEYS`; it does **not** yet
|
|
231
|
+
generate or retire either, so both remain until a follow-up derives the static `stages` array from the
|
|
232
|
+
canonical mapping (a parity test, then retirement of the duplicate) — flagged here as the residual
|
|
233
|
+
drift-surface the one-stepper decision does not close in v1. (The canonical stage *derivation* itself
|
|
234
|
+
already has a single author — `app/featureReadModel.ts`, from which both the SQLite VIEW and the
|
|
235
|
+
`deriveStage` TS oracle are generated; its `caseWhen` emits the `Requested`…`Done` literals directly and
|
|
236
|
+
does **not** import `STAGE_KEYS`. So `STAGE_KEYS` is not the derivation's source but *itself* one of the
|
|
237
|
+
two vocabulary duplicates whose literals must be reconciled against — and ultimately derived from — the
|
|
238
|
+
canonical `featureReadModel` declaration; `featureReadModel` is that canonical author, not a third
|
|
239
|
+
divergent one.)
|
|
240
|
+
|
|
241
|
+
**The current step is derived by per-shape correlation (not a single `process_key` join).**
|
|
242
|
+
The projection must answer "which cell has this unit reached," fusing two truth sources:
|
|
243
|
+
|
|
244
|
+
- **Engine truth — the furthest element reached — IS available on the runtime.** nwf runs against
|
|
245
|
+
**Nano's Rust engine over its REST API** (not the wasm testkit). That API exposes a full
|
|
246
|
+
element-instance read model: `POST /v2/element-instances/search` (`searchElementInstances` →
|
|
247
|
+
`elementId` + `elementType` + element **state**, keyed by process-instance) and the element-instance
|
|
248
|
+
**wait-states** search (`searchElementInstanceWaitStates` → **job and message** parks, not only user
|
|
249
|
+
tasks). With namespaced cell element ids (a convention §2/S4 must still define — the current compiler
|
|
250
|
+
uses node-local ids), a reached element maps to a reached cell — the
|
|
251
|
+
sharpest signal, and the only one that sees a token mid-cell (an active `implement` job) that no
|
|
252
|
+
work-table row has caught up to yet.
|
|
253
|
+
- **Work-table + aggregate truth — the correlated downstream state, per shape.** For a **feature**, the
|
|
254
|
+
`feature_runs` row (`status` / `pr_key` / flags) gives the coarse `Requested` / `Implementing` / `Done`
|
|
255
|
+
bracket; for an **epic** there is **no aggregate `pr_key`** — its status is `plans.status` / the plan
|
|
256
|
+
rollups and its slice-PR identity is `plan_tasks.pr_key` (`app/retro.ts`, `app/delivery.ts`). On top of
|
|
257
|
+
that bracket, `pull_requests` (and siblings) carry the `converging` / `waiting_review` /
|
|
258
|
+
`merging` sub-state — but this is a **coarse status handoff, not a preserved sub-state**:
|
|
259
|
+
`pollFeatureDelivery` maps the feature's **own `pr_key`** PR status into `feature_runs` (keeping the
|
|
260
|
+
detailed value only in `delivery_label`), and `deriveStage` reads neither `delivery_label` nor those
|
|
261
|
+
sub-states — an opened `pr_key` renders `PR open` and a `converging` run renders `Converging`, i.e. it
|
|
262
|
+
exposes only the coarse lifecycle key (never `Merging` as the active stage). (`pollFeatureDelivery` does
|
|
263
|
+
**not** consume the lineage/plan projections — those are separate aggregate rollups for the epic path,
|
|
264
|
+
not the feature derivation; `instanceTracking` supplies only the tracking-status edges, not the PR
|
|
265
|
+
rollup.) So S7 must describe this as a coarse handoff and, for an epic, treat the lineage/plan rollups
|
|
266
|
+
as their own shape-specific path. The projection must
|
|
267
|
+
name an explicit **precedence** between engine truth and this state (engine element position when
|
|
268
|
+
available, else aggregate/work-table), not silently pick one.
|
|
269
|
+
|
|
270
|
+
**Correlation is per-shape, because `process_key` is reassigned.** A feature/epic aggregate's
|
|
271
|
+
`process_key` identifies its *own* (feature / plan-fanout) process instance, but
|
|
272
|
+
`pull_requests.process_key` is **overwritten** — first to the downstream convergence instance, then
|
|
273
|
+
again to the merge instance (`app/service.ts` — `submitPr` writes the convergence-instance key,
|
|
274
|
+
`startMerge` the merge-instance key). The stable
|
|
275
|
+
aggregate↔PR link is **`pr_key`**, and epics additionally need node/root mapping (lineage
|
|
276
|
+
`rootRequestKey`, nwf#245). So §4b must define the per-shape joins — or persist a **canonical unit
|
|
277
|
+
key** — rather than assume a single `process_key` join; a naïve `process_key` join would misattribute
|
|
278
|
+
or miss work-table state. This canonical-key requirement is itself an argument for the §1 aggregate.
|
|
279
|
+
|
|
280
|
+
**The aggregate step is a frontier — reduced to one deterministic step for the renderer.** For the
|
|
281
|
+
1-node feature there is one active step. For an N-node/parallel DAG (epic waves, delivery graphs), two
|
|
282
|
+
branches can occupy incomparable cells at once, and no total order picks a unique maximum. But the
|
|
283
|
+
`pipeline` renderer binds a **single scalar** `activeField` (`pages/feature.page.json`), so S7 **must**
|
|
284
|
+
reduce that frontier deterministically to one step — the canonical choice is the **least-advanced active
|
|
285
|
+
branch** (the "still blocked on" read), so the aggregate never renders further along than its slowest
|
|
286
|
+
in-flight branch. This reduction is an explicit S7 decision, not left to the implementer; a set-valued /
|
|
287
|
+
multi-track render would be a separate renderer change, out of scope here. **v1 does not populate the
|
|
288
|
+
pipeline's `notInPathField` (the skipped-path axis) for the aggregate** — the reduction exposes only the
|
|
289
|
+
scalar `activeField` + terminal `state`, so no deterministic skipped-set rollup is defined and equivalent
|
|
290
|
+
graphs cannot diverge on a skipped path they never render; a per-branch skipped-set rollup onto
|
|
291
|
+
`notInPathField` is deferred with the set-valued renderer change (out of scope). The per-node steps are
|
|
292
|
+
individually well-defined **only in the conceptual / S8 element-instance model** underneath the
|
|
293
|
+
reduction; at S7's coarse fidelity an active agent/wait/connector node exposes only `Running` with **no
|
|
294
|
+
node id** (`deriveDeliveryPhase`), so S7 assigns it **no individual cell step** and renders a single
|
|
295
|
+
coarse run-level step — the per-cell reduction becomes observable only once S8 binds the element-instance
|
|
296
|
+
topology. Because the canonical unit union carries more
|
|
297
|
+
terminal states than `done`/`failed` (a branch can end `merged` / `converged` / `skipped` / `blocked` /
|
|
298
|
+
`abandoned`), the rule first **normalizes** each terminal branch — reusing the **shipped
|
|
299
|
+
`featureReadModel` `stage_state` tiers** (`app/featureReadModel.ts`: `STAGE_DONE_STATUSES` + the
|
|
300
|
+
`stage_state` CASE) as the tier basis rather than inventing a second mapping, per derivation-over-duplication.
|
|
301
|
+
Note `done` is **not** itself emitted by the feature model — its `STAGE_DONE_STATUSES`
|
|
302
|
+
(`app/featureReadModel.ts:41`) omits `done` and `stage_state` yields only `ok` / `blocked` / `failed` /
|
|
303
|
+
`null`; `done` is the **new** canonical success value the delivery-unit / plan / graph unions introduce.
|
|
304
|
+
The union normalizes explicitly: its success terminal — `done`, and likewise `merged` / `converged` — collapses to that same
|
|
305
|
+
**successful** terminal (`done`); `failed` / `skipped` /
|
|
306
|
+
`abandoned` collapse to a **failed** terminal; and `blocked` stays the renderer's **distinct `blocked`**
|
|
307
|
+
state — *not* folded into `done` or `failed`, preserving the existing per-node semantics (`skipped` is a
|
|
308
|
+
failed-tier terminal there, not a success; `blocked` is its own terminal). These canonical tiers are
|
|
309
|
+
**not** fed to the renderer as-is: the `pipeline` column's `state` field accepts only `ok` / `failed` /
|
|
310
|
+
`blocked` / `null` (`app/stage.ts` `StageState`), so when rendered the **successful** terminal (`done`)
|
|
311
|
+
must map to `activeField = Done` with `state = ok`, the **failed** tier to `state = failed`, and
|
|
312
|
+
`blocked` to the distinct `blocked` state — the raw canonical `done` never reaches the renderer's
|
|
313
|
+
`stateField` (any other string silently degrades to in-progress, so a converged terminal must not be fed
|
|
314
|
+
`done` verbatim). For the frontier precedence
|
|
315
|
+
below, `done` is the only **successful** terminal, while both `failed` and `blocked` are **non-success,
|
|
316
|
+
operator-actionable** terminals — so the combinations below are defined over `done` (success) vs. a
|
|
317
|
+
non-success terminal (`failed`/`blocked`). This `done` is the **derived per-cell/delivery success
|
|
318
|
+
terminal**, **not** the raw `plans.status = 'done'` fan-out handoff — per `app/delivery.ts:39-42` that
|
|
319
|
+
plan status only means "the fan-out finished and ≥1 slice opened a PR, dispatched to convergence" (other
|
|
320
|
+
slices may still be blocked/skipped), so feeding it verbatim would render an epic `Done` prematurely; the
|
|
321
|
+
epic's actual success signal is `delivery = landed` (all slice PRs merged), which is what maps to this
|
|
322
|
+
success bucket. The rule must then define the **terminal combinations** the least-advanced-*active*
|
|
323
|
+
read leaves unspecified, so parallel epics/graphs render deterministically: (a) **mixed** — one or more
|
|
324
|
+
branches terminal alongside ≥1 active branch — reduces to the least-advanced *active* branch (terminal
|
|
325
|
+
branches are past, not "still blocked on"), **except** that a **non-success** terminal (`failed` or
|
|
326
|
+
`blocked`) takes **precedence** and renders the aggregate at that branch's step with that terminal's
|
|
327
|
+
render state (`state = failed` for a failed branch, the distinct `blocked` state for a blocked one — an
|
|
328
|
+
operator-actionable signal, not something an in-flight sibling should mask); when **multiple** branches
|
|
329
|
+
are in a non-success terminal the tie-break is **earliest terminal step, then stable node id**, so the
|
|
330
|
+
exposed step (and its `failed`/`blocked` state) is deterministic; (b) **all-terminal** — reduces to the
|
|
331
|
+
**earliest non-success terminal** step (`failed`/`blocked`, same tie-break) if any branch is non-success,
|
|
332
|
+
else `done`.
|
|
333
|
+
|
|
334
|
+
**The success predicate is shape-aware — `converged` is a success terminal per-PR/cell, but NOT for the
|
|
335
|
+
epic rollup.** The normalization above collapses `converged` (and `merged`) into the success terminal
|
|
336
|
+
`done` at the **per-PR / per-cell frontier** — correct for a single PR's own stepper (a review-only PR
|
|
337
|
+
*is* done once converged). But an **epic** aggregates *slice PRs*, and there a slice that is `converged`
|
|
338
|
+
but not merged is **resolved-not-landed**: `deriveDelivery` reports epic success as `delivery = landed`
|
|
339
|
+
**only when every slice PR has merged** (`prsInFlight == 0 && prsMerged == prsOpened && prsOpened > 0`),
|
|
340
|
+
and treats `converged` / `abandoned` slices as terminal-but-not-landed that never make an epic `landed`
|
|
341
|
+
(`app/delivery.ts:39-64`). **A `blocked`/`skipped` task produces no PR and so is invisible to that
|
|
342
|
+
PR-count predicate** (`record-results` can set `plan.status = done` once *one* slice opens a PR while
|
|
343
|
+
siblings are blocked/skipped — `app/delivery.ts:40-42`), so `delivery = landed` alone would wrongly
|
|
344
|
+
bucket an epic with one merged slice plus a blocked task as `Done`. The epic success predicate is
|
|
345
|
+
therefore **`delivery = landed` AND no task in a non-success terminal** (`blocked`/`skipped`): such a
|
|
346
|
+
task independently vetoes the success bucket, and the epic renders that blocked/skipped branch's step
|
|
347
|
+
(`state = blocked`/`failed`), never `Done` — keeping `blocked`/`skipped` non-success terminals
|
|
348
|
+
consistent with the per-cell rule. So the all-terminal rule's `else done` is evaluated against a **shape-scoped
|
|
349
|
+
success predicate**: for a feature/graph cell, a `converged` branch counts as `done`; for the **epic**
|
|
350
|
+
aggregate, only `delivery = landed` satisfies the success bucket — an all-`converged` (review-only) epic
|
|
351
|
+
renders as its least-advanced *converged* step, **not** `Done`, so a review-only epic is never reported
|
|
352
|
+
as landed. **Concretely for the renderer:** the epic projection does **not** reuse the per-feature
|
|
353
|
+
`deriveStage` success-collapse (which folds `converged` → `Done`/`ok`, `app/featureReadModel.ts:73-95`);
|
|
354
|
+
its `activeField` is the **least-advanced slice cell's** `STAGE_KEYS` bracket and its `state` is that
|
|
355
|
+
cell's `pipeline` render state (`ok`/`blocked`/`failed`/in-progress), and the `Done` bucket is entered
|
|
356
|
+
**only** when `deriveDelivery` reports `delivery = landed` (all slice PRs merged) — never on `converged`
|
|
357
|
+
alone. So an all-`converged` epic pins `activeField` to the converged slices' shared bracket with
|
|
358
|
+
`state = ok` (resolved-not-landed), and renders `Done` only once every slice has merged. This `Done`
|
|
359
|
+
bucket is scoped to **integration-branch landing**: for a custom-base (`epic/*`) epic, `delivery = landed`
|
|
360
|
+
means every slice merged onto the integration branch, after which `pollPromotion` (#299,
|
|
361
|
+
`app/promotion.ts`, `app/service.ts`) opens the final `epic/* → <default>` promotion PR and tracks it via
|
|
362
|
+
`promotion_state` (`ready`/`open`/`promoted`). The §4b stepper **intentionally ends at integration-branch
|
|
363
|
+
landing** and does *not* fold that downstream promotion into cell success — a landed epic whose promotion
|
|
364
|
+
PR is still `open` reads `Done` on the delivery stepper, with promotion surfaced by the epic card's
|
|
365
|
+
existing `promotion_state`, not the stepper.
|
|
366
|
+
|
|
367
|
+
**Cross-instance correlation, because composed cells are child instances by default.** engine-core's
|
|
368
|
+
`CallActivity` spawns a **distinct child process instance** and links it to the parent
|
|
369
|
+
(`engine-core/src/model.rs`); `inline_call_activities` (embedded-subprocess, one flat instance) is an
|
|
370
|
+
**opt-in** transform, not the default. So a §2 cell composition is a **parent + child instances**
|
|
371
|
+
unless the composition explicitly opts into inlining — and the broker's `/v2/process-instances/search`
|
|
372
|
+
has been observed returning null parent/root keys (#464), meaning a filter on the parent key alone
|
|
373
|
+
cannot be assumed to see a child cell's elements. §4b therefore requires an explicit choice at S4:
|
|
374
|
+
either compose cells with `inline_call_activities` (keeping one flat instance, so the element query
|
|
375
|
+
under one key suffices) **or** correlate across child instances by the canonical unit key. This is a
|
|
376
|
+
named design decision, not a settled fact.
|
|
377
|
+
|
|
378
|
+
**The binding, not the engine, is the current limiter — and that is the claim to retire.** The
|
|
379
|
+
`@nanobpm/urban` `EngineClient` binding nwf consumes surfaces only `searchProcessInstances` +
|
|
380
|
+
`searchUserTasks` today, so nwf cannot *yet* read the element-instance model the engine already serves.
|
|
381
|
+
This is exactly why the two existing derivations are workarounds: `pollDeliveryGraphPhase`
|
|
382
|
+
(`app/deliveryGraphRun.ts`) sees only **user-task** parks (missing job/message parks and active
|
|
383
|
+
elements), and epic's `epic_phase` (`app/epicPhase.ts`, nano-ide#266) projects from **write-provenance**
|
|
384
|
+
(each worker stamps its own `job.elementId`) *because* a live "furthest element" query wasn't surfaced.
|
|
385
|
+
The enabling upstream step is to **surface `searchElementInstances` / wait-states on the `EngineClient`
|
|
386
|
+
binding** (nano-ide / urban), after which both workarounds collapse into one live element-instance
|
|
387
|
+
projection over the cell axis.
|
|
388
|
+
|
|
389
|
+
**v1 ships on what's correlated today, at lifecycle-stage fidelity; the element-instance query sharpens
|
|
390
|
+
it to per-cell.** Until the binding surfaces the element model, §4b's projection is derivable *now* only
|
|
391
|
+
from aggregate/work-table state, **process-instance lifecycle state** (`searchProcessInstances` —
|
|
392
|
+
`COMPLETED`/`TERMINATED`, which `deriveDeliveryPhase` folds to `done`/`failed`), and user-task parks —
|
|
393
|
+
and that is **lifecycle-stage, not per-cell, even
|
|
394
|
+
for feature**: `deriveStage` collapses a token parked in a readiness-probe service/timer loop, or an
|
|
395
|
+
active `implement-task`, all to `Implementing` while `feature_runs.status` stays `running` and no user
|
|
396
|
+
task is open. It is coarser still for the other two, for reasons that must be stated *separately*:
|
|
397
|
+
|
|
398
|
+
- **Delivery graph:** `deriveDeliveryPhase` (`app/deliveryGraphRun.ts`) returns a generic `Running`
|
|
399
|
+
with **no node id** for an active `agent` / `wait` / `connector` node when no human task is open, and
|
|
400
|
+
`delivery_graph_runs` stores no current node.
|
|
401
|
+
- **Epic:** `record-plan` is classified **Reviewing** (`app/epicPhase.ts`), not Planning, so the
|
|
402
|
+
*initial* Planning phase carries only the `plans` row — `plan_tasks` rows first become available once
|
|
403
|
+
**Reviewing** starts, where `record-plan` *does* write them, so from Reviewing on it is not literally
|
|
404
|
+
row-less — but **no field exposes the current
|
|
405
|
+
pre-PR process position** (which plan/review activity is live): there is no PR row, and while the
|
|
406
|
+
`plan-review-decision` user-task park *is* a real open user-task signal for the Reviewing decision
|
|
407
|
+
point, no field surfaces the **complete activity position** (which plan/review step is live) — the
|
|
408
|
+
initial Planning phase and the running plan/review work between decision parks remain unobservable.
|
|
409
|
+
Only the write-time `epic_phase` stamp (`app/epicPhase.ts`) and the plan/PR rollups
|
|
410
|
+
observe *where* it is. `deriveDeliveryPhase` is the delivery-graph projection only and provides no
|
|
411
|
+
evidence for epic resolution.
|
|
412
|
+
|
|
413
|
+
So v1's scope is deliberately bounded: the one derivation + `pipeline` renderer covers **feature and
|
|
414
|
+
delivery-graph at lifecycle-stage fidelity**, rendering any unobservable node as an **explicitly coarse**
|
|
415
|
+
in-flight step. Because `activeField` must resolve to a *configured* pipeline stage (`STAGE_KEYS` runs
|
|
416
|
+
`Requested`…`Done`, with no `In flight` key), S7's canonical behaviour for that coarse case is **fixed
|
|
417
|
+
here, not left to the implementer**: **derive a configured key statelessly from the current coarse
|
|
418
|
+
inputs on every pass** — map the run's `status`/`phase` to the corresponding `STAGE_KEYS` key (e.g. a
|
|
419
|
+
`running` feature *with no PR key* to `Implementing`, marked in-progress — preserving `deriveStage`'s
|
|
420
|
+
input precedence, where the `pr_key`/`opened` arm runs **before** the `running` arm, so a `running`
|
|
421
|
+
feature whose `pr_key` is set already derives `PR open`, not `Implementing`; `app/stage.ts`,
|
|
422
|
+
`app/featureReadModel.ts:77-85`) rather than remembering a prior key the read
|
|
423
|
+
model does not persist, so the projection survives a restart, no new stage is invented, and no
|
|
424
|
+
renderer/axis change is needed. (This stateless per-pass derivation is the **resolved** S7 policy and
|
|
425
|
+
**supersedes** any earlier "hold the aggregate at its last observed configured stage" phrasing — the read
|
|
426
|
+
model persists no such key, so there is nothing to hold; a coarse run is recomputed from current inputs
|
|
427
|
+
every pass.) Adding a dedicated `In flight` key to the canonical axis is
|
|
428
|
+
**explicitly rejected** for v1 — it would fork the stage vocabulary across surfaces; the S7 rollout
|
|
429
|
+
below binds this same **stateless coarse-key** rule, so independent S7 implementations cannot diverge on
|
|
430
|
+
the stage axis or `activeField`. For
|
|
431
|
+
a **first observation with no prior key** (a freshly `running` `delivery_graph_runs` row carries only
|
|
432
|
+
`phase = "Running"`, no stored lifecycle key to hold), S7 must pin a deterministic **initial**
|
|
433
|
+
`STAGE_KEYS` value from a **status-specific** map — a `running` graph, whose dispatch has begun, maps to
|
|
434
|
+
`Implementing` (not the `Requested` head), while the pre-dispatch case below maps to the pre-run initial
|
|
435
|
+
key (literally **`Requested`**, `STAGE_KEYS[0]`) — so the scalar `activeField` is never undefined. A
|
|
436
|
+
**terminal** row is pinned the same way so it can never render an undefined/invalid active stage: a
|
|
437
|
+
`done` graph (phase `Completed`) maps to the `STAGE_KEYS` **tail** (`Done`) with `state = ok`, while a
|
|
438
|
+
`failed`/`abandoned` graph (phase `Failed`) maps to that same tail bracket with `state = failed` —
|
|
439
|
+
terminal status settles the `activeField` outright rather than leaving it unset. The same rule must cover the **`awaiting-approval`** rows
|
|
440
|
+
`delivery-graphs.page.json` filters into the active grid: their schema leaves `process_key` **NULL**
|
|
441
|
+
(no engine instance yet) and the **door seeds `phase = "Awaiting approval"` at write time** (migration
|
|
442
|
+
058 only *creates* the `delivery_graph_runs` table — it seeds no rows and assigns no `phase`/`process_key`;
|
|
443
|
+
those come from the run builder/dispatch path), so they have no observable
|
|
444
|
+
element and no prior lifecycle key. **`awaiting-approval` is a legacy/reserved status — no longer
|
|
445
|
+
produced** (issue #460 moved dispatch to an operator action; the current producer claims a run directly
|
|
446
|
+
as `running`, `app/deliveryGraphDispatch.ts:104`), so this is only a **fallback for old rows**, not a
|
|
447
|
+
current pre-dispatch path — S7 must **not** reintroduce a removed approval flow. Those retained rows map
|
|
448
|
+
to that same deterministic **initial** pre-run
|
|
449
|
+
`STAGE_KEYS` value, literally **`Requested`** (they are dispatch-pending, before `Implementing`), not leave their `activeField`
|
|
450
|
+
undefined. It
|
|
451
|
+
must never fabricate a specific cell position it cannot observe. The **epic pipeline is deferred to S8**:
|
|
452
|
+
because its pre-PR position is unobservable from S7 inputs, epic keeps its `epic_phase` **text cell** as
|
|
453
|
+
an explicit, retained **second source** (write-provenance) until the element query lands — rather than
|
|
454
|
+
fabricating a derived step or promoting `epic_phase` to a permanent stepper source. When the binding
|
|
455
|
+
lands the element query (S8), the projection swaps its park/position source for the live engine element
|
|
456
|
+
instance — sharpening feature + delivery-graph to per-cell and bringing epic onto the same `pipeline` —
|
|
457
|
+
**preserving the step axis and renderer, and using whatever parent/child correlation strategy S4
|
|
458
|
+
selects** — retiring *both* the `epic_phase` write-time stamp and the user-task-only delivery-graph poll
|
|
459
|
+
into one derivation. **"Per-cell" here means per-cell *source fidelity*, rendered onto the existing
|
|
460
|
+
collapsed lifecycle axis — not a new per-cell step axis.** S8 sharpens the *source* the projection reads
|
|
461
|
+
(a live engine element-instance identifies exactly which cell a token occupies) while the rendered step
|
|
462
|
+
still **collapses that cell into the existing six `STAGE_KEYS` brackets** per §4b's cell→step mapping
|
|
463
|
+
(inserted `wait` / `human` / `escalation` cells fold into an existing bracket, §217-232); the static
|
|
464
|
+
six-stage `pipeline` `stages` array and its renderer are **preserved unchanged**. S8 does **not**
|
|
465
|
+
introduce a per-cell stages array or a distinct step per cell — a distinct cell can be *observed* as the
|
|
466
|
+
active source but is *displayed* at its collapsed lifecycle key. (Promoting cells to first-class steps
|
|
467
|
+
would be a separate axis-contract change, out of scope for S8.)
|
|
468
|
+
|
|
190
469
|
### 5. Preserve — the static-vs-adaptive execution axis (do NOT bundle it)
|
|
191
470
|
|
|
192
471
|
This ADR consolidates the *representation*, not the *execution strategy*. ADR 0005's deliberate
|
|
@@ -211,9 +490,25 @@ their topology is produced. Unifying that axis is explicitly out of scope here.
|
|
|
211
490
|
table-to-VIEW slice requires a **separately designed recovery/compatibility migration**, not a plain
|
|
212
491
|
revert.
|
|
213
492
|
- **Cost.** A backfill/migration for `delivery_units`; a one-time extraction of the shared cells; and
|
|
214
|
-
the process slices are sequenced behind the (now-live) engine-wasm unlock. No
|
|
215
|
-
intended — this is a representation consolidation, guarded by parity tests against
|
|
216
|
-
and by the deploy+run engine tests.
|
|
493
|
+
the process slices are sequenced behind the (now-live) engine-wasm unlock. No **process/data-semantic**
|
|
494
|
+
behaviour change is intended — this is a representation consolidation, guarded by parity tests against
|
|
495
|
+
the existing VIEWs and by the deploy+run engine tests. (The rendered cell *does* change for operators —
|
|
496
|
+
S7 turns the delivery-graph `phase` text cell into a `pipeline`, and S8 the epic phase cell; that
|
|
497
|
+
operator-visible presentation change is intended, per the S7/S8 rollout below.)
|
|
498
|
+
- **One stepper kills the second-order drift.** The unit's *progress projection* — the thing an
|
|
499
|
+
operator reads as "where is this in its lifecycle" — is triplicated the same way the aggregate is
|
|
500
|
+
(feature's `deriveStage`, epic's write-time `epic_phase`, delivery-graph's `pollDeliveryGraphPhase`),
|
|
501
|
+
and only feature renders an actual stepper; epic and delivery-graph render a bare string. §4b collapses
|
|
502
|
+
the three onto one derivation over the cell axis (with an explicit cell→step mapping and per-shape
|
|
503
|
+
correlation — `pr_key`/canonical unit key, since `pull_requests.process_key` is reassigned downstream),
|
|
504
|
+
rendered by one `pipeline` kind. **S7** unifies **feature + delivery-graph** at **lifecycle-stage
|
|
505
|
+
fidelity** with **no engine change** (even feature is not per-cell today — `deriveStage` collapses
|
|
506
|
+
readiness/timer/implement to `Implementing`); the **epic pipeline is deferred to S8**, keeping its
|
|
507
|
+
`epic_phase` text cell meanwhile. The *only* upstream **binding** dependency — surfacing the element-instance query
|
|
508
|
+
on the `@nanobpm/urban` `EngineClient` binding (S8, nano-ide#473), which the engine already serves —
|
|
509
|
+
then sharpens all three to per-cell and retires epic's write-provenance stamp. (This is the only
|
|
510
|
+
*binding/platform* dependency, **not** the sole upstream dependency: S8 additionally depends on S4's
|
|
511
|
+
inline-vs-child/correlation decision (#464) — #473 is necessary, not sufficient; see the S8 rollout.)
|
|
217
512
|
|
|
218
513
|
## Rollout (see #464 for the live checklist)
|
|
219
514
|
|
|
@@ -251,6 +546,145 @@ deployment-runtime prerequisite noted above), not on #416 alone.
|
|
|
251
546
|
merge (unit → base branch; graph → `main`).
|
|
252
547
|
- **S6 · compiler emits calls** — `deliveryGraphCompiler` references shared cells instead of inlining
|
|
253
548
|
per-node copies.
|
|
549
|
+
- **S7 · one derived stepper — v1 on today's surface** (Decision §4b) — define the **cell → step
|
|
550
|
+
mapping** (seeded from `STAGE_KEYS`, but owning the canonical definition — `STAGE_KEYS` mixes
|
|
551
|
+
lifecycle states with cells), derive current-step by correlating the aggregate/work-table state
|
|
552
|
+
**per shape**: for a **feature**, the terminal-folded `feature_runs__tracking` `derived_status`
|
|
553
|
+
(the canonical effective-status VIEW the projection reads — `app/featureReadModel.ts`, *not* the raw
|
|
554
|
+
`feature_runs.status`, which a terminated run can leave frozen at `running`/`escalated`)/`pr_key`/flags + the
|
|
555
|
+
member PR's terminal-folded **`pull_requests__tracking.derived_status`** (the ADR-0065 tracking VIEW that
|
|
556
|
+
is the *direct* source — `app/lineage.ts:382-394`; *not* `pollFeatureDelivery`, which is the **consumer**
|
|
557
|
+
that projects that state into `feature_runs`), joined by **`pr_key`** (*not* a naïve
|
|
558
|
+
`process_key` join, which is reassigned downstream); for a **delivery graph** — which has **no
|
|
559
|
+
`pr_key`** — its `delivery_graph_runs` row, its downstream PRs (read through the **same
|
|
560
|
+
`pull_requests__tracking.derived_status`** tracking VIEW, so a terminated convergence that leaves base
|
|
561
|
+
`status = 'converging'` while `derived_status = 'abandoned'` is *not* held in the live frontier —
|
|
562
|
+
`app/lineage.ts:382-394`) correlated through the run's **lineage
|
|
563
|
+
root mapping** (`app/lineage.ts` `collectRootPrs` over `pull_requests.root_request_key`), and engine
|
|
564
|
+
parks by the run's own `process_key`. Note the root mapping is **not** a single clean
|
|
565
|
+
`root_request_key = run_key` join for every PR, and the per-node correlation **differs by node kind**,
|
|
566
|
+
so S7 must **persist a run-level root rollup** — a stored run→node/PR mapping, or a canonical run root
|
|
567
|
+
threaded through every PR-producing node — **not** merely *document* the existing per-node keys (which
|
|
568
|
+
alone leaves the **agent** case below unattributed). **This persisted rollup must land on the canonical
|
|
569
|
+
aggregate, sequenced around S2's ownership — not on the legacy `delivery_graph_runs` row S2/S3 retire.**
|
|
570
|
+
Because S2 repoints `delivery_graph_runs` reads to `delivery_units`-derived VIEWs/rows and S3 moves the
|
|
571
|
+
writers off the legacy tables, the mapping's home is either (i) a column/relation on **`delivery_units`**
|
|
572
|
+
(added, dual-written, and backfilled under S2's expand/contract order — §508-519 — so the derived VIEW
|
|
573
|
+
exposes it), or (ii) **threaded into the PRs themselves** as each PR-producing node's `root_request_key`
|
|
574
|
+
(`app/service.ts`'s `submitPr`, line 503), which survives the legacy-table retirement. Storing it *only* on
|
|
575
|
+
`delivery_graph_runs` would be orphaned when that table becomes a derived VIEW, so S7 is **sequenced
|
|
576
|
+
after S2 establishes `delivery_units` ownership** (or takes the PR-threaded option). Rather than assume
|
|
577
|
+
one join: a **connector** PR roots by the connector's
|
|
578
|
+
*effective* `dedupeKey` — the author-supplied `connector.dedupeKey`, else the graph-derived
|
|
579
|
+
`<processInstanceKey>:<elementId>` (`app/deliveryConnector.ts` `connectorDedupeKey`) — threaded into
|
|
580
|
+
`submitPr` as its `rootRequestKey`; whereas an **agent** PR has **no `dedupeKey` at all** (the runner
|
|
581
|
+
seeds only `jobType`/`appendPrompt`/`timeout` for an `agent` node, `app/deliveryGraphCompiler.ts`
|
|
582
|
+
`ioMappingLines`, never a dedupe/root key), so it **self-roots on its own `pr_key`** (`submitPr`'s
|
|
583
|
+
`effectiveRoot = rootRequestKey ?? existing.root_request_key ?? pr_key`) unless the run explicitly
|
|
584
|
+
threads a root. The run-level rollup S7 defines must therefore attribute the **agent** case explicitly —
|
|
585
|
+
it cannot lean on the connector's `dedupeKey` fallback, which agent nodes never carry. The rollup also
|
|
586
|
+
**defines its precedence over `run.status`**: correlated-PR state **overrides** the raw run status for
|
|
587
|
+
the live frontier — a `running` run with any member PR still in flight reads as **`Converging`**, not
|
|
588
|
+
`Implementing` (matching the shipped `deliveryOriginStage`, `app/lineage.ts:332-336`) — whereas a
|
|
589
|
+
**terminal** run status settles the step **without** waiting on member PRs (`done` → `Done`). So `done`
|
|
590
|
+
does **not** block on an open PR, but a live `running` frontier must **not** mask PR-open/converging work
|
|
591
|
+
behind `Implementing`. **Park-versus-PR precedence:** when both are live at a `running` frontier — an open
|
|
592
|
+
engine park **and** a member PR in flight — the **human park wins** (the branch reads as its park step,
|
|
593
|
+
e.g. `Awaiting approval`, over the PR-derived `Converging`), because an open user-task gate is the branch's
|
|
594
|
+
current actionable frontier and the PR work sits behind it; absent a park, PR state drives the step as
|
|
595
|
+
above. Because `Awaiting approval` is a park **label**, not a `STAGE_KEYS` value, it can **not** be the
|
|
596
|
+
pipeline's scalar `activeField`: a parked branch pins its `activeField` to the frontier's current
|
|
597
|
+
`STAGE_KEYS` bracket per the coarse-key rule (a pre-PR `running` frontier → `Implementing`, an
|
|
598
|
+
`awaiting-approval`/pre-dispatch row → `Requested`), and the human-readable `Parked on human node:
|
|
599
|
+
<label>` text rides the **separate companion park-label field** — so the pipeline never receives a
|
|
600
|
+
non-`STAGE_KEYS` `activeField` while the actionable park label is still surfaced alongside it.
|
|
601
|
+
Engine parks correlate via `searchUserTasks({ processInstanceKey: run.process_key })`, which is
|
|
602
|
+
correct **for today's inlined graphs** (the compiler inlines subProcesses into one flat instance); once
|
|
603
|
+
S4's `callActivity` composition puts a human cell in a **child** instance, this parent-key query would
|
|
604
|
+
miss it, so that step is bound to the S4 inline-vs-child decision (correlate child instances, or keep
|
|
605
|
+
the graph inlined) — it is not a silent promise. Reduce any parallel frontier to the
|
|
606
|
+
**least-advanced active branch** and **promote the `pipeline` stepper
|
|
607
|
+
kind** (feature-only today) onto **every** delivery-graph surface — the list page, the detail
|
|
608
|
+
page (`pages/delivery-graph-detail.page.json:95`), **and** the *Active Delivery Graphs* grid on
|
|
609
|
+
`pages/overview.page.json:217` — each of which today renders `delivery_graph_runs.phase` as a
|
|
610
|
+
plain-text Phase column, so S7 does not leave any of those views on a second renderer while §4b claims the
|
|
611
|
+
surface uses the shared stepper. Promoting the `pipeline` kind must **not drop** the actionable
|
|
612
|
+
`Parked on human node: <label>` detail the plain-text Phase column carries today: S7 retains it as a
|
|
613
|
+
companion derived **park-label** field alongside the stepper on **all three** surfaces — the list and
|
|
614
|
+
*Active Delivery Graphs* grids (which have no other inline park detail) **and the detail page**, whose
|
|
615
|
+
`phase_node_id` field carries only the node **ID**, not the human-readable `Parked on human node:
|
|
616
|
+
<label>` text its plain-text `phase` cell shows today, so promoting the `pipeline` kind on the detail
|
|
617
|
+
page would otherwise lose that actionable label too. Because `delivery_graph_runs` stores **no
|
|
618
|
+
stage column** (only `phase`/park metadata, whose values such as `Running` are *not* `STAGE_KEYS`), S7
|
|
619
|
+
supplies the pipeline's `activeField`/`state` for the graph from a **read model/VIEW over
|
|
620
|
+
`delivery_graph_runs`** that maps `phase`/park metadata onto the `STAGE_KEYS` axis (the stateless
|
|
621
|
+
coarse-key rule) — it assumes no stored `activeField` column and defines the mapping columns explicitly,
|
|
622
|
+
so the page renders a valid configured step rather than a raw `phase` string.
|
|
623
|
+
This needs **no** engine-binding change, but ships at **lifecycle-stage fidelity only — for feature
|
|
624
|
+
*and* delivery-graph alike**: even feature is not truly per-cell today (`deriveStage` collapses a
|
|
625
|
+
readiness-probe/timer park or an active `implement-task` all to `Implementing`), and for a
|
|
626
|
+
delivery-graph node running with no open user task (`deriveDeliveryPhase` returns generic `Running`, no
|
|
627
|
+
node id) the stepper renders the configured coarse key derived statelessly from run `status`/`phase` (a *configured* stage — never a
|
|
628
|
+
fabricated cell position or an unconfigured `activeField` label; see the coarse-case rule in §4b). **The
|
|
629
|
+
least-advanced-*active* frontier reduction (§4b) is therefore *defined* at S7 but not yet *computable*
|
|
630
|
+
from this source:** `delivery_graph_runs` stores a **single** `phase`/`phase_node_id` per run, not a
|
|
631
|
+
per-branch topology, so a VIEW over it cannot compare branch advancement — at S7 the graph collapses to
|
|
632
|
+
that one coarse run-level step, and the genuine per-branch frontier reduction is deferred to S8's
|
|
633
|
+
element-instance/topology read model (a single-track feature is unaffected). The **epic pipeline is out of S7 scope**: epic's pre-PR position is unobservable from these
|
|
634
|
+
inputs, so epic keeps its write-provenance `epic_phase` **text cell** as an explicit retained second
|
|
635
|
+
source until S8 (below). The mapping can begin as soon as S4 names the cells; the `pipeline` render
|
|
636
|
+
binding for feature + delivery-graph can start immediately.
|
|
637
|
+
- **S8 · surface the element-instance query → retire the epic write-stamp** (Decision §4b) — the
|
|
638
|
+
slice that depends on an upstream binding change, tracked as **`nanobpm/nano-ide#473`** — but
|
|
639
|
+
#473 is **necessary, not sufficient**: it surfaces the element read model keyed by a *process-instance
|
|
640
|
+
key*, while the current process-instance read model has been observed returning **null parent/root
|
|
641
|
+
keys** (#464), so under S4's default `callActivity` child instances #473 alone cannot traverse from a
|
|
642
|
+
run to its child cells' elements. S8 therefore also depends on S4's inline-vs-child decision (inline
|
|
643
|
+
the graph so one flat instance suffices, **or** resolve the child/root correlation, tracked with #464)
|
|
644
|
+
— #473 is not the sole upstream dependency. Nano's
|
|
645
|
+
Rust engine already serves the element-instance read model (`POST /v2/element-instances/search`
|
|
646
|
+
`searchElementInstances` + element-instance **wait-states** — active elements and **job/message**
|
|
647
|
+
parks, not only user tasks), but the `@nanobpm/urban` `EngineClient` binding nwf consumes surfaces only
|
|
648
|
+
`searchProcessInstances` + `searchUserTasks`. Surface `searchElementInstances` / wait-states on the
|
|
649
|
+
binding (upstream in nano-ide / urban, **nano-ide#473**), then swap the S7 projection's park/position
|
|
650
|
+
source for the live engine element instance — **preserving the step axis and the renderer, and using
|
|
651
|
+
whatever parent/child correlation strategy S4 selects**: if S4 chooses cross-instance correlation
|
|
652
|
+
(default `callActivity` child instances), S8 must also extend the element lookup to resolve the
|
|
653
|
+
child/root instance keys, so the "no change to the correlation key" is scoped to the axis and renderer,
|
|
654
|
+
not to a fixed single-instance join. This is
|
|
655
|
+
what lets epic's **Planning** phase and the **non-parked** part of **Reviewing** — which run inside
|
|
656
|
+
`plan-fanout.bpmn` before any PR exists — Planning has no `plan_tasks` work-table row (its `plans`
|
|
657
|
+
aggregate row *does* exist, carrying `status`/`process_key`, so the gap is a missing per-activity field,
|
|
658
|
+
not a missing row), and while pre-PR Reviewing
|
|
659
|
+
*does* carry `plan_tasks` rows (`record-plan` writes them), **no field exposes the currently executing
|
|
660
|
+
plan/review activity** (Reviewing *is* partly visible via the real `plan-review-decision` user task,
|
|
661
|
+
but Planning and the running review work are not), today
|
|
662
|
+
knowable only from write-provenance — become a pure read-model derivation, bringing epic onto the same
|
|
663
|
+
`pipeline`, retiring the `epic_phase` write-time stamp (`app/epicPhase.ts`, nano-ide#266 — **including
|
|
664
|
+
its genesis write in `startPlan`, `app/plan.ts`**, which stamps `epic_phase` for fresh/replanned
|
|
665
|
+
epics, so the retirement must move or drop that write too or those epics still depend on the retired
|
|
666
|
+
source) and folding
|
|
667
|
+
the user-task-only `pollDeliveryGraphPhase` into one live projection. **Ownerships this retirement
|
|
668
|
+
must carry forward, not silently drop:** (a) `app/lineage.ts` consumes `plans.epic_phase` to project
|
|
669
|
+
`epicPhaseLabel` and writes `pull_requests.epic_phase_label`, shown on the home/convergence surfaces —
|
|
670
|
+
so S8's replacement contract must supply that label from the derived pipeline (a derived
|
|
671
|
+
compatibility field) or update the lineage projection, or it either breaks the display or leaves a
|
|
672
|
+
second phase source; and (b) `pollDeliveryGraphPhase` is not only a park projection — it also owns the
|
|
673
|
+
`COMPLETED → done` / `TERMINATED → failed` terminal reconciliation for graphs (`app/service.ts`,
|
|
674
|
+
since a delivery graph has no spine worker to write its own terminal row), so S8 must **retain or move**
|
|
675
|
+
that reconciliation into the live projection, or completed graphs stay wedged in the active `running`
|
|
676
|
+
set; and (c) `app/lineage.ts` also reads `delivery_graph_runs.phase` to form `LineageThread.stageLabel`
|
|
677
|
+
and persists `lineage_threads.stage_label` (shown on delivery-lineage / home rows), so S8 must supply
|
|
678
|
+
that phase narrative from the derived projection or update the lineage read too, else those rows lose
|
|
679
|
+
their phase label; and (d) **four field bindings across three pages** read the `epic_phase` field, but **not all read
|
|
680
|
+
raw `plans`**: the epic index grid (`pages/epic.page.json:119`) and the epic-detail page
|
|
681
|
+
(`pages/epic-detail.page.json:99,146`) bind it through the **`plan_read_model` VIEW**
|
|
682
|
+
(`pages/epic.page.json:102`, `pages/epic-detail.page.json:95,140`; the VIEW projects `pl.epic_phase`,
|
|
683
|
+
`db/migrations/083_plan_read_model_declare_once.sql`), while only the nested Epic grid on the Home page
|
|
684
|
+
(`pages/home.page.json:284`) binds **raw `plans`** directly — so
|
|
685
|
+
retiring the column without repointing these bindings to the pipeline/read-model field
|
|
686
|
+
renders a missing field on the epic index, epic-detail, and Home pages; S8's checklist must update
|
|
687
|
+
**both** the `plan_read_model` VIEW (or its derived field) **and** the raw-`plans` Home binding, not just raw `plans` references.
|
|
254
688
|
|
|
255
689
|
## Non-goals / deferred
|
|
256
690
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.139.
|
|
3
|
+
"version": "0.139.3",
|
|
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",
|