@nanobpm/nano-workforce 0.106.1 → 0.106.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,17 @@
1
+ ## [0.106.3](https://github.com/nanobpm/nano-workforce/compare/v0.106.2...v0.106.3) (2026-08-20)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **models:** regenerate stale retro.bpmn DI ([#365](https://github.com/nanobpm/nano-workforce/issues/365)) ([5619ee5](https://github.com/nanobpm/nano-workforce/commit/5619ee5805fb788ae5deebeb75bb0aa11f9c478d)), closes [355/#356](https://github.com/nanobpm/nano-workforce/issues/356)
7
+
8
+ ## [0.106.2](https://github.com/nanobpm/nano-workforce/compare/v0.106.1...v0.106.2) (2026-08-20)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **velocity:** bucket burn-up by local timezone, not UTC ([#361](https://github.com/nanobpm/nano-workforce/issues/361)) ([#362](https://github.com/nanobpm/nano-workforce/issues/362)) ([84d842a](https://github.com/nanobpm/nano-workforce/commit/84d842ad443fba8a0c399bdd5f4fca466da4a21c))
14
+
1
15
  ## [0.106.1](https://github.com/nanobpm/nano-workforce/compare/v0.106.0...v0.106.1) (2026-08-20)
2
16
 
3
17
 
@@ -12,6 +12,13 @@ import { assert, assertEquals } from "#test-assert";
12
12
  import type { DataLayer } from "@nanobpm/urban";
13
13
  import { deriveMergesPerDay, type MergeAuditRow, pollMergesPerDay } from "./mergesPerDay.ts";
14
14
 
15
+ // The bucketing is now LOCAL-calendar-day (issue #361: use the viewer's timezone, not UTC). The
16
+ // derivation buckets in an explicit IANA `timeZone` argument (via `Intl.DateTimeFormat`), so these
17
+ // tests pass the zone directly rather than mutating the process-global `process.env.TZ` — which
18
+ // `node --test` runs concurrently across files, so an in-process `TZ` flip could leak into and
19
+ // reorder unrelated date-handling tests. The UTC-based assertions pass `"UTC"`; the
20
+ // timezone-specific ones pass the zone they exercise.
21
+
15
22
  // A tiny in-memory record gateway (all/find/insert/update/delete), mirroring the fake-app style used
16
23
  // across the app tests (see app/delivery.test.ts), enough to exercise the `pollMergesPerDay`
17
24
  // projection.
@@ -62,7 +69,7 @@ test("counts DISTINCT merged PRs per calendar day", () => {
62
69
  merged("o/r#1", "2026-01-01T09:00:00Z"),
63
70
  merged("o/r#2", "2026-01-01T18:30:00Z"),
64
71
  merged("o/r#3", "2026-01-02T10:00:00Z"),
65
- ]);
72
+ ], "UTC");
66
73
  assertEquals(days.map((d) => [d.day, d.merged]), [
67
74
  ["2026-01-01", 2],
68
75
  ["2026-01-02", 1],
@@ -74,7 +81,7 @@ test("dedupes duplicate merged rows for the same PR on the same day (COUNT DISTI
74
81
  merged("o/r#1", "2026-01-01T09:00:00Z"),
75
82
  merged("o/r#1", "2026-01-01T09:00:05Z"), // retry / already-merged short-circuit
76
83
  merged("o/r#1", "2026-01-01T23:59:00Z"),
77
- ]);
84
+ ], "UTC");
78
85
  assertEquals(days.length, 1);
79
86
  assertEquals(days[0].merged, 1);
80
87
  });
@@ -84,7 +91,7 @@ test("the same PR merged on two different days counts once per day", () => {
84
91
  const days = deriveMergesPerDay([
85
92
  merged("o/r#1", "2026-01-01T09:00:00Z"),
86
93
  merged("o/r#1", "2026-01-02T09:00:00Z"),
87
- ]);
94
+ ], "UTC");
88
95
  assertEquals(days.map((d) => [d.day, d.merged]), [
89
96
  ["2026-01-01", 1],
90
97
  ["2026-01-02", 1],
@@ -96,7 +103,7 @@ test("ignores queued and blocked attempts", () => {
96
103
  merged("o/r#1", "2026-01-01T09:00:00Z"),
97
104
  { pr_key: "o/r#2", outcome: "queued", at: "2026-01-01T09:10:00Z" },
98
105
  { pr_key: "o/r#3", outcome: "blocked", at: "2026-01-01T09:20:00Z" },
99
- ]);
106
+ ], "UTC");
100
107
  assertEquals(days.length, 1);
