toga-ai 1.0.284 → 1.0.286
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/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/architecture.md +12 -1
- package/knowledge/2.0/apps/_underscore/features/async-query-execution.md +89 -0
- package/knowledge/2.0/apps/worker2/architecture.md +84 -15
- package/knowledge/2.0/apps/worker2/features/clickup-work-type-automation.md +19 -1
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
|
|
7
7
|
| [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | `_Model_Client_Address::validateAddress` verifies a US address against a **carrier waterfall (USPS → FedEx → UPS)** and returns a single canonical, carrier-norm | _underscore/Model/Client/Address.php |
|
|
8
8
|
| [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
|
|
9
|
+
| [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
|
|
9
10
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
10
11
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
11
12
|
| [Error Reporting — Issue/Event Aggregation (exceptionHandler)](features/error-reporting-issue-event.md) | `_underscore`'s global exception handler persists every uncaught exception into a two-table **Issue / Event** model in the **shared Core Logs DB** (`_underscore | _underscore/Error.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, dbchanges2/Logs/2026-07-06 - Issue and Event tables.sql |
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-07
|
|
10
10
|
owners: ["jcardinal", "rgirish", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/_underscore.php
|
|
@@ -119,6 +119,16 @@ multi-tenant client-DB lookup from Core. `Query.php` wraps MySQLi with read/writ
|
|
|
119
119
|
auto-routing and result helpers: `fetchRows()`, `fetchRow()`, `fetchOne()`,
|
|
120
120
|
`fetchRowsAssocArray()`, `fetchKeyValuePairs()`, `fetchValues()`.
|
|
121
121
|
|
|
122
|
+
`_Query` also supports a **writes-only async mode** (`isAsync` — 3rd constructor arg /
|
|
123
|
+
`setIsAsync()`): a slow WRITE is dispatched to the Worker tier via `_Worker::runTask()`
|
|
124
|
+
instead of running on the request connection, so a long write cannot hold a request-scoped
|
|
125
|
+
connection open long enough to hit **"MySQL server has gone away."** See
|
|
126
|
+
[features/async-query-execution.md](./features/async-query-execution.md). This rides on the
|
|
127
|
+
**SQS-first** `_Worker::runTask()` enqueue path (below): `runTask()` no longer INSERTs the
|
|
128
|
+
`WorkerJobs` row on the caller's connection — it sends `{uuid, action, parameters}` to the
|
|
129
|
+
worker SQS queue and the worker tier creates/tracks the row on its own fresh connection at
|
|
130
|
+
pickup (see the worker2 architecture doc's SQS-first decision).
|
|
131
|
+
|
|
122
132
|
### Physical infrastructure
|
|
123
133
|
|
|
124
134
|
Each **environment** is one or more MySQL clusters. Every cluster has a single
|
|
@@ -338,3 +348,4 @@ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentNa
|
|
|
338
348
|
- 2026-06-25 — Added the Surface platform UI presentation/configuration layer (DB-driven UI config replacing `Page::meta()`, CTO-reviewed AGREE-WITH-ADJUSTMENTS) (jcardinal)
|
|
339
349
|
- 2026-06-29 — Surface made the enforced (un-flagged) presentation path; reserved id blocks renumbered to Records 333–341 / RecordFields 2246–2433 (known seed-vs-provisioned drift). (jcardinal)
|
|
340
350
|
- 2026-07-02 — Fixed framework-wide `FIELD_STORAGE` write-drop and folder-read bugs in `Model.php`; noted the remaining unconditional-refetch dirty-read follow-up. (mhammontree)
|
|
351
|
+
- 2026-07-07 — Documented `_Query` writes-only async mode (`isAsync` → Worker `Infrastructure/Database/Query`) in the database-architecture section, linked `features/async-query-execution.md`, and noted the SQS-first `_Worker::runTask()` enqueue departure (caller does no MySQL; worker tier creates the row on pickup). (jcardinal)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Asynchronous Query Execution (writes-only, via Worker)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-07
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Query.php
|
|
13
|
+
- worker2/Worker/Infrastructure/Database.php
|
|
14
|
+
- worker2/Worker/Team/Transcripts.php
|
|
15
|
+
related:
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- ./per-client-database-connections.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
`_Query` can run a **write** query asynchronously so a long/slow write does not hold a
|
|
23
|
+
request-scoped DB connection open long enough to hit **"MySQL server has gone away"**.
|
|
24
|
+
Instead of executing on the local connection, the query is dispatched as a Worker task
|
|
25
|
+
(`Infrastructure/Database/Query`) that opens its own connection on the target DB, runs the
|
|
26
|
+
write, and commits. It is **writes-only by design** — an async SELECT captures no result set
|
|
27
|
+
and is a harmless no-op.
|
|
28
|
+
|
|
29
|
+
## Key files / entry points
|
|
30
|
+
|
|
31
|
+
- **`_underscore/Query.php`** — `_Query` gained an `isAsync` flag: constructor 3rd param
|
|
32
|
+
`bool $isAsync = false`, a `setIsAsync()` setter, and an `_isAsync` property. When async,
|
|
33
|
+
`execute()` resolves the target DB connection config and dispatches a Worker task instead of
|
|
34
|
+
running locally.
|
|
35
|
+
- **`worker2/Worker/Infrastructure/Database.php`** — `_Worker_Infrastructure_Database::Query()`
|
|
36
|
+
is the worker-side action that runs the write on the target DB and commits.
|
|
37
|
+
- **`worker2/Worker/Team/Transcripts.php`** — live example caller in `SyncKnowledgeBases()`
|
|
38
|
+
(lines ~911 / ~925).
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
1. **Opt in.** Construct with the third arg true:
|
|
43
|
+
`new _Query($sql, _underscore::DB_TEAM, true);` (or call `setIsAsync(true)` before the
|
|
44
|
+
query is set).
|
|
45
|
+
2. **Dispatch (in `execute()`).** When `_isAsync` is true, `execute()` does **not** run the
|
|
46
|
+
query locally. It resolves the target DB connection config — from `_Database::$_registers`
|
|
47
|
+
or the `_Config` `DATABASE` group (direct hostname, or `writehost`/`readhost` + `backuphost`
|
|
48
|
+
params) — and calls
|
|
49
|
+
`_Worker::runTask(action: 'Infrastructure/Database/Query', parameters: {hostname, database,
|
|
50
|
+
username, password, query})`.
|
|
51
|
+
3. **Worker side.** `_Worker_Infrastructure_Database::Query()` calls `_Database::register(...)`
|
|
52
|
+
under the alias `'Query'`, runs `new _Query($query, 'Query')` to execute the write on the
|
|
53
|
+
worker's own connection, then commits with `_Database::transactionCommit('Query')`.
|
|
54
|
+
4. **Writes only.** Async SELECT captures no result set (fire-and-forget) and is treated as a
|
|
55
|
+
harmless no-op — do not use async for reads.
|
|
56
|
+
|
|
57
|
+
## Correctness gotchas (all fixed this session — do not regress)
|
|
58
|
+
|
|
59
|
+
- **Constructor order (critical).** `setIsAsync()` must run **before** `setQuery()`, because
|
|
60
|
+
`setQuery()` executes non-SELECT queries immediately. With the original order (setQuery first)
|
|
61
|
+
every async WRITE ran synchronously and the async path was never taken. The constructor was
|
|
62
|
+
reordered to set the async flag first.
|
|
63
|
+
- **Worker-side commit (critical).** `_Database::register()` with an alias auto-starts a lazy
|
|
64
|
+
transaction; the write flips autocommit off and is rolled back when the worker connection
|
|
65
|
+
closes unless explicitly committed. The worker action must call
|
|
66
|
+
`_Database::transactionCommit('Query')` after the write. This is the known _underscore
|
|
67
|
+
[lazy-transaction gotcha](../architecture.md) — the same trap that silently discarded Rate
|
|
68
|
+
SAML user INSERTs.
|
|
69
|
+
- **Double-dispatch.** The async branch leaves `_results` null, and `execute()` is guarded by
|
|
70
|
+
`is_null(_results)`, so `__destruct()`/getters re-fire the job. Fixed by setting a sentinel
|
|
71
|
+
(`_results = true`) at the end of the async branch.
|
|
72
|
+
- **Dangling local transaction.** The original async check ran **after** the
|
|
73
|
+
write-connection/transaction setup, so a dispatching async write opened a local writer
|
|
74
|
+
connection and started a local transaction it never committed. Fixed by moving the async
|
|
75
|
+
dispatch to the **top** of `execute()`, before any read/write connection or transaction setup.
|
|
76
|
+
|
|
77
|
+
## Known open items
|
|
78
|
+
|
|
79
|
+
- **SECURITY (open, developer aware).** DB username/password are currently sent in **plaintext**
|
|
80
|
+
through the Worker queue payload (SQS + `WorkerJobs` table). Being handled separately: the
|
|
81
|
+
worker should resolve credentials from config by database name rather than receiving them in
|
|
82
|
+
the payload. Do not build on the plaintext-credential path.
|
|
83
|
+
|
|
84
|
+
## Change history
|
|
85
|
+
|
|
86
|
+
- 2026-07-07 — Built writes-only async query execution (`_Query` `isAsync` → Worker
|
|
87
|
+
`Infrastructure/Database/Query` action); fixed four correctness bugs found in review
|
|
88
|
+
(constructor order, worker-side commit, double-dispatch sentinel, dangling local
|
|
89
|
+
transaction). Credential-in-payload security item left open. (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-07
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Controller/Index.php
|
|
@@ -15,6 +15,7 @@ files:
|
|
|
15
15
|
- _underscore/Worker.php
|
|
16
16
|
related:
|
|
17
17
|
- ./features/creating-worker-actions.md
|
|
18
|
+
- ../_underscore/features/async-query-execution.md
|
|
18
19
|
---
|
|
19
20
|
|
|
20
21
|
## Summary
|
|
@@ -92,8 +93,29 @@ New path (`{workerJobId}` present):
|
|
|
92
93
|
5. Call `initialize()` if present, then the action with spread parameters.
|
|
93
94
|
6. **Check-out:** `UPDATE dtCompleted, executionTime, isSuccess, failureReason`. Return 200.
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
Direct-payload path (`action` present, no `workerJobId`): formerly the debug-only "legacy"
|
|
97
|
+
path (untracked, could return HTTP 500). As of 2026-07-07 it is a **first-class tracked
|
|
98
|
+
path** — it is how the SQS-first `_Worker::runTask()` (below) delivers production jobs. The
|
|
99
|
+
message carries `{uuid, action, parameters}`; the worker creates and tracks the `WorkerJobs`
|
|
100
|
+
row on its own fresh connection and **always returns HTTP 200** (no DLQ + 3600s visibility
|
|
101
|
+
timeout means any 500 = poison-message storm):
|
|
102
|
+
|
|
103
|
+
1. **Dedupe guard.** SELECT `WorkerJobs` by `uuid`. Skip (200) **only if `isSuccess IS NOT
|
|
104
|
+
NULL`** (already completed). A row that exists but is incomplete (prior crash / in-flight)
|
|
105
|
+
is **re-run**, not dropped — mirrors the `workerJobId` guard. (SQS is at-least-once.)
|
|
106
|
+
2. **New job → INSERT** `(uuid, jobType='ACTION', action, parameters, dtQueued=NOW(),
|
|
107
|
+
dtStarted=NOW(), instanceId)`. A UNIQUE index on `WorkerJobs.uuid` (added in `dbchanges2`)
|
|
108
|
+
backstops the SELECT→INSERT TOCTOU race.
|
|
109
|
+
3. **INSERT-failure disambiguation** is by **re-resolving the uuid**, not by parsing the
|
|
110
|
+
error code: if the row now exists it was a benign concurrent duplicate (skip 200);
|
|
111
|
+
otherwise the write genuinely failed — log CRITICAL + `\Sentry\captureException`, still
|
|
112
|
+
return 200.
|
|
113
|
+
4. `dtQueued=NOW()` at insert (not NULL) so JobScheduler's `dtQueued IS NULL` recovery does
|
|
114
|
+
not re-queue these rows; the watchdog still covers them via `dtStarted`.
|
|
115
|
+
|
|
116
|
+
> **Known follow-up (not done):** the ~100-line log-DB-registration + `initialize()` blocks
|
|
117
|
+
> are duplicated between the `workerJobId` path and the direct-payload path — candidate for
|
|
118
|
+
> extraction to shared private helpers.
|
|
97
119
|
|
|
98
120
|
## Action → class/method routing
|
|
99
121
|
|
|
@@ -103,9 +125,17 @@ file `Worker/Team/Github.php`.
|
|
|
103
125
|
|
|
104
126
|
## Programmatic invocation & manual retry
|
|
105
127
|
|
|
106
|
-
- **`_Worker::runTask(action, parameters)`** (`_underscore/Worker.php`) —
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
- **`_Worker::runTask(action, parameters)`** (`_underscore/Worker.php`) — **SQS-first as of
|
|
129
|
+
2026-07-07:** the production branch does **no MySQL** (no INSERT, no commit). It generates a
|
|
130
|
+
client-side `uuid` and sends `{uuid, action, parameters}` to the worker SQS queue; the worker
|
|
131
|
+
tier creates+tracks the `WorkerJobs` row on pickup (see direct-payload path above). Payload is
|
|
132
|
+
capped at `_Worker::SQS_MAX_MESSAGE_BYTES = 262144` — `runTask()` **throws** if the JSON
|
|
133
|
+
exceeds the SQS 256KB limit (loud failure, not a silent drop); oversized params must be
|
|
134
|
+
offloaded to S3 by the caller. Debug branch unchanged (synchronous POST). **Why:** long-running
|
|
135
|
+
scripts hit "MySQL server has gone away" on the caller's stale Core connection during the old
|
|
136
|
+
INSERT; SQS sends do not depend on the DB connection, so row creation moved off the caller
|
|
137
|
+
entirely. This is the enqueue path used by `_Query` writes-only async mode. See the SQS-first
|
|
138
|
+
decision + trade-off under *Key design decisions*.
|
|
109
139
|
- **`_Worker_Infrastructure_Worker::Retry($workerJobId)`** — resets the job fields,
|
|
110
140
|
commits before SQS, sends `{workerJobId}` immediately. Sets `dtQueued=NOW()` (not NULL)
|
|
111
141
|
so JobScheduler doesn't double-pick it.
|
|
@@ -118,17 +148,56 @@ file `Worker/Team/Github.php`.
|
|
|
118
148
|
## Critical transaction pattern
|
|
119
149
|
|
|
120
150
|
**Any INSERT/UPDATE that must be visible to another connection before an SQS message is
|
|
121
|
-
delivered must be immediately committed.**
|
|
122
|
-
|
|
151
|
+
delivered must be immediately committed.** Sites: `Retry()` and the `Controller/Index.php`
|
|
152
|
+
check-in. (`_Worker::runTask()` no longer applies as of 2026-07-07 — it does no MySQL; the
|
|
153
|
+
worker tier creates the row itself on pickup.) Reason: the caller's MySQL transaction is still
|
|
123
154
|
open when SQS delivers; the worker reads in a separate connection and, if uncommitted, its
|
|
124
155
|
SELECT returns nothing and the guard fires ("already processed or not found — skipping").
|
|
125
156
|
|
|
126
157
|
## Key design decisions
|
|
127
158
|
|
|
128
|
-
MySQL-first (SQS on success) prevents lost
|
|
129
|
-
failures, SQS retry unwanted) · 3600s visibility
|
|
130
|
-
(developer-controlled `Retry()` preferred) · all fallback
|
|
131
|
-
SQS failure → reset `dtQueued=NULL` (rescheduled next
|
|
132
|
-
CronJobs, env var for ACTION jobs · `/Webhook` appended
|
|
133
|
-
API Gateway in front of Lambda (SCP blocks Function URLs) ·
|
|
134
|
-
messages tiny · commit before SQS avoids the
|
|
159
|
+
MySQL-first for the **Lambda/webhook/cron ingestion paths** (SQS on success) prevents lost
|
|
160
|
+
jobs · always HTTP 200 (WorkerJobs tracks failures, SQS retry unwanted) · 3600s visibility
|
|
161
|
+
timeout for hour-long jobs · no DLQ (developer-controlled `Retry()` preferred) · all fallback
|
|
162
|
+
payloads to S3 (no size split) · SQS failure → reset `dtQueued=NULL` (rescheduled next
|
|
163
|
+
minute) · `maxExecutionTime` on CronJobs, env var for ACTION jobs · `/Webhook` appended
|
|
164
|
+
enforces method-name convention · API Gateway in front of Lambda (SCP blocks Function URLs) ·
|
|
165
|
+
`workerJobId` lookup keeps SQS messages tiny · commit before SQS avoids the
|
|
166
|
+
delivery-before-commit race.
|
|
167
|
+
|
|
168
|
+
### `_Worker::runTask()` is SQS-first, NOT MySQL-first (2026-07-07 — intentional departure)
|
|
169
|
+
|
|
170
|
+
For **programmatic** enqueue via `_Worker::runTask()`, the MySQL-first rule was deliberately
|
|
171
|
+
reversed. Long-running PHP scripts were hitting "MySQL server has gone away" on the caller's
|
|
172
|
+
`WorkerJobs` INSERT because the caller's Core connection had gone **stale** (not because the
|
|
173
|
+
server was dying). Approach chosen (**A — SQS-first**): the caller does no MySQL; it sends
|
|
174
|
+
`{uuid, action, parameters}` to SQS and the worker tier creates+tracks the row on its own
|
|
175
|
+
fresh connection at pickup.
|
|
176
|
+
|
|
177
|
+
**Rejected alternative (CTO-recommended):** Approach C/D — reconnect-on-stale inside
|
|
178
|
+
`_Database`, keeping the durable id-based (MySQL-first) pipeline. The CTO review advised
|
|
179
|
+
**against** Approach A because the failure is a stale PHP connection rather than a dying
|
|
180
|
+
server, and because promoting the debug-only untracked path risked poison-message storms and
|
|
181
|
+
duplicate execution. The developer chose Approach A anyway, **accepting the durability
|
|
182
|
+
trade-off**: a bare SQS-send failure loses the job, and jobs are not visible in `WorkerJobs`
|
|
183
|
+
until worker pickup. The CTO review's known-danger mitigations were then implemented — uuid
|
|
184
|
+
dedupe that re-runs incomplete rows, UNIQUE `uuid` index for the TOCTOU race, uuid
|
|
185
|
+
re-resolution to disambiguate INSERT failures (CRITICAL log + Sentry on genuine failure),
|
|
186
|
+
always-200, and a 256KB payload cap that throws. Record kept so a future reader understands
|
|
187
|
+
the MySQL-first design was departed from **on purpose** here.
|
|
188
|
+
|
|
189
|
+
**Gotchas from this change:**
|
|
190
|
+
- `dtQueued` on these rows now reflects worker pickup/row-creation time, **not** true SQS
|
|
191
|
+
enqueue time — semantic drift for any queue-latency metric.
|
|
192
|
+
- Debug/legacy messages without a `uuid` get a random one, so dedupe does not apply to them —
|
|
193
|
+
an HTTP-layer retry of a debug call could double-run.
|
|
194
|
+
|
|
195
|
+
## Change history
|
|
196
|
+
|
|
197
|
+
- 2026-06-08 — Initial worker2 architecture doc. (jcardinal)
|
|
198
|
+
- 2026-07-07 — `_Worker::runTask()` moved to SQS-first (caller does no MySQL; worker tier
|
|
199
|
+
creates/tracks the row on pickup); the former debug-only direct-payload path in
|
|
200
|
+
`Controller/Index.php` promoted to a first-class tracked path with uuid dedupe, UNIQUE-index
|
|
201
|
+
TOCTOU backstop, uuid re-resolution on INSERT failure, and a 256KB payload cap. Documented as
|
|
202
|
+
an intentional departure from MySQL-first (CTO recommended against; trade-off accepted).
|
|
203
|
+
Related: `_underscore` `_Query` writes-only async mode. (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-08
|
|
10
10
|
owners: ["jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Clickup.php
|
|
@@ -69,8 +69,26 @@ None — internal team/sprint tooling, uniform across clients.
|
|
|
69
69
|
- **No PHPUnit harness** in worker2. The regression test
|
|
70
70
|
`Tests/Worker/ClickupWorkTypeTest.php` is a plain-PHP script (reflection on
|
|
71
71
|
`isStatusComplete`) run with `php Tests/Worker/ClickupWorkTypeTest.php`.
|
|
72
|
+
- **ClickUp dropdown custom-field `value` is the option ORDERINDEX, not its name.** The
|
|
73
|
+
webhook payload for a dropdown/labels custom field delivers the selected option's integer
|
|
74
|
+
`orderindex`, *not* the option's display name. Any name-keyed lookup (e.g.
|
|
75
|
+
`$lookupPreviousSprintWorkTypeByName`) will silently never match if fed the raw `value`.
|
|
76
|
+
Resolve the name first via the field's own `type_config->options` — build a map keyed by
|
|
77
|
+
`orderindex` (`$workTypesByIndex[$opt->orderindex] = $opt`) and read `->name` from it. This
|
|
78
|
+
is the proven pattern in `_Worker_Team_Sprint::SprintLock` (`Worker/Team/Sprint.php`, the
|
|
79
|
+
Work Type resolve ~L3894 and the POST ~L3971). Guard the lookup with
|
|
80
|
+
`isset($workTypesByIndex[$value])` so a stale/deleted option orderindex doesn't warn.
|
|
72
81
|
|
|
73
82
|
## Change history
|
|
83
|
+
- 2026-07-08 — Fixed **Previous Sprint Work Type** never being set on tasks created after a
|
|
84
|
+
sprint launched (`taskCreated` case, `Worker/Clickup.php` ~L715-751). Root cause: the handler
|
|
85
|
+
assigned `$workType = $customField->value` (the ClickUp option orderindex, an int) then looked
|
|
86
|
+
it up in a name-keyed map, so the `isset()` guard never matched and the POST silently never
|
|
87
|
+
fired. Now resolves orderindex→name via the WORKTYPE field's `type_config->options`
|
|
88
|
+
(`$workTypesByIndex`), matching the `SprintLock` convention, with an added
|
|
89
|
+
`isset($workTypesByIndex[$value])` guard. Also added: empty Work Type now defaults to
|
|
90
|
+
`Unplanned` before the Previous Sprint Work Type lookup so newly-created tasks still get a
|
|
91
|
+
value. `php -l` clean; php-reviewer approved. (jcardinal)
|
|
74
92
|
- 2026-06-23 — Fixed dependents staying Conditional after a blocker completed: completion check
|
|
75
93
|
now treats `closed` as terminal alongside `done`, and `taskStatusUpdated` re-evaluates
|
|
76
94
|
dependents (gated to terminal statuses). Added `COMPLETE_STATUS_TYPES`, `isStatusComplete()`,
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
20
|
+
- **_underscore** (_Underscore) _(framework core)_ — 25 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 27 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
22
|
- **api2** (API) — 10 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
package/package.json
CHANGED