toga-ai 1.0.188 → 1.0.190
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.
|
@@ -30,10 +30,15 @@ related:
|
|
|
30
30
|
The Cart page (`src/pages/Cart/`) is the most config-heavy page in `toga2-commerce`. As of
|
|
31
31
|
`_production` it is **config-driven at the field level but hardcoded at the section level**, and it
|
|
32
32
|
renders the cart through **two near-duplicate dual-mode forms** (cart-checkout vs edit-order). A
|
|
33
|
-
config-driven refactor is **planned but not yet
|
|
34
|
-
|
|
35
|
-
re-deriving it. The cart consumes the per-tenant
|
|
36
|
-
`
|
|
33
|
+
config-driven refactor to the toga2.5 (`toga25-supply`) pattern is **planned but not yet
|
|
34
|
+
implemented** (see *Planned refactor*). This doc is written to be **self-sufficient** — a teammate
|
|
35
|
+
can pick the work up from here without re-deriving it. The cart consumes the per-tenant
|
|
36
|
+
`CARTPAGE.ts` config resolved through `useAssignClientFields` — see
|
|
37
|
+
[client-fields](client-fields.md) for that resolution model.
|
|
38
|
+
|
|
39
|
+
> A longer-form plan with the same content also lives in the toga2-commerce repo at
|
|
40
|
+
> `docs/reviews/implement-config-cart-refactor-PLAN.md` (not distributed in the KB bundle). This doc
|
|
41
|
+
> is the authoritative copy for teammates who only have the knowledge base.
|
|
37
42
|
|
|
38
43
|
## Current architecture (`_production`)
|
|
39
44
|
|
|
@@ -71,8 +76,8 @@ based on `inEditMode`. `CartFormRenderer.tsx` exists only on the `implement-conf
|
|
|
71
76
|
- **Config-key naming drift.** `CARTPAGE.ts` uses `inputs:` for the `orderFor` section but
|
|
72
77
|
`formFields:` for `shippingAddress` / `costCenter` / `notifications`, while `CartFormSection`
|
|
73
78
|
reads a `fields` prop. Three names for one concept; `CartForm` hand-maps each section's array into
|
|
74
|
-
the `fields` prop. This pre-existing drift is what tripped the prior spike
|
|
75
|
-
a single `fields` key.)
|
|
79
|
+
the `fields` prop. **This pre-existing drift is what tripped the prior spike.** (Target: rename all
|
|
80
|
+
to a single `fields` key.)
|
|
76
81
|
- **`useForm` is seeded with a hardcoded union of every client's fields.** `CartPage.tsx`
|
|
77
82
|
(~lines 230–258) calls `useForm` with one static `defaultValues` object that is the **union** of
|
|
78
83
|
all clients' fields, with the **COMPASS-only magic shipping-method UUID `087a9d0b-…` baked in**.
|
|
@@ -82,74 +87,201 @@ based on `inEditMode`. `CartFormRenderer.tsx` exists only on the `implement-conf
|
|
|
82
87
|
- **Client differences are hardcoded, not config.** Examples observed: COMPASS renders
|
|
83
88
|
`DuplicateKitGuardrail`, QUAD does not; COMPASS seeds the cart form (cost center / manager / emails)
|
|
84
89
|
from the order-for user, QUAD does not. These are per-client conditionals in cart code today.
|
|
90
|
+
- **Standards debt in the dual forms** (fold in as each file is touched): silent error swallows
|
|
91
|
+
(e.g. the cost-center `onError` that never reaches Sentry), the magic Standard-Ground shipping
|
|
92
|
+
UUID, and a 13-arg `checkIsProceedToCheckoutEnabled` helper (violates the 4+ params rule).
|
|
85
93
|
- `useAssignClientFields.ts` returns `FIELDS?.[clientName]?.[language]?.[role]` with no transform —
|
|
86
94
|
there is no fallback (a missing combo renders empty). See [client-fields](client-fields.md).
|
|
87
95
|
|
|
96
|
+
## Target: the toga2.5 pattern (what "done" looks like)
|
|
97
|
+
|
|
98
|
+
Seven conventions, each already demonstrated in `toga25-supply` (read these as the reference
|
|
99
|
+
implementation):
|
|
100
|
+
|
|
101
|
+
| # | Convention | toga2.5 reference file |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| **C1** | One key name for a section's input array — toga2.5 uses **`fields`** everywhere. | `orderViewFields.json`, `itemRecordCreateFields.json` |
|
|
104
|
+
| **C2** | A section declares its renderer via a **`type`** discriminator; a `Record<type, fn>` map dispatches it. No hardcoded section list. | `SalesOrderSummaryGrid.tsx` (`sectionRenderers[section.type]`) |
|
|
105
|
+
| **C3** | Visibility is `isVisible` filtered at render; **runtime** visibility is computed in the view-model and **patched into config** (memo that rewrites the section), not branched in JSX. | `useSalesOrderRecordModalViewModel` (`patchedTenantFields`) |
|
|
106
|
+
| **C4** | Config stays **serializable**; behavior (handlers, predicates) is a **string key hydrated → function once** in a memo. | `ItemRecordCreate.tsx` (`hydrateField`, `SECOND_VALUE_VISIBILITY`) |
|
|
107
|
+
| **C5** | Generic fields via one `renderField` + generic input component; specialized layout (totals, address) is a standalone component that **still consumes config `fields`**. | `renderField.tsx`, `OrderSectionCard`, `BaseTotals` |
|
|
108
|
+
| **C6** | **Client-specific behavior is declared in config and evaluated generically — never `if (client === "COMPASS")` in code.** Capabilities are flags; conditional logic is a **named rule** (`{ type: "..." }`) evaluated by a small pure evaluator in the view-model. | `approvalActionFields.json` (`approvalActionStatuses`, `approveButtonRule`) → `evaluateApproveRule()` in `useApprovalModalViewModel.tsx` |
|
|
109
|
+
| **C7** | **Form `defaultValues` are derived from the same config that renders the fields** so each client's defaults match exactly the fields it collects. (Cart-specific extension — see below.) | toga2.5 uses a static `emptyItemDefaults` (single-shape form); cart needs config-derived |
|
|
110
|
+
|
|
111
|
+
Applied to the cart, "done" = `CartPage` owns state → a `CartFormContext` distributes it → a
|
|
112
|
+
`CartFormRenderer` walks the section config and dispatches each section (generic `CartFormSection`
|
|
113
|
+
or a standalone like Summary/Confirm/Email) → **one** code path serves both cart and edit-order
|
|
114
|
+
modes via an `inEditMode` flag in context + `visibleWhen: { context: "inEditMode" }` in config.
|
|
115
|
+
|
|
116
|
+
## Gap: `_production` → toga2.5 (the work to close each)
|
|
117
|
+
|
|
118
|
+
| Std | `_production` today | Work to close it |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| **C1** one key (`fields`) | `inputs` (orderFor) + `formFields` (others); `CartFormSection` wants `fields` | Rename all section arrays to **`fields`** in every `CARTPAGE.ts`; drop the hand-mapping. |
|
|
121
|
+
| **C2** section `type` + dispatch map | section layout hardcoded in `CartForm.tsx`, built twice | Add `type` (+ `order`/`isVisible`) to each section in config; build `CartFormRenderer` mapping `type → component`. Delete hardcoded layout. |
|
|
122
|
+
| **C3** runtime visibility patched in VM | `isHiddenDuringEditMode`/`inEditMode` branches scattered | Express edit-order differences as `visibleWhen: { context: "inEditMode" }`; resolve once via context. |
|
|
123
|
+
| **C4** string keys hydrated to fns | `labelButton.onClick:"cantFindUser"`, `inputButtons[].valueKey` are strings but `handleModalOpen` is wired ad-hoc + drilled | One `hydrateCartConfig` memo mapping config strings → handlers from context. |
|
|
124
|
+
| **C5** standalone sections consume config | Summary / email / confirm are inline JSX in `CartForm` | Extract to `SummarySection` / `CartEditEmailSection` / `ConfirmSection`, each reading its config slice. |
|
|
125
|
+
| **(state)** context, not props | ~30 props drilled through 4 levels | `CartFormContext` owns watched values + handlers + `inEditMode`; components read it. |
|
|
126
|
+
| **(VMs)** consolidated | `useEditCartViewModel` + `useEditOrderViewModel` + dispatcher, duplicated effects | Merge into `useCartFormViewModel` (data) + `useCartFormSync` (mount / orderFor-cascade / form→store, mode-branched once). |
|
|
127
|
+
| **C6** client behavior in config | client differences hardcoded (DuplicateKitGuardrail, cost-center seeding) | Move each to a config capability flag or named rule (below). |
|
|
128
|
+
|
|
88
129
|
## Planned refactor (DECIDED 2026-06-24 — plan only, not implemented)
|
|
89
130
|
|
|
90
131
|
**Approach (Route B): rebuild the config-driven cart from the working `_production` branch**, using
|
|
91
132
|
the broken `implement-config` spike branch only as a *reference blueprint* (its target-shaped files
|
|
92
133
|
— `CartFormContext`, `CartFormRenderer`, `useCartFormViewModel`, `useCartFormSync`, standalone
|
|
93
134
|
sections — are architecturally sound but mid-rename and non-functional; lift code where correct,
|
|
94
|
-
do not finish that branch). Rationale
|
|
135
|
+
do not finish that branch). **Rationale:** the spike's −3,940-line deletion silently dropped real
|
|
95
136
|
behavior (email seeding, duplicate-email validation, edit-order cost-center/address). The risk is
|
|
96
137
|
**behavior parity, not architecture**, so migrate in verifiable slices against the running
|
|
97
|
-
`_production` app as the oracle.
|
|
98
|
-
|
|
138
|
+
`_production` app as the oracle.
|
|
139
|
+
|
|
140
|
+
### Phases
|
|
141
|
+
|
|
142
|
+
**Phase 0 — Branch + safety net.** Branch `feature/cart-config-driven-form` off `_production`.
|
|
143
|
+
**Pin a behavior oracle:** no tests cover the cart today, so first document both flows from the
|
|
144
|
+
running app (sections shown, fields, buttons, validation messages, email auto-population, edit-order
|
|
145
|
+
original address) and add RTL smoke tests rendering the cart for one tenant/role in **cart** and
|
|
146
|
+
**edit-order** mode — these guard against the "renders nothing" failure the spike hit. **Inventory
|
|
147
|
+
the hardcoded client branches (C6):** grep `src/pages/Cart` for `clientSlug`/client-name comparisons
|
|
148
|
+
and per-client conditionals (start with `DuplicateKitGuardrail` and the order-for
|
|
149
|
+
cost-center/manager/email seeding) — this list is the C6 work-list for Phases 3–4.
|
|
150
|
+
|
|
151
|
+
**Phase 1 — Normalize config to one key (C1). 🔴 Do first, standalone, behavior-neutral.**
|
|
152
|
+
In every `CARTPAGE.ts` (COMPASS / COMPASSCANADA EN+FR / QUAD × all roles) rename each section's
|
|
153
|
+
input array to **`fields`** (`inputs → fields`, `formFields → fields`). Update `CartForm.tsx`'s
|
|
154
|
+
per-section hand-mapping to read `section.fields` uniformly. Add a `CartSectionConfig` /
|
|
155
|
+
`CartFieldConfig` type so the key is compiler-enforced (kills the untyped `any` that allowed the
|
|
156
|
+
drift). Grep to prove zero `inputs:` / `formFields:` remain. **Ships as its own PR.**
|
|
157
|
+
|
|
158
|
+
**Phase 2 — State into context + config-derived defaults.** Add `CartFormContext` (lift from the
|
|
159
|
+
branch) owning watched values, handlers (`handleModalOpen`, email handlers, cost-center search),
|
|
160
|
+
`formMethods`, `advancedSelectConfig`, `managerName`, and **`inEditMode`**. `CartPage` builds the
|
|
161
|
+
context once; `CartForm`/`CartFormSection` read context instead of props. Replace the hardcoded
|
|
162
|
+
union `defaultValues` (CartPage.tsx ~230–258) and the two `reset()` effects (~267, ~282) with the
|
|
163
|
+
config-derived builder (below).
|
|
164
|
+
|
|
165
|
+
**Phase 3 — Consolidate view-models + sync effects.** Merge `useEditCartViewModel` +
|
|
166
|
+
`useEditOrderViewModel` + the dispatcher into **`useCartFormViewModel`** (data). Move the three
|
|
167
|
+
duplicated effects into **`useCartFormSync`**: (a) on-mount populate from `salesOrder` (use a
|
|
168
|
+
`hasSyncedOnMount` ref for run-once — the branch did this correctly), (b) orderFor-change cascade,
|
|
169
|
+
(c) form→store sync — branching on `inEditMode` *once* inside each effect, not across two files.
|
|
170
|
+
Drive client-specific seeding by **rule (C6.2)**, not client name.
|
|
171
|
+
> **⚠ Parity gates — this is exactly where the prior spike silently regressed. Verify each against the Phase 0 oracle:**
|
|
172
|
+
> - email auto-population (contact / supervisor / supported-by / current user) for **both** modes;
|
|
173
|
+
> - duplicate-email path still surfaces `duplicateEmailAddressError` to the user (dedupe must not swallow it);
|
|
174
|
+
> - edit-order cost-center "prefer fresh user data over stale salesOrder" logic;
|
|
175
|
+
> - effect dependency arrays — no stale closures; justify every `eslint-disable`.
|
|
99
176
|
|
|
100
|
-
|
|
101
|
-
`
|
|
177
|
+
**Phase 4 — Section layout from config (C2/C3/C5). 🔴 The core of the refactor.** Add
|
|
178
|
+
`type` + `order` + `settings.isVisible` to each section in config. Build **`CartFormRenderer`**:
|
|
179
|
+
order sections, dispatch `SECTION_RENDERERS[section.type]`, with an **explicit fallback (log + skip)**
|
|
180
|
+
for an unknown/missing type so a section can't silently vanish. Extract inline Summary / email /
|
|
181
|
+
Confirm JSX into `SummarySection` / `CartEditEmailSection` / `ConfirmSection`, each consuming its
|
|
182
|
+
config slice via context. Express edit-order-only pieces (original-address block) as
|
|
183
|
+
`visibleWhen: { context: "inEditMode", value: true }` — or confirm with product they're dropped.
|
|
184
|
+
Gate client-optional UI on **capability flags (C6.1)**. **Delete `CartForm.tsx`, `EditCart.tsx`,
|
|
185
|
+
`EditOrder.tsx`** once the renderer covers both modes and the oracle passes.
|
|
102
186
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
187
|
+
**Phase 5 — Hydration + standards cleanup (C4).** One `hydrateCartConfig` memo mapping
|
|
188
|
+
`labelButton.onClick` / `inputButtons[].valueKey` / `visibleWhen.context` strings → handlers &
|
|
189
|
+
predicates from context. Standards sweep on every file touched: no commented-out code, no
|
|
190
|
+
`console.log`; restore the cost-center `onError` Sentry path; convert `checkIsProceedToCheckoutEnabled`
|
|
191
|
+
to an options object (4+ params rule); replace remaining `any` (`UseFormReturn<any>`, untyped
|
|
192
|
+
`fields`); the magic shipping UUID is removed from code entirely — it now lives as a config
|
|
193
|
+
`defaultValue` (C7, Phase 2).
|
|
110
194
|
|
|
111
|
-
### Key principle
|
|
195
|
+
### Key principle — client behavior comes from config, never `if (client === ...)` (C6)
|
|
112
196
|
|
|
113
|
-
A new client must be onboarded by **editing `CARTPAGE.ts`**, not by adding `clientSlug` branches
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
197
|
+
A new client must be onboarded by **editing `CARTPAGE.ts`**, not by adding `clientSlug` branches.
|
|
198
|
+
Modeled exactly on toga2.5's SalesOrder approval pattern: config carries `approvalActionStatuses`
|
|
199
|
+
+ `approveButtonRule:{type:"requireStepTwoAssigned"}`; a pure `evaluateApproveRule(rule, ctx)`
|
|
200
|
+
switch over a discriminated union in `useApprovalModalViewModel.tsx` interprets it — the component
|
|
201
|
+
never knows it is COMPASS. Two flavors:
|
|
118
202
|
|
|
119
203
|
1. **Capability flags** — pure on/off presence, no runtime data:
|
|
120
|
-
`capabilities.duplicateKitGuardrail.isEnabled` (COMPASS true
|
|
121
|
-
flag to decide whether to mount `DuplicateKitGuardrail`.
|
|
204
|
+
`capabilities.duplicateKitGuardrail.isEnabled` (COMPASS `true`, QUAD omits → safe-off default).
|
|
205
|
+
The renderer reads the flag to decide whether to mount `DuplicateKitGuardrail`.
|
|
122
206
|
2. **Named rules** — conditional logic depending on runtime data, declared as `{ type }` and
|
|
123
|
-
evaluated generically
|
|
207
|
+
evaluated generically. e.g. `orderForSeedRules: [{type:"seedCostCenterFromUser"}, …]` iterated by
|
|
124
208
|
an `applySeedRule` evaluator in `useCartFormSync` (mirrors `evaluateApproveRule`). QUAD omits the
|
|
125
209
|
rules → seeds nothing, no branch.
|
|
126
210
|
|
|
127
|
-
|
|
211
|
+
**Rule of thumb:** if a behavior differs by client it must be reachable from `CARTPAGE.ts` as a flag
|
|
212
|
+
or a `{ type }` rule. No `clientSlug` comparisons anywhere in `src/pages/Cart`.
|
|
213
|
+
|
|
214
|
+
### Config-derived form defaults (C7 — cart-specific extension of toga2.5)
|
|
128
215
|
|
|
129
216
|
The config already enumerates exactly the fields a client renders, so derive `useForm`
|
|
130
|
-
`defaultValues` from it via
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
217
|
+
`defaultValues` from it via `buildCartDefaultValues(fields, ctx)` (`ctx = { user, salesOrder, emails,
|
|
218
|
+
inEditMode }`) with three sources, in precedence order per field:
|
|
219
|
+
|
|
220
|
+
1. **per-field `defaultValue` in config** — where the COMPASS shipping UUID **moves to**; QUAD omits
|
|
221
|
+
it and falls through;
|
|
222
|
+
2. **dynamic `defaultValueRules`** reusing the C6.2 `{type}` rule shape for state-dependent values
|
|
223
|
+
(e.g. `{valueKey:"orderFor", type:"currentUserWhenNotEditable"}`,
|
|
224
|
+
`{valueKey:"costCenter", type:"fromSalesOrderErpEntity"}`);
|
|
225
|
+
3. **type-derived empties** otherwise (`text`/`textArea`→`""`, `checkbox`→`false`,
|
|
226
|
+
`select`/`advancedSelect`→`{uuid:"",name:""}`).
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const defaultValues = useMemo(() => buildCartDefaultValues(fields, ctx), [fields, /* … */]);
|
|
230
|
+
const formMethods = useForm({ mode: "all", defaultValues });
|
|
231
|
+
// both mode-change reset()s call the SAME builder: reset(buildCartDefaultValues(fields, ctx))
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Result: `useForm` registers **exactly** the fields the client collects — no union object, no orphan
|
|
235
|
+
defaults, no controlled/uncontrolled warnings; resets stay config-consistent; the shipping UUID and
|
|
236
|
+
any client default live in `CARTPAGE.ts`; new-client onboarding needs no `CartPage` edit. Noted
|
|
237
|
+
honestly: toga2.5 uses a *static* `emptyItemDefaults` because its item-create form is single-shape;
|
|
238
|
+
the cart is genuinely multi-shape per client, so config-derived defaults is a **cart-specific
|
|
239
|
+
extension** of toga2.5's philosophy, not a literal copy.
|
|
141
240
|
|
|
142
241
|
> The general "no `if (client === ...)` — drive client behavior from config via capability flags and
|
|
143
|
-
> named rules
|
|
144
|
-
>
|
|
242
|
+
> named rules" rule, applied across 2.0 React frontends beyond the cart, was proposed as a team
|
|
243
|
+
> standard separately and **declined** for now (the principle stays scoped to this cart doc).
|
|
244
|
+
|
|
245
|
+
## Definition of Done
|
|
246
|
+
|
|
247
|
+
- [ ] Single key name (`fields`) across `src/pages/Cart`; `CartForm`/`EditCart`/`EditOrder` deleted.
|
|
248
|
+
- [ ] One render path serves cart **and** edit-order; renderer has a fallback for unknown section types.
|
|
249
|
+
- [ ] Parity verified against the Phase 0 oracle: all sections render & submit; validation messages
|
|
250
|
+
fire; email seeding + duplicate-email error reproduced in **both** modes.
|
|
251
|
+
- [ ] No prop-drilling (context); VMs and sync effects consolidated; no stale-closure deps.
|
|
252
|
+
- [ ] **No `clientSlug`/client-name comparisons in `src/pages/Cart` (C6)** — every client difference
|
|
253
|
+
(DuplicateKitGuardrail, order-for seeding, tax/shipping calc) is a capability flag or named
|
|
254
|
+
rule; a client that omits a key gets the safe default.
|
|
255
|
+
- [ ] **Form `defaultValues` built from config (C7)** — registers exactly the fields the client
|
|
256
|
+
collects; no hardcoded union object; shipping-method UUID lives in config; resets use the builder.
|
|
257
|
+
- [ ] Standards clean: no dead code / `console.log`, cost-center error reaches Sentry, no new `any`.
|
|
258
|
+
- [ ] `tsc` clean; smoke tests green; PR body documents both test flows + the
|
|
259
|
+
`inputs/formFields → fields` migration.
|
|
260
|
+
|
|
261
|
+
## Sequencing & risk
|
|
262
|
+
|
|
263
|
+
- **Phase 1 ships independently** (pure rename, behavior-neutral) — land it first to de-risk the rest.
|
|
264
|
+
- **Phase 4 is the heart**; it depends on Phases 2–3 so the renderer has context + consolidated data.
|
|
265
|
+
- **Biggest risk is behavior parity, not rendering.** The dual forms hold real logic (email seeding,
|
|
266
|
+
duplicate validation, edit-order cost-center/address). Phase 0's oracle and Phase 3's parity gates
|
|
267
|
+
exist specifically because the prior spike deleted −3,940 lines and lost behavior static reading
|
|
268
|
+
could not catch. Migrate in verifiable slices against a running `_production`.
|
|
269
|
+
|
|
270
|
+
## Open questions for kickoff
|
|
271
|
+
|
|
272
|
+
*(Approach is settled: rebuild from `_production` with the branch as reference.)*
|
|
273
|
+
1. **Config key name** — `fields` (toga2.5 standard, recommended) vs keep per-section names.
|
|
274
|
+
2. **Edit-order original-address block** — still required, or intentionally dropped?
|
|
275
|
+
3. **Edit-order duplicate-user guard** — the prior spike made it "always applied"; intended for
|
|
276
|
+
edit-order mode?
|
|
145
277
|
|
|
146
278
|
## Change history
|
|
147
279
|
- 2026-06-24 — Initial: documented the current `_production` Cart architecture (field-level config
|
|
148
280
|
via `CartFormSection`, hardcoded section layout in `CartForm`, dual `EditCart`/`EditOrder` forms +
|
|
149
|
-
dispatcher VM, prop-drilling), its gotchas (config-key drift `
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
281
|
+
dispatcher VM, prop-drilling), its gotchas (config-key drift, hardcoded union `defaultValues` with
|
|
282
|
+
the COMPASS magic shipping UUID, hardcoded per-client branches), and the DECIDED config-driven
|
|
283
|
+
refactor approach. (apeterson)
|
|
284
|
+
- 2026-06-24 — Expanded to be self-sufficient for handoff: added the C1–C7 target conventions table
|
|
285
|
+
with toga2.5 reference files, the gap→work table, the full phased plan (incl. Phase 0 oracle +
|
|
286
|
+
smoke tests and Phase 3 parity gates), Definition of Done, sequencing/risk, and open questions.
|
|
287
|
+
Fixed stray closing tags accidentally written into the file. (apeterson)
|
|
@@ -193,9 +193,30 @@ backfills `workTypeAtLock` the first time a task is seen).
|
|
|
193
193
|
and the committed values should be rotated. See team coding/security standards.
|
|
194
194
|
- **Worker contract.** Like all 2.0 workers this must return cleanly and commit writes via
|
|
195
195
|
the `_Db`/transaction layer to be durable — uncommitted captures can be silently dropped.
|
|
196
|
+
- **ClickUp pagination returns duplicate tasks — the sync loop must be idempotent.** ClickUp's
|
|
197
|
+
page-based task API (`/list/{id}/task?page=N`) can return the **same task on more than one
|
|
198
|
+
page**, especially on large/long runs (a real failure ran ~418s across many pages). If the
|
|
199
|
+
sync loop treats a re-seen task as new, it builds a second `_Model_Team_Task` (id=NULL) and
|
|
200
|
+
`save()`s it → a second INSERT → `Duplicate entry '<sprintId>-<clickupId>' for key
|
|
201
|
+
'Tasks.sprintId_clickupIdentifier'` and the whole run aborts. **Design rule (the two-map
|
|
202
|
+
pattern):** keep two separate maps keyed by `clickupIdentifier`. `$lookupTaskByClickUpId`
|
|
203
|
+
drives update-vs-insert and **must stay current for the entire run** — register every
|
|
204
|
+
newly-created task into it immediately and never `unset()` it mid-loop, so a duplicate page
|
|
205
|
+
row resolves to the same model and does an UPDATE. A separate `$tasksToDelete` map (seeded
|
|
206
|
+
from existing tasks) has entries removed as each task is seen; whatever remains after the
|
|
207
|
+
loop is deleted (SprintLock) / archived to `statusNow = ARCHIVED` (the Capture methods).
|
|
208
|
+
Do **not** overload one map for both jobs. This pattern is replicated identically in
|
|
209
|
+
`SprintLock()`, `CaptureSprintEnd()`, and `CaptureSprintDaily()` — fix all three together.
|
|
196
210
|
|
|
197
211
|
## Change history
|
|
198
212
|
|
|
213
|
+
- 2026-06-24 — Fixed a duplicate-key crash (`Tasks.sprintId_clickupIdentifier`) in the
|
|
214
|
+
ClickUp→DB sync: ClickUp pagination can return the same task on multiple pages, and the
|
|
215
|
+
loop was overloading one map (`$lookupTaskByClickUpId`) for both update-vs-insert and
|
|
216
|
+
deletion tracking, `unset()`ing entries mid-loop so a re-seen task was treated as new and
|
|
217
|
+
re-INSERTed. Decoupled into two maps — a persistent `$lookupTaskByClickUpId` (new tasks
|
|
218
|
+
registered immediately, never unset) and a separate `$tasksToDelete` — making re-processing
|
|
219
|
+
idempotent. Applied to `SprintLock()`, `CaptureSprintEnd()`, and `CaptureSprintDaily()`. (jcardinal)
|
|
199
220
|
- 2026-06-24 — Initial documentation of `_Worker_Team_Sprint` (`Worker/Team/Sprint.php`):
|
|
200
221
|
the sprint lifecycle (warnings → lock → daily → end → release notes), the four-metric
|
|
201
222
|
developer scoring model (reliability/quality/hygiene/integrity with weights, team-relative
|
package/package.json
CHANGED