101
108
  assertEquals(days[0].merged, 1);
102
109
  });
@@ -107,7 +114,7 @@ test("orders days ascending and carries a running burn-up cumulative", () => {
107
114
  merged("o/r#1", "2026-01-01T10:00:00Z"),
108
115
  merged("o/r#2", "2026-01-01T11:00:00Z"),
109
116
  merged("o/r#4", "2026-01-02T10:00:00Z"),
110
- ]);
117
+ ], "UTC");
111
118
  assertEquals(days.map((d) => d.day), ["2026-01-01", "2026-01-02", "2026-01-03"]);
112
119
  assertEquals(days.map((d) => d.merged), [2, 1, 1]);
113
120
  assertEquals(days.map((d) => d.cumulative), [2, 3, 4]);
@@ -122,7 +129,7 @@ test("bar scales against the busiest day: full for the max, non-empty for a lone
122
129
  merged("o/r#4", "2026-01-01T04:00:00Z"),
123
130
  // day B: 1 merge → short but visible bar
124
131
  merged("o/r#5", "2026-01-02T01:00:00Z"),
125
- ]);
132
+ ], "UTC");
126
133
  const [a, b] = days;
127
134
  assert(a.bar.length > b.bar.length, "the busier day must draw a longer bar");
128
135
  assert(b.bar.length >= 1, "a day with any merge must draw at least one glyph");
@@ -133,6 +140,83 @@ test("empty audit yields no days", () => {
133
140
  assertEquals(deriveMergesPerDay([]), []);
134
141
  });
135
142
 
