@nanobpm/nano-workforce 0.143.0 → 0.145.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 +12 -0
- package/app/deliveryGraphReadModel.test.ts +392 -0
- package/app/deliveryGraphReadModel.ts +166 -0
- package/app/deliveryGraphRun.test.ts +1 -1
- package/app/epicPhase.test.ts +86 -3
- package/app/epicPhase.ts +98 -4
- package/app/pollEpicPhase.test.ts +159 -0
- package/app/service.ts +71 -7
- package/app/stepAxis.test.ts +142 -0
- package/app/stepAxis.ts +217 -0
- package/db/migrations/087_delivery_graph_read_model.sql +75 -0
- package/package.json +2 -2
- package/pages/delivery-graph-detail.page.json +18 -2
- package/pages/delivery-graphs.page.json +25 -8
- package/pages/overview.page.json +18 -2
- package/scripts/pages-contract.test.ts +15 -11
- package/workers/record-plan/worker.ts +0 -6
- package/workers/record-results/worker.ts +7 -8
- package/workers/record-wave/worker.test.ts +6 -5
- package/workers/record-wave/worker.ts +5 -21
- package/workers/select-wave/worker.test.ts +4 -3
- package/workers/select-wave/worker.ts +15 -29
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
// and, crucially, so a later wave's `dependsOn` can reference the PR keys earlier waves produced.
|
|
16
16
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
17
17
|
import { appendEntry } from "../../app/blackboard.ts";
|
|
18
|
-
import { EPIC_PHASE, implementingPhase } from "../../app/epicPhase.ts";
|
|
19
18
|
import { fetchPrFiles, fetchPrHead } from "../../app/github.ts";
|
|
20
19
|
import { deriveExclusions, recordExclusions } from "../../app/mergeExclusion.ts";
|
|
21
20
|
import { loadMergeProtocol } from "../../app/mergeProtocol.ts";
|
|
@@ -302,25 +301,11 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
302
301
|
const nextWave = stillPendingCurrentWave ? currentWave : currentWave + 1;
|
|
303
302
|
const hasMoreWaves = stillPendingCurrentWave || nextWave < waveCount;
|
|
304
303
|
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
// Operator-visibility wave progress (current_wave / wave_label) was RETIRED as a stored projection
|
|
311
|
-
// (epic #412) — it is now derived from `plan_tasks` by the `plan_wave_label` / `plan_read_model`
|
|
312
|
-
// VIEWs (060/061), so this worker no longer denormalises it (select-wave no longer writes it
|
|
313
|
-
// either). `projectedCurrentWave` above is not persisted; it only feeds the phase derivation.
|
|
314
|
-
|
|
315
|
-
// Domain-phase projection (#261): the wave landed — stamp the phase the epic is ENTERING next,
|
|
316
|
-
// which is data-dependent here (unlike the structural spine writers). A trial merge runs → Trial
|
|
317
|
-
// merging; another wave follows → Implementing (next wave n/t); otherwise the finalizer runs →
|
|
318
|
-
// Finalizing (record-results then advances to the Dispatched terminal).
|
|
319
|
-
const epicPhase = runTrialMerge
|
|
320
|
-
? EPIC_PHASE.TRIAL_MERGING
|
|
321
|
-
: hasMoreWaves
|
|
322
|
-
? implementingPhase(projectedCurrentWave, waveCount)
|
|
323
|
-
: EPIC_PHASE.FINALIZING;
|
|
304
|
+
// The epic's domain phase is no longer stamped here (S8, #542) — it is a pure read-model derivation
|
|
305
|
+
// off the live element-instance model (`pollEpicPhase` → `deriveEpicPhaseLive`, app/epicPhase.ts),
|
|
306
|
+
// which reads the live token position (a running `implement` fan-out, `trial-merge`, or the
|
|
307
|
+
// finalizer) directly rather than this worker projecting the phase it is ABOUT to enter. This write
|
|
308
|
+
// now only arms the durable wave-merge barrier marker.
|
|
324
309
|
|
|
325
310
|
// Wave-merge barrier: when another wave follows, park the plan-fanout instance at the
|
|
326
311
|
// `wait-wave-merged` catch event until THIS wave's opened PRs have MERGED (not merely opened).
|
|
@@ -334,7 +319,6 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
334
319
|
try {
|
|
335
320
|
await plans(app.data).update(planKey, {
|
|
336
321
|
gate_wave: hasMoreWaves ? currentWave : null,
|
|
337
|
-
epic_phase: epicPhase,
|
|
338
322
|
updated_at: ts,
|
|
339
323
|
});
|
|
340
324
|
} catch (err) {
|
|
@@ -83,9 +83,10 @@ test("select-wave dispatches the active wave without writing wave-progress colum
|
|
|
83
83
|
assertEquals(plans[0].current_wave, undefined);
|
|
84
84
|
assertEquals(plans[0].wave_count, undefined);
|
|
85
85
|
assertEquals(plans[0].wave_label, undefined);
|
|
86
|
-
// Domain-phase projection (#
|
|
87
|
-
//
|
|
88
|
-
|
|
86
|
+
// Domain-phase projection is no longer stamped by this worker (S8, #542) — the epic phase is now a
|
|
87
|
+
// pure read-model derivation off the live element-instance model (`pollEpicPhase`, app/service.ts),
|
|
88
|
+
// so select-wave introduces no `epic_phase` onto the plan row.
|
|
89
|
+
assertEquals(plans[0].epic_phase, undefined);
|
|
89
90
|
});
|
|
90
91
|
|
|
91
92
|
test("select-wave captures the preflight's resolvedArtifacts onto plans.bound_artifacts (#292 S4)", async () => {
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
// immediately (the same 0-task path the flat fan-out already relied on).
|
|
17
17
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
18
18
|
import type { CapabilityNeed } from "../../app/capabilityNeed.ts";
|
|
19
|
-
import { deriveEpicPhase } from "../../app/epicPhase.ts";
|
|
20
19
|
import { plans, planTaskDeps, planTaskNeeds, planTasks } from "../../app/plan.ts";
|
|
21
20
|
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
22
21
|
|
|
@@ -52,20 +51,6 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
52
51
|
const statusById = new Map<string, string>();
|
|
53
52
|
for (const r of rows) statusById.set(r.task_id, r.status);
|
|
54
53
|
|
|
55
|
-
// Operator-visibility projection (issue #137): mark this as the wave the fleet is now
|
|
56
|
-
// implementing, so the epics-index can show wave X/N at a glance. wave_count is derivable from
|
|
57
|
-
// the levelized rows (max wave + 1), so the "X/N" label stays correct even if a re-levelize
|
|
58
|
-
// changed the total. Best-effort + idempotent (a retry re-writes the same value) and
|
|
59
|
-
// display-only — it must never gate control flow, which stays driven by the process
|
|
60
|
-
// `currentWave`/`waveCount`/`gate_wave` state.
|
|
61
|
-
const waveCount = rows.reduce((m, r) => Math.max(m, r.wave ?? 0), -1) + 1;
|
|
62
|
-
// Domain-phase projection (#261): select-wave dispatches this wave and is the last host write
|
|
63
|
-
// before the write-silent `implement` MI, so it durably marks the implementation phase for the
|
|
64
|
-
// wave it launches — `Implementing (wave n/t)` from the levelize records (job.elementId +
|
|
65
|
-
// current/total waves). A null derivation (element id absent) must not clobber the phase.
|
|
66
|
-
const epicPhase = waveCount > 0
|
|
67
|
-
? deriveEpicPhase(job.elementId, { current: currentWave, total: waveCount })
|
|
68
|
-
: null;
|
|
69
54
|
// Inter-epic gate projection (#292 slice S4): reaching select-wave proves this epic's leading
|
|
70
55
|
// capability PREFLIGHT (S3) already went GREEN, so capture the `resolvedArtifacts` the preflight
|
|
71
56
|
// bound — the exact `pkg@version`s first carrying each producer's awaited capability — onto the
|
|
@@ -76,20 +61,21 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
76
61
|
const boundArtifacts = Array.isArray(job.variables.resolvedArtifacts)
|
|
77
62
|
? job.variables.resolvedArtifacts.filter((v): v is string => typeof v === "string" && v.trim().length > 0)
|
|
78
63
|
: [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
64
|
+
// The epic's Implementing (wave n/t) phase is no longer stamped here (S8, #542) — it is a pure
|
|
65
|
+
// read-model derivation off the live element-instance model (`pollEpicPhase`, app/service.ts), which
|
|
66
|
+
// reads the running `implement` fan-out directly. This write now only carries the inter-epic gate's
|
|
67
|
+
// bound artifacts, so it is skipped entirely when there are none.
|
|
68
|
+
if (boundArtifacts.length > 0) {
|
|
69
|
+
try {
|
|
70
|
+
await plans(app.data).update(planKey, {
|
|
71
|
+
bound_artifacts: JSON.stringify(boundArtifacts),
|
|
72
|
+
updated_at: ts,
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
app.log.error(`select-wave: projecting plan row (bound artifacts) failed for ${planKey}`, {
|
|
76
|
+
err: String(err),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
93
79
|
}
|
|
94
80
|
|
|
95
81
|
const deps = await planTaskDeps(app.data).find({ plan_key: planKey });
|