@nanobpm/nano-workforce 0.105.0 → 0.106.1
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/.github/workflows/ci.yml +5 -0
- package/AGENTS.md +31 -1
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/app/agentCompletion.test.ts +28 -0
- package/app/agentCompletion.ts +14 -2
- package/app/conformance.test.ts +94 -1
- package/app/conformance.ts +89 -0
- package/app/instance-tracking.test.ts +24 -0
- package/app/migration-upgrade-smoke.test.ts +55 -0
- package/app/migrationHeal.test.ts +75 -0
- package/app/migrationHeal.ts +74 -0
- package/app/pollUserTasks.test.ts +37 -0
- package/app/service.ts +40 -0
- package/app/userTasks.test.ts +20 -0
- package/app/userTasks.ts +2 -0
- package/db/migrations/054_conformance_review_tracking.sql +27 -0
- package/nano.app.json +18 -0
- package/package.json +4 -2
- package/pages/tasks.page.json +84 -0
- package/resources/forms/conformance-escalation.form +17 -0
- package/resources/processes/retro.bpmn +74 -13
- package/scripts/check-migrations.test.ts +58 -0
- package/scripts/check-migrations.ts +127 -5
- package/scripts/heal-migration-ledger.ts +49 -0
- package/test/migrations.ts +121 -0
- package/workers/conformance-ack/worker.test.ts +50 -0
- package/workers/conformance-ack/worker.ts +31 -0
- package/workers/conformance-record/worker.test.ts +47 -5
- package/workers/conformance-record/worker.ts +33 -1
|
@@ -209,6 +209,43 @@ test("pollUserTasks: projects a blocked feature run (feature-blocked) with the d
|
|
|
209
209
|
assertEquals(byKey["ut-blocked"].question, "agent gave up: no PR");
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
+
test("pollUserTasks: projects a conformance-escalation ack (issue #216) keyed to the epic, question from summary", async () => {
|
|
213
|
+
// The advisory `retro` process parks on a native `conformance-escalation` user task when the
|
|
214
|
+
// spec-conformance audit found the epic did not cleanly meet its spec. retro is not a delivery
|
|
215
|
+
// aggregate, so its instance is tracked on `plan_conformance` (review_status = 'reviewing'); the
|
|
216
|
+
// poller scans those rows, reads the open ack task from the engine, and projects it under the epic
|
|
217
|
+
// (plan) subject with the audit `summary` as its question. A settled ('reviewed') row is skipped.
|
|
218
|
+
const { data, stores } = memData({
|
|
219
|
+
plan_conformance: [
|
|
220
|
+
{
|
|
221
|
+
plan_key: "o/r#70",
|
|
222
|
+
process_key: "cp-70",
|
|
223
|
+
review_status: "reviewing",
|
|
224
|
+
summary: "slice 2 reduced; auth cache never verified",
|
|
225
|
+
},
|
|
226
|
+
{ plan_key: "o/r#71", process_key: "cp-71", review_status: "reviewed", summary: "all clean" },
|
|
227
|
+
],
|
|
228
|
+
plans: [
|
|
229
|
+
{ plan_key: "o/r#70", status: "done", issue_url: "https://github.com/o/r/issues/70", title: "Ship the cache" },
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
const engine = fakeEngine({
|
|
233
|
+
"cp-70": [{ userTaskKey: "ut-conf", elementId: "conformance-escalation" }],
|
|
234
|
+
"cp-71": [{ userTaskKey: "ut-conf-settled", elementId: "conformance-escalation" }],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await pollUserTasks(data, engine);
|
|
238
|
+
|
|
239
|
+
const byKey = Object.fromEntries((stores.user_tasks ?? []).map((r) => [r.user_task_key, r]));
|
|
240
|
+
assertEquals(Object.keys(byKey), ["ut-conf"]);
|
|
241
|
+
assertEquals(byKey["ut-conf"].element_id, "conformance-escalation");
|
|
242
|
+
assertEquals(byKey["ut-conf"].kind_label, "Conformance review");
|
|
243
|
+
assertEquals(byKey["ut-conf"].subject_type, "plan");
|
|
244
|
+
assertEquals(byKey["ut-conf"].subject_key, "o/r#70");
|
|
245
|
+
assertEquals(byKey["ut-conf"].subject_title, "Ship the cache");
|
|
246
|
+
assertEquals(byKey["ut-conf"].question, "slice 2 reduced; auth cache never verified");
|
|
247
|
+
});
|
|
248
|
+
|
|
212
249
|
test("pollUserTasks: removes a row once its task is no longer open (completed / out-of-band)", async () => {
|
|
213
250
|
const { data, stores } = memData({
|
|
214
251
|
user_tasks: [
|
package/app/service.ts
CHANGED
|
@@ -21,6 +21,11 @@ import {
|
|
|
21
21
|
renderResolvedDepsBrief,
|
|
22
22
|
UnresolvableCapabilityRefError,
|
|
23
23
|
} from "./capabilityNeed.ts";
|
|
24
|
+
import {
|
|
25
|
+
activeConformanceReviews,
|
|
26
|
+
CONFORMANCE_ESCALATION_ELEMENT,
|
|
27
|
+
conformanceEscalationQuestion,
|
|
28
|
+
} from "./conformance.ts";
|
|
24
29
|
import { isUniqueConstraintFence } from "./dbFence.ts";
|
|
25
30
|
import { deriveDelivery, TERMINAL_STATUSES } from "./delivery.ts";
|
|
26
31
|
import { fleetSupportsDurableResume } from "./durableResume.ts";
|
|
@@ -2199,6 +2204,41 @@ export async function pollUserTasks(data: DataLayer, engine: EngineClient) {
|
|
|
2199
2204
|
}
|
|
2200
2205
|
}
|
|
2201
2206
|
|
|
2207
|
+
// Conformance-review acks (`conformance-escalation`) — the advisory `retro` process parks on a
|
|
2208
|
+
// human ack when the spec-conformance audit finds the epic did NOT cleanly meet its spec (issue
|
|
2209
|
+
// #216). retro is not one of the delivery aggregates above, so its instance is tracked on
|
|
2210
|
+
// `plan_conformance` (migration 054): scan each row still `reviewing`, read its open ack task, and
|
|
2211
|
+
// project it keyed to the epic (plan) subject, sourcing the question from the audit's `summary`.
|
|
2212
|
+
for (const review of await activeConformanceReviews(data)) {
|
|
2213
|
+
if (!review.process_key) continue;
|
|
2214
|
+
let tasks: { userTaskKey: string; elementId?: string }[];
|
|
2215
|
+
try {
|
|
2216
|
+
tasks = await engine.openUserTasks({ processInstanceKey: review.process_key });
|
|
2217
|
+
} catch (err) {
|
|
2218
|
+
console.error(`[poller] user tasks (conformance ${review.plan_key}): ${err}`);
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
const plan = await plans(data).get(review.plan_key);
|
|
2222
|
+
for (const t of tasks) {
|
|
2223
|
+
if (t.elementId !== CONFORMANCE_ESCALATION_ELEMENT) continue;
|
|
2224
|
+
push(
|
|
2225
|
+
buildUserTaskRow(
|
|
2226
|
+
{
|
|
2227
|
+
userTaskKey: t.userTaskKey,
|
|
2228
|
+
elementId: CONFORMANCE_ESCALATION_ELEMENT,
|
|
2229
|
+
subjectType: "plan",
|
|
2230
|
+
subjectKey: review.plan_key,
|
|
2231
|
+
subjectTitle: plan?.title ?? null,
|
|
2232
|
+
subjectUrl: plan?.issue_url ?? null,
|
|
2233
|
+
question: conformanceEscalationQuestion(review),
|
|
2234
|
+
processKey: review.process_key,
|
|
2235
|
+
},
|
|
2236
|
+
at,
|
|
2237
|
+
),
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2202
2242
|
const persisted = await userTasks(data).all();
|
|
2203
2243
|
const { inserts, updates, deletes } = reconcileUserTasks(persisted, desired);
|
|
2204
2244
|
for (const row of inserts) await userTasks(data).insert(row);
|
package/app/userTasks.test.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// These are the pure source of truth the poller projects.
|
|
6
6
|
import { test } from "node:test";
|
|
7
7
|
import { assert, assertEquals } from "#test-assert";
|
|
8
|
+
import { CONFORMANCE_ESCALATION_ELEMENT } from "./conformance.ts";
|
|
8
9
|
import type { PlanReview } from "./plan.ts";
|
|
9
10
|
import type { TrialMergeAuditRow } from "./trialMerge.ts";
|
|
10
11
|
import {
|
|
@@ -63,6 +64,25 @@ test("buildUserTaskRow: a blank question / missing url normalises to null", () =
|
|
|
63
64
|
assertEquals(row?.kind_label, "Trial merge");
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
test("buildUserTaskRow: a conformance-escalation projects the 'Conformance review' label (issue #216)", () => {
|
|
68
|
+
const row = buildUserTaskRow(
|
|
69
|
+
{
|
|
70
|
+
userTaskKey: "ut-c",
|
|
71
|
+
elementId: CONFORMANCE_ESCALATION_ELEMENT,
|
|
72
|
+
subjectType: "plan",
|
|
73
|
+
subjectKey: "o/r#7",
|
|
74
|
+
subjectTitle: "Ship the cache",
|
|
75
|
+
question: "slice 2 reduced",
|
|
76
|
+
},
|
|
77
|
+
AT,
|
|
78
|
+
);
|
|
79
|
+
assert(row !== null);
|
|
80
|
+
assertEquals(row?.element_id, "conformance-escalation");
|
|
81
|
+
assertEquals(row?.kind_label, "Conformance review");
|
|
82
|
+
assertEquals(row?.subject_type, "plan");
|
|
83
|
+
assertEquals(row?.question, "slice 2 reduced");
|
|
84
|
+
});
|
|
85
|
+
|
|
66
86
|
test("buildUserTaskRow: an unknown (non-escalation) element yields null — no arbitrary user task leaks", () => {
|
|
67
87
|
const row = buildUserTaskRow(
|
|
68
88
|
{ userTaskKey: "ut-3", elementId: "some-internal-task", subjectType: "plan", subjectKey: "o/r#3" },
|
package/app/userTasks.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// tasks visible; a completed task's row is removed on the next pass when the engine no longer reports
|
|
19
19
|
// it open.
|
|
20
20
|
import type { DataLayer } from "@nanobpm/urban";
|
|
21
|
+
import { CONFORMANCE_ESCALATION_ELEMENT } from "./conformance.ts";
|
|
21
22
|
import { FEATURE_BLOCKED_ELEMENT, FEATURE_ESCALATION_ELEMENT, type FeatureEscalationRow } from "./feature.ts";
|
|
22
23
|
import type { PlanReview } from "./plan.ts";
|
|
23
24
|
import type { TrialMergeAuditRow } from "./trialMerge.ts";
|
|
@@ -74,6 +75,7 @@ export const USER_TASK_KIND_LABELS: Readonly<Record<string, string>> = {
|
|
|
74
75
|
[TRIAL_MERGE_ELEMENT]: "Trial merge",
|
|
75
76
|
[PR_WAIT_ANSWER_ELEMENT]: "PR review",
|
|
76
77
|
[PR_WAIT_MERGE_ANSWER_ELEMENT]: "PR merge",
|
|
78
|
+
[CONFORMANCE_ESCALATION_ELEMENT]: "Conformance review",
|
|
77
79
|
};
|
|
78
80
|
|
|
79
81
|
/** The denormalised context the poller has resolved for an open escalation user task. */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Conformance review — escalation instance tracking (issue #216).
|
|
2
|
+
--
|
|
3
|
+
-- When conformance finds the epic did NOT cleanly meet its spec (a reduced / not-verified item, or a
|
|
4
|
+
-- deviation nobody raised), it escalates to the Tasks inbox as a NON-BLOCKING follow-up: the `retro`
|
|
5
|
+
-- process parks on a native `conformance-escalation` user task until an operator acknowledges it.
|
|
6
|
+
--
|
|
7
|
+
-- The unified inbox (`user_tasks`, 034) is reconciled by `pollUserTasks`, which scans the open user
|
|
8
|
+
-- tasks of each *instance-tracked* aggregate (feature_runs / plans / pull_requests). The retro process
|
|
9
|
+
-- had no such tracking, so its user task was invisible to the inbox. These two columns make
|
|
10
|
+
-- `plan_conformance` the retro run's tracking row: `process_key` is the retro process instance, and
|
|
11
|
+
-- `review_status` is its escalation lifecycle — `reviewing` while the ack task is open (the poller
|
|
12
|
+
-- scans these), `reviewed` once the run settles (set by `record-conformance` when there is nothing to
|
|
13
|
+
-- escalate, by the `pr.conformance-ack` worker (`acknowledgeConformance`) when an operator
|
|
14
|
+
-- acknowledges the escalation and the retro instance COMPLETES normally, and — as a crash/cancel
|
|
15
|
+
-- safety net — by `instanceTracking.onTerminated` when the retro instance is TERMINATED rather than
|
|
16
|
+
-- completing).
|
|
17
|
+
--
|
|
18
|
+
-- Forward-only, additive (expand): two nullable/defaulted columns on the table 052 just added; the
|
|
19
|
+
-- runner wraps each file in its own transaction, so this file must NOT contain BEGIN/COMMIT.
|
|
20
|
+
ALTER TABLE plan_conformance ADD COLUMN process_key TEXT;
|
|
21
|
+
ALTER TABLE plan_conformance ADD COLUMN review_status TEXT NOT NULL DEFAULT 'reviewed';
|
|
22
|
+
|
|
23
|
+
-- `pollUserTasks` scans open retro escalations by `review_status = 'reviewing'` every poll pass
|
|
24
|
+
-- (app/conformance.ts `activeConformanceReviews`). Index it so the common-case status scan stays a
|
|
25
|
+
-- cheap lookup instead of a full table scan as conformance rows accumulate — mirrors the
|
|
26
|
+
-- `idx_feature_runs_status` precedent (028) for the equivalent status-scanned aggregate.
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_plan_conformance_review_status ON plan_conformance(review_status);
|
package/nano.app.json
CHANGED
|
@@ -67,6 +67,20 @@
|
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
"pollMs": 5000
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"table": "plan_conformance",
|
|
73
|
+
"keyField": "process_key",
|
|
74
|
+
"statusField": "review_status",
|
|
75
|
+
"activeStatuses": [
|
|
76
|
+
"reviewing"
|
|
77
|
+
],
|
|
78
|
+
"onTerminated": {
|
|
79
|
+
"set": {
|
|
80
|
+
"review_status": "reviewed"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"pollMs": 5000
|
|
70
84
|
}
|
|
71
85
|
],
|
|
72
86
|
"workers": [
|
|
@@ -166,6 +180,10 @@
|
|
|
166
180
|
"taskType": "pr.conformance-record",
|
|
167
181
|
"handler": "workers/conformance-record/worker.ts"
|
|
168
182
|
},
|
|
183
|
+
{
|
|
184
|
+
"taskType": "pr.conformance-ack",
|
|
185
|
+
"handler": "workers/conformance-ack/worker.ts"
|
|
186
|
+
},
|
|
169
187
|
{
|
|
170
188
|
"taskType": "pr.progress-check",
|
|
171
189
|
"handler": "workers/progress-check/worker.ts"
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.106.1",
|
|
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",
|
|
7
7
|
"imports": {
|
|
8
|
-
"#test-assert": "./test/assert.ts"
|
|
8
|
+
"#test-assert": "./test/assert.ts",
|
|
9
|
+
"#test-migrations": "./test/migrations.ts"
|
|
9
10
|
},
|
|
10
11
|
"engines": {
|
|
11
12
|
"node": ">=22.6"
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"pretypecheck": "urban gen",
|
|
38
39
|
"check:prompts": "node --experimental-strip-types scripts/check-agent-prompts.ts",
|
|
39
40
|
"check:migrations": "node --experimental-strip-types scripts/check-migrations.ts",
|
|
41
|
+
"heal:migrations": "node --experimental-strip-types scripts/heal-migration-ledger.ts",
|
|
40
42
|
"check:contracts": "node --experimental-strip-types scripts/check-contracts.ts",
|
|
41
43
|
"reconcile:contracts": "node --experimental-strip-types scripts/reconcile-contracts.ts",
|
|
42
44
|
"gen": "urban gen",
|
package/pages/tasks.page.json
CHANGED
|
@@ -532,6 +532,90 @@
|
|
|
532
532
|
},
|
|
533
533
|
"refreshMs": 5000
|
|
534
534
|
}
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
"type": "dataGrid",
|
|
538
|
+
"id": "conformance-reviews",
|
|
539
|
+
"props": {
|
|
540
|
+
"title": "Conformance reviews",
|
|
541
|
+
"data": {
|
|
542
|
+
"kind": "datasource",
|
|
543
|
+
"source": "app",
|
|
544
|
+
"table": "user_tasks",
|
|
545
|
+
"filter": [
|
|
546
|
+
{
|
|
547
|
+
"field": "element_id",
|
|
548
|
+
"in": [
|
|
549
|
+
"conformance-escalation"
|
|
550
|
+
]
|
|
551
|
+
}
|
|
552
|
+
],
|
|
553
|
+
"orderBy": {
|
|
554
|
+
"field": "updated_at",
|
|
555
|
+
"dir": "desc"
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
"columns": [
|
|
559
|
+
{
|
|
560
|
+
"field": "subject_title",
|
|
561
|
+
"template": "{{subject_title}}",
|
|
562
|
+
"header": "Epic",
|
|
563
|
+
"subtitleField": "subject_key",
|
|
564
|
+
"truncate": true,
|
|
565
|
+
"linkField": "subject_url"
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
"field": "question",
|
|
569
|
+
"header": "Findings"
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"field": "subject_url",
|
|
573
|
+
"header": "Issue"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"field": "process_key",
|
|
577
|
+
"header": "Process",
|
|
578
|
+
"link": {
|
|
579
|
+
"kind": "processExplorer",
|
|
580
|
+
"keyField": "process_key"
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
"field": "updated_at",
|
|
585
|
+
"header": "Updated",
|
|
586
|
+
"format": "datetime"
|
|
587
|
+
}
|
|
588
|
+
],
|
|
589
|
+
"rowKey": "user_task_key",
|
|
590
|
+
"detail": {
|
|
591
|
+
"linkField": "subject_url",
|
|
592
|
+
"fields": [
|
|
593
|
+
{
|
|
594
|
+
"field": "question",
|
|
595
|
+
"label": "Findings"
|
|
596
|
+
}
|
|
597
|
+
],
|
|
598
|
+
"form": {
|
|
599
|
+
"showWhenField": "user_task_key",
|
|
600
|
+
"title": "Acknowledge conformance review",
|
|
601
|
+
"promptField": "question",
|
|
602
|
+
"inputKey": "note",
|
|
603
|
+
"inputLabel": "Disposition note (what you decided / follow-up)",
|
|
604
|
+
"submitLabel": "Acknowledge & close",
|
|
605
|
+
"action": {
|
|
606
|
+
"path": "/app/api/actions/complete-user-task",
|
|
607
|
+
"successLabel": "Acknowledged — the review will close",
|
|
608
|
+
"body": {
|
|
609
|
+
"userTaskKey": "{{row.user_task_key}}",
|
|
610
|
+
"variables": {
|
|
611
|
+
"note": "{{form.note}}"
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
"refreshMs": 5000
|
|
618
|
+
}
|
|
535
619
|
}
|
|
536
620
|
]
|
|
537
621
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "conformance-escalation",
|
|
3
|
+
"schemaVersion": 18,
|
|
4
|
+
"type": "default",
|
|
5
|
+
"components": [
|
|
6
|
+
{
|
|
7
|
+
"type": "text",
|
|
8
|
+
"text": "The spec-conformance review found this epic did **not** cleanly meet its spec — a slice was reduced or could not be verified against the code, or a deviation was made that nobody raised during implementation. The delivery already landed, so this is a **non-blocking** follow-up: review the report on the epic issue, decide any remediation, then acknowledge to close it.",
|
|
9
|
+
"label": "Conformance review"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "textarea",
|
|
13
|
+
"key": "note",
|
|
14
|
+
"label": "Disposition note (what you decided / follow-up — recorded on the review)"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -61,7 +61,32 @@
|
|
|
61
61
|
</zeebe:properties>
|
|
62
62
|
</bpmn:extensionElements>
|
|
63
63
|
<bpmn:incoming>f_toRecordConformance</bpmn:incoming>
|
|
64
|
-
<bpmn:outgoing>
|
|
64
|
+
<bpmn:outgoing>f_toDeviationsGw</bpmn:outgoing>
|
|
65
|
+
</bpmn:serviceTask>
|
|
66
|
+
<bpmn:exclusiveGateway id="gw-deviations" name="deviations?" default="f_noDeviations">
|
|
67
|
+
<bpmn:incoming>f_toDeviationsGw</bpmn:incoming>
|
|
68
|
+
<bpmn:outgoing>f_deviations</bpmn:outgoing>
|
|
69
|
+
<bpmn:outgoing>f_noDeviations</bpmn:outgoing>
|
|
70
|
+
</bpmn:exclusiveGateway>
|
|
71
|
+
<bpmn:userTask id="conformance-escalation" name="Conformance review (human)">
|
|
72
|
+
<bpmn:extensionElements>
|
|
73
|
+
<zeebe:formDefinition formId="conformance-escalation" />
|
|
74
|
+
<zeebe:userTask />
|
|
75
|
+
<zeebe:assignmentDefinition candidateGroups="operators" />
|
|
76
|
+
</bpmn:extensionElements>
|
|
77
|
+
<bpmn:incoming>f_deviations</bpmn:incoming>
|
|
78
|
+
<bpmn:outgoing>f_toConformanceAck</bpmn:outgoing>
|
|
79
|
+
</bpmn:userTask>
|
|
80
|
+
<bpmn:serviceTask id="record-conformance-ack" name="Record acknowledgement">
|
|
81
|
+
<bpmn:extensionElements>
|
|
82
|
+
<zeebe:taskDefinition type="pr.conformance-ack" />
|
|
83
|
+
<zeebe:ioMapping>
|
|
84
|
+
<zeebe:input source="=planKey" target="planKey" />
|
|
85
|
+
<zeebe:input source="=if (is defined(note)) then note else null" target="note" />
|
|
86
|
+
</zeebe:ioMapping>
|
|
87
|
+
</bpmn:extensionElements>
|
|
88
|
+
<bpmn:incoming>f_toConformanceAck</bpmn:incoming>
|
|
89
|
+
<bpmn:outgoing>f_ackToSynthesize</bpmn:outgoing>
|
|
65
90
|
</bpmn:serviceTask>
|
|
66
91
|
<bpmn:serviceTask id="synthesize" name="Synthesize & promote (agent)">
|
|
67
92
|
<bpmn:extensionElements>
|
|
@@ -73,7 +98,8 @@
|
|
|
73
98
|
<zeebe:input source="=retroDigest" target="appendPrompt" />
|
|
74
99
|
</zeebe:ioMapping>
|
|
75
100
|
</bpmn:extensionElements>
|
|
76
|
-
<bpmn:incoming>
|
|
101
|
+
<bpmn:incoming>f_noDeviations</bpmn:incoming>
|
|
102
|
+
<bpmn:incoming>f_ackToSynthesize</bpmn:incoming>
|
|
77
103
|
<bpmn:outgoing>f_toRecord</bpmn:outgoing>
|
|
78
104
|
</bpmn:serviceTask>
|
|
79
105
|
<bpmn:serviceTask id="record" name="Record retro">
|
|
@@ -92,7 +118,13 @@
|
|
|
92
118
|
<bpmn:sequenceFlow id="f_start" sourceRef="Start" targetRef="gather" />
|
|
93
119
|
<bpmn:sequenceFlow id="f_toConformance" sourceRef="gather" targetRef="conformance" />
|
|
94
120
|
<bpmn:sequenceFlow id="f_toRecordConformance" sourceRef="conformance" targetRef="record-conformance" />
|
|
95
|
-
<bpmn:sequenceFlow id="
|
|
121
|
+
<bpmn:sequenceFlow id="f_toDeviationsGw" sourceRef="record-conformance" targetRef="gw-deviations" />
|
|
122
|
+
<bpmn:sequenceFlow id="f_deviations" name="deviations" sourceRef="gw-deviations" targetRef="conformance-escalation">
|
|
123
|
+
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=hasDeviations = true</bpmn:conditionExpression>
|
|
124
|
+
</bpmn:sequenceFlow>
|
|
125
|
+
<bpmn:sequenceFlow id="f_noDeviations" sourceRef="gw-deviations" targetRef="synthesize" />
|
|
126
|
+
<bpmn:sequenceFlow id="f_toConformanceAck" sourceRef="conformance-escalation" targetRef="record-conformance-ack" />
|
|
127
|
+
<bpmn:sequenceFlow id="f_ackToSynthesize" sourceRef="record-conformance-ack" targetRef="synthesize" />
|
|
96
128
|
<bpmn:sequenceFlow id="f_toRecord" sourceRef="synthesize" targetRef="record" />
|
|
97
129
|
<bpmn:sequenceFlow id="f_toEnd" sourceRef="record" targetRef="End" />
|
|
98
130
|
</bpmn:process>
|
|
@@ -113,16 +145,28 @@
|
|
|
113
145
|
<bpmndi:BPMNShape id="BPMNShape_record-conformance" bpmnElement="record-conformance">
|
|
114
146
|
<dc:Bounds x="616" y="80" width="100" height="80" />
|
|
115
147
|
</bpmndi:BPMNShape>
|
|
148
|
+
<bpmndi:BPMNShape id="BPMNShape_gw-deviations" bpmnElement="gw-deviations" isMarkerVisible="true">
|
|
149
|
+
<dc:Bounds x="776" y="95" width="50" height="50" />
|
|
150
|
+
<bpmndi:BPMNLabel>
|
|
151
|
+
<dc:Bounds x="770" y="65" width="62" height="14" />
|
|
152
|
+
</bpmndi:BPMNLabel>
|
|
153
|
+
</bpmndi:BPMNShape>
|
|
154
|
+
<bpmndi:BPMNShape id="BPMNShape_conformance-escalation" bpmnElement="conformance-escalation">
|
|
155
|
+
<dc:Bounds x="751" y="240" width="100" height="80" />
|
|
156
|
+
</bpmndi:BPMNShape>
|
|
157
|
+
<bpmndi:BPMNShape id="BPMNShape_record-conformance-ack" bpmnElement="record-conformance-ack">
|
|
158
|
+
<dc:Bounds x="911" y="240" width="100" height="80" />
|
|
159
|
+
</bpmndi:BPMNShape>
|
|
116
160
|
<bpmndi:BPMNShape id="BPMNShape_synthesize" bpmnElement="synthesize">
|
|
117
|
-
<dc:Bounds x="
|
|
161
|
+
<dc:Bounds x="1056" y="80" width="100" height="80" />
|
|
118
162
|
</bpmndi:BPMNShape>
|
|
119
163
|
<bpmndi:BPMNShape id="BPMNShape_record" bpmnElement="record">
|
|
120
|
-
<dc:Bounds x="
|
|
164
|
+
<dc:Bounds x="1256" y="80" width="100" height="80" />
|
|
121
165
|
</bpmndi:BPMNShape>
|
|
122
166
|
<bpmndi:BPMNShape id="BPMNShape_End" bpmnElement="End">
|
|
123
|
-
<dc:Bounds x="
|
|
167
|
+
<dc:Bounds x="1456" y="102" width="36" height="36" />
|
|
124
168
|
<bpmndi:BPMNLabel>
|
|
125
|
-
<dc:Bounds x="
|
|
169
|
+
<dc:Bounds x="1434" y="143" width="80" height="14" />
|
|
126
170
|
</bpmndi:BPMNLabel>
|
|
127
171
|
</bpmndi:BPMNShape>
|
|
128
172
|
<bpmndi:BPMNEdge id="BPMNEdge_f_start" bpmnElement="f_start">
|
|
@@ -137,17 +181,34 @@
|
|
|
137
181
|
<di:waypoint x="516" y="120" />
|
|
138
182
|
<di:waypoint x="616" y="120" />
|
|
139
183
|
</bpmndi:BPMNEdge>
|
|
140
|
-
<bpmndi:BPMNEdge id="
|
|
184
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_toDeviationsGw" bpmnElement="f_toDeviationsGw">
|
|
141
185
|
<di:waypoint x="716" y="120" />
|
|
142
|
-
<di:waypoint x="
|
|
186
|
+
<di:waypoint x="776" y="120" />
|
|
187
|
+
</bpmndi:BPMNEdge>
|
|
188
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_noDeviations" bpmnElement="f_noDeviations">
|
|
189
|
+
<di:waypoint x="826" y="120" />
|
|
190
|
+
<di:waypoint x="1056" y="120" />
|
|
191
|
+
</bpmndi:BPMNEdge>
|
|
192
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_deviations" bpmnElement="f_deviations">
|
|
193
|
+
<di:waypoint x="801" y="145" />
|
|
194
|
+
<di:waypoint x="801" y="240" />
|
|
195
|
+
</bpmndi:BPMNEdge>
|
|
196
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_toConformanceAck" bpmnElement="f_toConformanceAck">
|
|
197
|
+
<di:waypoint x="851" y="280" />
|
|
198
|
+
<di:waypoint x="911" y="280" />
|
|
199
|
+
</bpmndi:BPMNEdge>
|
|
200
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_ackToSynthesize" bpmnElement="f_ackToSynthesize">
|
|
201
|
+
<di:waypoint x="1011" y="280" />
|
|
202
|
+
<di:waypoint x="1106" y="280" />
|
|
203
|
+
<di:waypoint x="1106" y="160" />
|
|
143
204
|
</bpmndi:BPMNEdge>
|
|
144
205
|
<bpmndi:BPMNEdge id="BPMNEdge_f_toRecord" bpmnElement="f_toRecord">
|
|
145
|
-
<di:waypoint x="
|
|
146
|
-
<di:waypoint x="
|
|
206
|
+
<di:waypoint x="1156" y="120" />
|
|
207
|
+
<di:waypoint x="1256" y="120" />
|
|
147
208
|
</bpmndi:BPMNEdge>
|
|
148
209
|
<bpmndi:BPMNEdge id="BPMNEdge_f_toEnd" bpmnElement="f_toEnd">
|
|
149
|
-
<di:waypoint x="
|
|
150
|
-
<di:waypoint x="
|
|
210
|
+
<di:waypoint x="1356" y="120" />
|
|
211
|
+
<di:waypoint x="1456" y="120" />
|
|
151
212
|
</bpmndi:BPMNEdge>
|
|
152
213
|
</bpmndi:BPMNPlane>
|
|
153
214
|
</bpmndi:BPMNDiagram>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Red/green coverage for the migration immutability gate (scripts/check-migrations.ts, issue #357).
|
|
2
|
+
//
|
|
3
|
+
// The runtime keys the migration ledger by FILENAME, so once a migration merges it must never be
|
|
4
|
+
// renamed (re-runs against a migrated DB and aborts boot), deleted (desyncs ledger from schema), or
|
|
5
|
+
// edited (silently no-ops on migrated installs). The gate diffs db/migrations against the merge-base
|
|
6
|
+
// with origin/main; here we drive its diff classifier directly with representative
|
|
7
|
+
// `git diff --find-renames --name-status` output so each violation shape is pinned.
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import { immutabilityErrorsFromDiff } from "./check-migrations.ts";
|
|
10
|
+
import { assert, assertEquals } from "#test-assert";
|
|
11
|
+
|
|
12
|
+
test("a rename of a merged migration is a violation", () => {
|
|
13
|
+
const errors = immutabilityErrorsFromDiff(
|
|
14
|
+
"R100\tdb/migrations/043_user_tasks_subject_title.sql\tdb/migrations/046_user_tasks_subject_title.sql",
|
|
15
|
+
);
|
|
16
|
+
assertEquals(errors.length, 1);
|
|
17
|
+
assert(/RENAMED/.test(errors[0]));
|
|
18
|
+
assert(/043_user_tasks_subject_title.sql -> 046_user_tasks_subject_title.sql/.test(errors[0]));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("a delete of a merged migration is a violation", () => {
|
|
22
|
+
const errors = immutabilityErrorsFromDiff("D\tdb/migrations/046_user_tasks_subject_title.sql");
|
|
23
|
+
assertEquals(errors.length, 1);
|
|
24
|
+
assert(/DELETED/.test(errors[0]));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("an edit of a merged migration is a violation", () => {
|
|
28
|
+
const errors = immutabilityErrorsFromDiff("M\tdb/migrations/046_user_tasks_subject_title.sql");
|
|
29
|
+
assertEquals(errors.length, 1);
|
|
30
|
+
assert(/EDITED/.test(errors[0]));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("adding a new migration is allowed", () => {
|
|
34
|
+
assertEquals(
|
|
35
|
+
immutabilityErrorsFromDiff("A\tdb/migrations/061_brand_new.sql"),
|
|
36
|
+
[],
|
|
37
|
+
"an addition is not a violation",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("a clean diff (no migration changes) yields no violations", () => {
|
|
42
|
+
assertEquals(immutabilityErrorsFromDiff(""), []);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("mixed changes report every violation but ignore the addition", () => {
|
|
46
|
+
const errors = immutabilityErrorsFromDiff(
|
|
47
|
+
[
|
|
48
|
+
"A\tdb/migrations/061_new.sql",
|
|
49
|
+
"M\tdb/migrations/010_old.sql",
|
|
50
|
+
"D\tdb/migrations/011_gone.sql",
|
|
51
|
+
"R096\tdb/migrations/012_a.sql\tdb/migrations/013_a.sql",
|
|
52
|
+
].join("\n"),
|
|
53
|
+
);
|
|
54
|
+
assertEquals(errors.length, 3, "the three mutations are flagged, the addition is not");
|
|
55
|
+
assert(errors.some((e) => /EDITED/.test(e)));
|
|
56
|
+
assert(errors.some((e) => /DELETED/.test(e)));
|
|
57
|
+
assert(errors.some((e) => /RENAMED/.test(e)));
|
|
58
|
+
});
|