143
+ test("buckets by the viewer's LOCAL calendar day, not UTC (issue #361)", () => {
144
+ // 02:00Z on Jan 1 is still Dec 31 in a west-of-UTC zone (America/New_York, UTC-5).
145
+ {
146
+ const days = deriveMergesPerDay([merged("o/r#1", "2026-01-01T02:00:00Z")], "America/New_York");
147
+ assertEquals(
148
+ days.map((d) => d.day),
149
+ ["2025-12-31"],
150
+ );
151
+ }
152
+ // 23:00Z on Jan 1 is already Jan 2 in an east-of-UTC zone (Pacific/Kiritimati, UTC+14).
153
+ {
154
+ const days = deriveMergesPerDay([merged("o/r#1", "2026-01-01T23:00:00Z")], "Pacific/Kiritimati");
155
+ assertEquals(
156
+ days.map((d) => d.day),
157
+ ["2026-01-02"],
158
+ );
159
+ }
160
+ });
161
+
162
+ test("two merges either side of local midnight land on the same local day (issue #361)", () => {
163
+ // In UTC these are two different UTC days; in America/New_York (UTC-5) both are Jan 1 evening,
164
+ // so a local-time bucketing counts them together on 2026-01-01.
165
+ {
166
+ const days = deriveMergesPerDay(
167
+ [
168
+ merged("o/r#1", "2026-01-01T18:00:00Z"), // 13:00 local, Jan 1
169
+ merged("o/r#2", "2026-01-02T04:00:00Z"), // 23:00 local, Jan 1
170
+ ],
171
+ "America/New_York",
172
+ );
173
+ assertEquals(
174
+ days.map((d) => [d.day, d.merged]),
175
+ [["2026-01-01", 2]],
176
+ );
177
+ }
178
+ });
179
+
180
+ test("non-ISO / malformed `at` still groups deterministically without throwing", () => {
181
+ const days = deriveMergesPerDay([
182
+ { pr_key: "o/r#1", outcome: "merged", at: "not-a-timestamp" },
183
+ { pr_key: "o/r#2", outcome: "merged", at: "not-a-timestamp" },
184
+ ], "UTC");
185
+ assertEquals(days.length, 1);
186
+ assertEquals(days[0].day, "not-a-timestamp");
187
+ assertEquals(days[0].merged, 2);
188
+ });
189
+
190
+ test("ambiguous partially-formed `at` (date-only / offset-less) buckets on the trimmed string, not a runtime-dependent day", () => {
191
+ // `new Date("2026-01-01")` parses as UTC midnight while `new Date("2026-01-01T12:00:00")` parses in
192
+ // the host's local zone — bucketing either would be runtime/timezone-dependent, the exact drift this
193
+ // read model exists to avoid. Neither carries an explicit `Z`/offset, so both must fall back to the
194
+ // trimmed string and group deterministically regardless of the viewer's `timeZone`.
195
+ const rows: MergeAuditRow[] = [
196
+ { pr_key: "o/r#1", outcome: "merged", at: "2026-01-01" },
197
+ { pr_key: "o/r#2", outcome: "merged", at: "2026-01-01T12:00:00" },
198
+ ];
199
+ for (const zone of ["UTC", "America/New_York", "Pacific/Kiritimati"]) {
200
+ const days = deriveMergesPerDay(rows, zone);
201
+ assertEquals(days.map((d) => [d.day, d.merged]), [
202
+ ["2026-01-01", 1],
203
+ ["2026-01-01T12:00:00", 1],
204
+ ]);
205
+ }
206
+ });
207
+
208
+ test("an invalid IANA timeZone falls back to the host zone instead of throwing (issue #361)", () => {
209
+ // A bogus zone would make `Intl.DateTimeFormat` throw a `RangeError`; bucketing must stay
210
+ // deterministic and not wedge `deriveMergesPerDay`/`pollMergesPerDay`.
211
+ const days = deriveMergesPerDay(
212
+ [merged("o/r#1", "2026-01-01T12:00:00Z")],
213
+ "Not/AZone",
214
+ );
215
+ assertEquals(days.length, 1);
216
+ assertEquals(days[0].merged, 1);
217
+ assert(/^\d{4}-\d{2}-\d{2}$/.test(days[0].day));
218
+ });
219
+
136
220
  test("pollMergesPerDay projects the aggregate onto merges_per_day", async () => {
137
221
  const { data, stores } = memData();
138
222
  stores.merges = [
@@ -141,7 +225,7 @@ test("pollMergesPerDay projects the aggregate onto merges_per_day", async () =>
141
225
  { id: 3, pr_key: "o/r#2", outcome: "merged", at: "2026-01-02T09:00:00Z" },
142
226
  { id: 4, pr_key: "o/r#3", outcome: "queued", at: "2026-01-02T09:10:00Z" }, // ignored
143
227
  ];
144
- await pollMergesPerDay(data);
228
+ await pollMergesPerDay(data, "UTC");
145
229
  const rows = (stores.merges_per_day ?? []).slice().sort((x, y) => x.day.localeCompare(y.day));
146
230
  assertEquals(rows.map((r) => [r.day, r.merged, r.cumulative]), [
147
231
  ["2026-01-01", 1, 1],
@@ -153,10 +237,10 @@ test("pollMergesPerDay projects the aggregate onto merges_per_day", async () =>
153
237
  test("pollMergesPerDay is idempotent — a steady-state re-run writes nothing", async () => {
154
238
  const { data, stores, writes } = memData();
155
239
  stores.merges = [{ id: 1, pr_key: "o/r#1", outcome: "merged", at: "2026-01-01T09:00:00Z" }];
156
- await pollMergesPerDay(data);
240
+ await pollMergesPerDay(data, "UTC");
157
241
  const afterFirst = writes();
158
242
  assert(afterFirst > 0, "the first pass must project at least one row");
159
- await pollMergesPerDay(data);
243
+ await pollMergesPerDay(data, "UTC");
160
244
  assertEquals(writes(), afterFirst, "a steady-state re-run must not write");
161
245
  });
162
246
 
@@ -166,7 +250,7 @@ test("pollMergesPerDay prunes a day that no longer derives from the audit", asyn
166
250
  { day: "2025-12-31", merged: 3, cumulative: 3, bar: "███", updated_at: "old" },
167
251
  ];
168
252
  stores.merges = [{ id: 1, pr_key: "o/r#1", outcome: "merged", at: "2026-01-01T09:00:00Z" }];
169
- await pollMergesPerDay(data);
253
+ await pollMergesPerDay(data, "UTC");
170
254
  const days = (stores.merges_per_day ?? []).map((r: any) => r.day);
171
255
  assert(!days.includes("2025-12-31"), "a stale day must be pruned");
172
256
  assert(days.includes("2026-01-01"), "the derived day must be present");
@@ -7,10 +7,12 @@
7
7
  // set is not exhaustive), `at` = ISO timestamp. Only `outcome = 'merged'` rows feed this aggregate,
8
8
  // so merged-per-day is
9
9
  // fully DERIVABLE from that audit trail with NO new write-path bookkeeping (AGENTS.md: "Derivation
10
- // over duplication"). The canonical aggregate is the one in the issue:
10
+ // over duplication"). The `at` audit value is UTC, but the day is bucketed in the viewer's LOCAL
11
+ // timezone (issue #361) so an operator sees merges on the calendar day they happened locally, not
12
+ // shifted across a UTC midnight. The canonical aggregate is the one in the issue, in local time:
11
13
  //
12
- // SELECT date(at) AS day, COUNT(DISTINCT pr_key) AS merged
13
- // FROM merges WHERE outcome = 'merged' GROUP BY date(at);
14
+ // SELECT date(at, 'localtime') AS day, COUNT(DISTINCT pr_key) AS merged
15
+ // FROM merges WHERE outcome = 'merged' GROUP BY date(at, 'localtime');
14
16
  //
15
17
  // Two halves, mirroring the `deriveDelivery`/`pollDelivery` and `deriveLineage`/`pollLineage`
16
18
  // convention:
@@ -37,7 +39,7 @@ export interface MergeAuditRow {
37
39
 
38
40
  /** One projected calendar day of merge throughput. */
39
41
  export interface MergeDay {
40
- /** Calendar day, ISO `YYYY-MM-DD` (SQLite `date(at)`). */
42
+ /** Local calendar day, ISO `YYYY-MM-DD` (SQLite `date(at, 'localtime')` — issue #361). */
41
43
  day: string;
42
44
  /** Distinct PRs merged that day (`COUNT(DISTINCT pr_key)`). */
43
45
  merged: number;
@@ -51,12 +53,72 @@ export interface MergeDay {
51
53
  const BAR_WIDTH = 30;
52
54
  const BAR_FULL = "█";
53
55
 
54
- /** The calendar day of an ISO timestamp — the JS twin of SQLite `date(at)`. A well-formed `merges.at`
55
- * is an ISO string, so the first 10 chars are `YYYY-MM-DD`; fall back to the whole trimmed value for
56
- * any non-ISO shape so a malformed row still groups deterministically rather than throwing. */
57
- function dayOf(at: string): string {
56
+ /** The **local** calendar day of an ISO timestamp — the viewer's-timezone twin of SQLite
57
+ * `date(at, 'localtime')` (issue #361). The `merges.at` audit value is a UTC ISO string, but the
58
+ * Velocity page is read by an operator in their own timezone, so bucketing on the UTC date split a
59
+ * single local day across two rows (a late-evening merge west of UTC, or an early-morning one east
60
+ * of it, landed on the wrong day). We derive the day in the target `timeZone` via
61
+ * `Intl.DateTimeFormat` — an explicit, side-effect-free zone rather than one mutated through the
62
+ * process-global `process.env.TZ`. When `timeZone` is omitted the formatter uses the host's
63
+ * resolved zone (so a remote deployment can still pin the operator's zone via `TZ`, and a
64
+ * co-located console — the default `npm start` on `localhost` — is already the browser's zone). An
65
+ * invalid/unknown IANA `timeZone` falls back to the host-resolved zone (rather than throwing a
66
+ * `RangeError` that would wedge `deriveMergesPerDay`/`pollMergesPerDay`), so bucketing stays
67
+ * deterministic. Any value that is not an UNAMBIGUOUS ISO instant — one carrying an explicit
68
+ * timezone designator (`Z` or a `±HH:MM`/`±HHMM` offset) — falls back to the whole trimmed string so
69
+ * it still groups deterministically. This deliberately excludes partially-formed values a bare
70
+ * `new Date(s)` would still parse but *ambiguously*: a date-only `"2026-01-01"` is read as UTC
71
+ * midnight while an offset-less `"2026-01-01T12:00:00"` is read in the host's local zone — so
72
+ * bucketing them would be runtime/timezone-dependent, the very drift this read model exists to avoid.
73
+ * Production `merges.at` values are always `new Date().toISOString()` (UTC, `Z`-suffixed), so only a
74
+ * malformed audit row ever takes the fallback. */
75
+ const dayFormatters = new Map<string, Intl.DateTimeFormat>();
76
+
77
+ /** An unambiguous ISO-8601 instant: a full `YYYY-MM-DDTHH:MM[:SS[.sss]]` carrying an explicit zone
78
+ * designator (`Z`, or a `±HH:MM`/`±HHMM` offset). Only these parse to a timezone-independent instant;
79
+ * anything else (date-only, offset-less datetime, free text) buckets ambiguously, so `dayOf` treats
80
+ * it as a non-instant and groups on the trimmed string instead. */
81
+ const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
82
+
83
+ /** A cached `en-CA` day formatter for `timeZone`, falling back to the host-resolved zone when the
84
+ * zone is invalid/unknown (an invalid IANA string makes `Intl.DateTimeFormat` throw a `RangeError`).
85
+ * Keyed so an invalid zone is only probed once. */
86
+ function dayFormatter(timeZone?: string): Intl.DateTimeFormat {
87
+ const key = timeZone ?? "";
88
+ const cached = dayFormatters.get(key);
89
+ if (cached) return cached;
90
+ const opts: Intl.DateTimeFormatOptions = {
91
+ year: "numeric",
92
+ month: "2-digit",
93
+ day: "2-digit",
94
+ };
95
+ let fmt: Intl.DateTimeFormat;
96
+ try {
97
+ fmt = new Intl.DateTimeFormat("en-CA", { ...opts, timeZone });
98
+ } catch {
99
+ fmt = new Intl.DateTimeFormat("en-CA", opts);
100
+ }
101
+ dayFormatters.set(key, fmt);
102
+ return fmt;
103
+ }
104
+
105
+ function dayOf(at: string, timeZone?: string): string {
58
106
  const s = String(at).trim();
59
- return /^\d{4}-\d{2}-\d{2}/.test(s) ? s.slice(0, 10) : s;
107
+ // Only bucket unambiguous ISO instants (explicit `Z`/offset). A partially-formed value a bare
108
+ // `new Date(s)` would still parse — a date-only or offset-less datetime — buckets differently per
109
+ // runtime/timezone, so group it deterministically on the trimmed string instead.
110
+ if (!ISO_INSTANT.test(s)) return s;
111
+ const d = new Date(s);
112
+ if (Number.isNaN(d.getTime())) return s;
113
+ const parts = dayFormatter(timeZone).formatToParts(d);
114
+ const field = (type: string) => parts.find((p) => p.type === type)?.value ?? "";
115
+ const year = field("year");
116
+ const month = field("month");
117
+ const day = field("day");
118
+ // Guard against a formatter that somehow omits a field — never emit a `"--"`-shaped key; fall back
119
+ // to the trimmed string so the row still groups deterministically.
120
+ if (!year || !month || !day) return s;
121
+ return `${year}-${month}-${day}`;
60
122
  }
61
123
 
62
124
  /** Render a proportional bar: `merged` glyphs scaled against the busiest day's `max`, min one glyph
@@ -69,17 +131,20 @@ function barFor(merged: number, max: number): string {
69
131
 
70
132
  /** PURE aggregate: merge audit rows → one ordered `MergeDay` per calendar day (ascending).
71
133
  *
72
- * Only `outcome === "merged"` rows count; `queued`/`blocked` attempts are ignored. Within a day a
73
- * `pr_key` is counted once (`COUNT(DISTINCT pr_key)`), so duplicate `merged` audit rows — an
74
- * `already-merged` short-circuit or a retry do not double-count. `cumulative` is the running total
75
- * across days (burn-up); `bar` is scaled against the busiest day so the chart is comparable. */
76
- export function deriveMergesPerDay(rows: readonly MergeAuditRow[]): MergeDay[] {
134
+ * Days are bucketed in `timeZone` (an IANA zone, e.g. `America/New_York`); omit it to use the host's
135
+ * resolved zone the production default, matching SQLite `date(at, 'localtime')` for the operator's
136
+ * console (issue #361). Only `outcome === "merged"` rows count; `queued`/`blocked` attempts are
137
+ * ignored. Within a day a `pr_key` is counted once (`COUNT(DISTINCT pr_key)`), so duplicate `merged`
138
+ * audit rows an `already-merged` short-circuit or a retry — do not double-count. `cumulative` is
139
+ * the running total across days (burn-up); `bar` is scaled against the busiest day so the chart is
140
+ * comparable. */
141
+ export function deriveMergesPerDay(rows: readonly MergeAuditRow[], timeZone?: string): MergeDay[] {
77
142
  // day -> set of distinct merged pr_keys that day.
78
143
  const prKeysByDay = new Map<string, Set<string>>();
79
144
  for (const r of rows) {
80
145
  if (r.outcome !== "merged") continue;
81
146
  if (r.pr_key == null || r.at == null) continue;
82
- const day = dayOf(r.at);
147
+ const day = dayOf(r.at, timeZone);
83
148
  let set = prKeysByDay.get(day);
84
149
  if (!set) {
85
150
  set = new Set<string>();
@@ -114,13 +179,14 @@ const mergesAudit = (data: DataLayer) => data.table<MergeAuditRow>("merges", "id
114
179
  * it onto the `merges_per_day` read table the Velocity page reads. Additive/derived only — never
115
180
  * touches `merges`. Upserts a day only when its projection actually changes (so a steady-state pass is
116
181
  * a no-op) and prunes any stale day row that no longer derives (defensive — days are append-only in
117
- * practice, but a purge/rewrite of the audit must not leave a phantom). */
118
- export async function pollMergesPerDay(data: DataLayer): Promise<void> {
182
+ * practice, but a purge/rewrite of the audit must not leave a phantom). Buckets in `timeZone` (an
183
+ * IANA zone) when given; the production caller omits it to use the host's resolved zone. */
184
+ export async function pollMergesPerDay(data: DataLayer, timeZone?: string): Promise<void> {
119
185
  try {
120
186
  // Only `outcome === "merged"` rows contribute to the aggregate, so filter at the read rather than
121
187
  // scanning queued/blocked rows as the audit grows (deriveMergesPerDay ignores non-merged rows too).
122
188
  const audit = await mergesAudit(data).find({ outcome: "merged" });
123
- const want = deriveMergesPerDay(audit);
189
+ const want = deriveMergesPerDay(audit, timeZone);
124
190
  const wantByDay = new Map(want.map((d) => [d.day, d]));
125
191
 
126
192
  const existing = await mergesPerDay(data).all();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.106.1",
3
+ "version": "0.106.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",
@@ -146,27 +146,27 @@
146
146
  <dc:Bounds x="616" y="80" width="100" height="80" />
147
147
  </bpmndi:BPMNShape>
148
148
  <bpmndi:BPMNShape id="BPMNShape_gw-deviations" bpmnElement="gw-deviations" isMarkerVisible="true">
149
- <dc:Bounds x="776" y="95" width="50" height="50" />
149
+ <dc:Bounds x="816" y="95" width="50" height="50" />
150
150
  <bpmndi:BPMNLabel>
151
- <dc:Bounds x="770" y="65" width="62" height="14" />
151
+ <dc:Bounds x="801" y="76" width="81" height="14" />
152
152
  </bpmndi:BPMNLabel>
153
153
  </bpmndi:BPMNShape>
154
154
  <bpmndi:BPMNShape id="BPMNShape_conformance-escalation" bpmnElement="conformance-escalation">
155
- <dc:Bounds x="751" y="240" width="100" height="80" />
155
+ <dc:Bounds x="966" y="240" width="100" height="80" />
156
156
  </bpmndi:BPMNShape>
157
157
  <bpmndi:BPMNShape id="BPMNShape_record-conformance-ack" bpmnElement="record-conformance-ack">
158
- <dc:Bounds x="911" y="240" width="100" height="80" />
158
+ <dc:Bounds x="1166" y="240" width="100" height="80" />
159
159
  </bpmndi:BPMNShape>
160
160
  <bpmndi:BPMNShape id="BPMNShape_synthesize" bpmnElement="synthesize">
161
- <dc:Bounds x="1056" y="80" width="100" height="80" />
161
+ <dc:Bounds x="1366" y="80" width="100" height="80" />
162
162
  </bpmndi:BPMNShape>
163
163
  <bpmndi:BPMNShape id="BPMNShape_record" bpmnElement="record">
164
- <dc:Bounds x="1256" y="80" width="100" height="80" />
164
+ <dc:Bounds x="1566" y="80" width="100" height="80" />
165
165
  </bpmndi:BPMNShape>
166
166
  <bpmndi:BPMNShape id="BPMNShape_End" bpmnElement="End">
167
- <dc:Bounds x="1456" y="102" width="36" height="36" />
167
+ <dc:Bounds x="1766" y="102" width="36" height="36" />
168
168
  <bpmndi:BPMNLabel>
169
- <dc:Bounds x="1434" y="143" width="80" height="14" />
169
+ <dc:Bounds x="1744" y="143" width="80" height="14" />
170
170
  </bpmndi:BPMNLabel>
171
171
  </bpmndi:BPMNShape>
172
172
  <bpmndi:BPMNEdge id="BPMNEdge_f_start" bpmnElement="f_start">
@@ -183,32 +183,36 @@
183
183
  </bpmndi:BPMNEdge>
184
184
  <bpmndi:BPMNEdge id="BPMNEdge_f_toDeviationsGw" bpmnElement="f_toDeviationsGw">
185
185
  <di:waypoint x="716" y="120" />
186
- <di:waypoint x="776" y="120" />
186
+ <di:waypoint x="816" y="120" />
187
187
  </bpmndi:BPMNEdge>
188
188
  <bpmndi:BPMNEdge id="BPMNEdge_f_noDeviations" bpmnElement="f_noDeviations">
189
- <di:waypoint x="826" y="120" />
190
- <di:waypoint x="1056" y="120" />
189
+ <di:waypoint x="866" y="120" />
190
+ <di:waypoint x="1366" y="120" />
191
+ </bpmndi:BPMNEdge>
192
+ <bpmndi:BPMNEdge id="BPMNEdge_f_toRecord" bpmnElement="f_toRecord">
193
+ <di:waypoint x="1466" y="120" />
194
+ <di:waypoint x="1566" y="120" />
195
+ </bpmndi:BPMNEdge>
196
+ <bpmndi:BPMNEdge id="BPMNEdge_f_toEnd" bpmnElement="f_toEnd">
197
+ <di:waypoint x="1666" y="120" />
198
+ <di:waypoint x="1766" y="120" />
191
199
  </bpmndi:BPMNEdge>
192
200
  <bpmndi:BPMNEdge id="BPMNEdge_f_deviations" bpmnElement="f_deviations">
193
- <di:waypoint x="801" y="145" />
194
- <di:waypoint x="801" y="240" />
201
+ <di:waypoint x="841" y="145" />
202
+ <di:waypoint x="841" y="280" />
203
+ <di:waypoint x="966" y="280" />
204
+ <bpmndi:BPMNLabel>
205
+ <dc:Bounds x="846" y="206" width="74" height="14" />
206
+ </bpmndi:BPMNLabel>
195
207
  </bpmndi:BPMNEdge>
196
208
  <bpmndi:BPMNEdge id="BPMNEdge_f_toConformanceAck" bpmnElement="f_toConformanceAck">
197
- <di:waypoint x="851" y="280" />
198
- <di:waypoint x="911" y="280" />
209
+ <di:waypoint x="1066" y="280" />
210
+ <di:waypoint x="1166" y="280" />
199
211
  </bpmndi:BPMNEdge>
200
212
  <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" />
204
- </bpmndi:BPMNEdge>
205
- <bpmndi:BPMNEdge id="BPMNEdge_f_toRecord" bpmnElement="f_toRecord">
206
- <di:waypoint x="1156" y="120" />
207
- <di:waypoint x="1256" y="120" />
208
- </bpmndi:BPMNEdge>
209
- <bpmndi:BPMNEdge id="BPMNEdge_f_toEnd" bpmnElement="f_toEnd">
210
- <di:waypoint x="1356" y="120" />
211
- <di:waypoint x="1456" y="120" />
213
+ <di:waypoint x="1266" y="280" />
214
+ <di:waypoint x="1416" y="280" />
215
+ <di:waypoint x="1416" y="160" />
212
216
  </bpmndi:BPMNEdge>
213
217
  </bpmndi:BPMNPlane>
214
218
  </bpmndi:BPMNDiagram>