toga-ai 1.0.332 → 1.0.334
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/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/features/language-translation-layer.md +47 -5
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/clickup-project-routing.md +7 -2
- package/knowledge/2.0/apps/worker2/features/clickup-subtask-activity.md +117 -0
- package/knowledge/2.0/apps/worker2/features/etilize-item-translation-import.md +38 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-canada/INDEX.md +1 -0
- package/knowledge/clients/compass-canada/features/french-item-feature-translations.md +96 -0
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
| [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
|
|
6
6
|
| [Multi-Client (Cross-Client) Data Retrieval](features/cross-client-data-retrieval.md) | A single authenticated V2 GET listing can return records across **many** clients (designed for 1000+) that the caller is entitled to, honoring **each target cli | api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php, api2/_.php, _underscore/Model/Cache/Table.php, _underscore/Model/Cache/Tables/Client.php, dbchanges2/Core/2026-06-30b - CacheClusterRegistrationAndRecordTtl.sql, dbchanges2/Cache/2026-06-30a - MultiClientCacheTables.sql |
|
|
7
7
|
| [Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)](features/encrypted-user-uuid-auth-handoff.md) | `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**: given an encrypted `{client, user}` UUID pair, it mints a fr | api2/Component/Api/CrossClient/CrossClient.php |
|
|
8
|
-
| [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription,
|
|
8
|
+
| [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text — `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql |
|
|
9
9
|
| [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php |
|
|
10
10
|
| [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
|
|
11
11
|
| [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-14
|
|
10
10
|
owners: ["jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -15,10 +15,16 @@ files:
|
|
|
15
15
|
- _underscore/Model/Core/RecordField.php
|
|
16
16
|
- _underscore/Model/Core/DefaultGlobalSetting.php
|
|
17
17
|
- _underscore/Model/Client/ItemTranslation.php
|
|
18
|
+
- _underscore/Model/Client/FeatureTranslation.php
|
|
19
|
+
- _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php
|
|
20
|
+
- _underscore/Model/Client/ItemFeatureTranslation.php
|
|
18
21
|
- dbchanges2/Client/2026-06-23a - ItemTranslations.sql
|
|
19
22
|
- dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql
|
|
23
|
+
- dbchanges2/Client/2026-07-13a - FeatureTranslations.sql
|
|
24
|
+
- dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql
|
|
20
25
|
- dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql
|
|
21
26
|
- dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql
|
|
27
|
+
- dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql
|
|
22
28
|
related:
|
|
23
29
|
- ./acl-permission-chain.md
|
|
24
30
|
- ../../_underscore/features/model-magic-field-access.md
|
|
@@ -26,8 +32,9 @@ related:
|
|
|
26
32
|
|
|
27
33
|
## Summary
|
|
28
34
|
|
|
29
|
-
Serves the same TOGa data (Item title/description/longDescription,
|
|
30
|
-
|
|
35
|
+
Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text —
|
|
36
|
+
`Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures.value`/`presentationValue`) in
|
|
37
|
+
multiple languages without forking the schema or breaking English consumers. English stays in the source
|
|
31
38
|
table; per-language overlays live in per-table **sidecar** tables (`ItemTranslations` first).
|
|
32
39
|
The caller's language is resolved once at authentication, surfaced on every response's
|
|
33
40
|
`audience.language`, and embedded in the JWT; reads/writes are redirected to the sidecar in the
|
|
@@ -126,8 +133,19 @@ endpoint for those.
|
|
|
126
133
|
- `Core.RecordFields.translationRecordFieldId` — new nullable self-FK (after `recordId`).
|
|
127
134
|
- `Core.Records` 331 = `item-translations` (`aclDatabase='CLIENT'`); `Core.RecordFields` 2233–2239
|
|
128
135
|
(its fields), 2240 (the self-describing `translationRecordFieldId` field).
|
|
129
|
-
-
|
|
130
|
-
|
|
136
|
+
- **Feature-text sidecars** (same pattern as `ItemTranslations`, ship via `dbchanges2/Client/`):
|
|
137
|
+
- `Client.FeatureTranslations` — overlay for `Features.name`; `id, uuid, dtCreated, dtUpdated,
|
|
138
|
+
featureId (FK), languageId (FK), name`; `UNIQUE(featureId, languageId)`.
|
|
139
|
+
- `Client.ItemCategoryFeatureGroupTranslations` — overlay for `ItemCategoryFeatureGroups.name`;
|
|
140
|
+
`…, itemCategoryFeatureGroupId (FK), languageId, name`; `UNIQUE(fk, languageId)`.
|
|
141
|
+
- `Client.ItemFeatureTranslations` — overlay for `ItemFeatures.value` + `presentationValue`;
|
|
142
|
+
`…, itemFeatureId (FK), languageId, value, presentationValue`; `UNIQUE(fk, languageId)`.
|
|
143
|
+
- `Core.Records` 343 / 344 / 345 = the three above; `Core.RecordFields` 2442–2457. The
|
|
144
|
+
`translationRecordFieldId` column on the source fields (`features.name`,
|
|
145
|
+
`item-category-feature-groups.name`, `item-features.value`/`presentationValue`) is set by an
|
|
146
|
+
`UPDATE…JOIN` so `getTranslatedFieldValue()`/`shouldTranslate()` substitute the sidecar value on read.
|
|
147
|
+
- `item-translations` (and the feature sidecars') ACL chain lives in each client DB targeting the
|
|
148
|
+
Base role — see [acl-permission-chain.md](./acl-permission-chain.md).
|
|
131
149
|
|
|
132
150
|
## Client variations
|
|
133
151
|
|
|
@@ -157,6 +175,22 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
|
|
|
157
175
|
`$this->response->status` to `200` **before** finalization. An `is_null` guard there suppresses the
|
|
158
176
|
`201` + tokens and yields a broken `200`/`data:null` login. Branch on the explicit
|
|
159
177
|
`$invalidLanguageSetting` flag instead.
|
|
178
|
+
- **The all-fields (expand-everything) serializer branch must also translate.** `V2.php` has two
|
|
179
|
+
nested-child serialization paths: when the caller requests **specific** fields it already ran each
|
|
180
|
+
value through `getTranslatedFieldValue()`, but when the caller requested **no** specific fields
|
|
181
|
+
(empty `$varOptions`, i.e. "return everything I have access to") the code copied raw child values
|
|
182
|
+
straight into `$outData` and never translated — so any field returned via the all-fields expansion
|
|
183
|
+
stayed in base English even for a non-base-language user. Both branches must route through
|
|
184
|
+
`getTranslatedFieldValue($record, $field, $sourceModel, $defaultValue)` (preserving the
|
|
185
|
+
`renamedFields` alias). Symptom that found it: on the Compass Canada French storefront item-detail
|
|
186
|
+
page (loaded via `/bundles`), feature-spec **values** were French but the feature-group **headers**
|
|
187
|
+
(`itemCategoryFeatureGroup.name`, e.g. "Processeur"/"Mémoire") stayed English, because the group
|
|
188
|
+
object serialized through the all-fields branch. **Not a query-depth problem** — a commerce-side
|
|
189
|
+
`depth:6` theory was wrong and reverted; the fix returns French headers at the default `meta.depth 3`.
|
|
190
|
+
- **Sidecar/source collation mismatch.** The translation sidecar tables use `utf8mb4_unicode_ci`
|
|
191
|
+
(matching `ItemTranslations`) while the source fields are `utf8mb4_0900_ai_ci`. Ad-hoc queries that
|
|
192
|
+
compare a sidecar string against a source string must add `COLLATE utf8mb4_bin` or MySQL throws a
|
|
193
|
+
collation-mismatch error.
|
|
160
194
|
- Reading a possibly-absent magic field off a generic `_Model` needs `array_key_exists(...)` +
|
|
161
195
|
`__get` — `isset()`/`?? null` always read false/null. See
|
|
162
196
|
[_Model magic-field access](../../_underscore/features/model-magic-field-access.md); this was the
|
|
@@ -168,6 +202,14 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
|
|
|
168
202
|
|
|
169
203
|
## Change history
|
|
170
204
|
|
|
205
|
+
- 2026-07-13 — Extended the translation layer to item **feature** text: three new sidecar tables
|
|
206
|
+
(`FeatureTranslations`, `ItemCategoryFeatureGroupTranslations`, `ItemFeatureTranslations`; Records
|
|
207
|
+
343/344/345, RecordFields 2442–2457) with `translationRecordFieldId` wired to the source fields,
|
|
208
|
+
so feature names, group headers, and feature values translate on read exactly like Item text. Fixed
|
|
209
|
+
the all-fields (expand-everything) nested-child serializer branch in `V2.php` (~L5958) to route
|
|
210
|
+
through `getTranslatedFieldValue()` — previously it copied raw base-English child values (Compass
|
|
211
|
+
Canada French group headers stayed English while values were French); the `depth:6` theory was wrong
|
|
212
|
+
and reverted. Noted the sidecar/source collation-mismatch gotcha. (bala)
|
|
171
213
|
- 2026-06-23 — Initial build: audience.language + JWT embedding, ItemTranslations sidecar + metadata +
|
|
172
214
|
full ACL chain, and translation-aware read/write at the API layer. Also fixed a latent autoload bug
|
|
173
215
|
by renaming `DefaultFlobalSetting.php` → `DefaultGlobalSetting.php`. (jcardinal)
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| [ClickUp GitHub-tab Auto-linking & Ticket-id Branch Naming](features/clickup-github-autolink.md) | How ClickUp surfaces branches/PRs/commits in a ticket's **GitHub tab**, and the branch / PR-title naming convention that triggers it. | |
|
|
10
10
|
| [ClickUp Project & Opportunity Multi-List Routing](features/clickup-project-routing.md) | Routes ClickUp tasks into the correct **secondary multi-list memberships** based on their custom-field values, via the `clickup` webhook. | worker2/Worker/Clickup/Project.php, worker2/Worker/Clickup.php |
|
|
11
11
|
| [ClickUp Rich-Text Custom Fields via Quill Delta (API)](features/clickup-richtext-api.md) | ClickUp custom text fields (type `text` and long-text) support rich formatting only through a **Quill Delta** written to the undocumented `value_richtext` key o | test/@dave/clickup_md2delta.js, .claude/skills/plan-ticket/scripts/clickup.js |
|
|
12
|
+
| [ClickUp Subtask Activity → Parent Opportunity/Epic Comments](features/clickup-subtask-activity.md) | Surfaces **subtask** progress, completion, and discussion on the top-level **Opportunity** or **Epic** it rolls up to, so a deal/project owner sees activity whe | worker2/Worker/Clickup/Subtask.php, worker2/Worker/Clickup.php, dbchanges2/Team/2026-07-14a - Add ClickupSubtaskActivity ledger.sql |
|
|
12
13
|
| [ClickUp Work Type Automation (Committed / Conditional / Stretch)](features/clickup-work-type-automation.md) | The ClickUp webhook handler (`_Worker_Clickup`) automatically maintains each task's **Work Type** custom field — `Committed`, `Conditional`, or `Stretch` — base | worker2/Worker/Clickup.php, worker2/Tests/Worker/ClickupWorkTypeTest.php |
|
|
13
14
|
| [Creating Worker Actions](features/creating-worker-actions.md) | How to add a new callable Worker action — a PHP class whose `public static` methods are invoked as background jobs (via webhook, cron, or `_Worker::runTask()`). | worker2/Worker/, worker2/Controller/Index.php, _underscore/Worker.php |
|
|
14
15
|
| [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
|
|
@@ -15,6 +15,7 @@ related:
|
|
|
15
15
|
- ../architecture.md
|
|
16
16
|
- ./creating-worker-actions.md
|
|
17
17
|
- ./clickup-connectivity-watchdog.md
|
|
18
|
+
- ./clickup-subtask-activity.md
|
|
18
19
|
---
|
|
19
20
|
|
|
20
21
|
## Summary
|
|
@@ -78,8 +79,11 @@ ids are discovered. Configure via the `DiscoverTaskTypes()` / `DiscoverIds()` ac
|
|
|
78
79
|
2. Each handler fetches task details via `_Worker_Clickup::getTaskDetails()` (a per-invocation
|
|
79
80
|
`static` cache shared across all handlers in one job), then guards on **space id** and
|
|
80
81
|
**top-level** (`parent` empty). The three routing handlers identify Epics/tasks
|
|
81
|
-
**structurally** (space + top-level).
|
|
82
|
-
|
|
82
|
+
**structurally** (space + top-level). The `enforceHubPlacement` guard was written to key on
|
|
83
|
+
`custom_item_id`, but the Task-Types ClickApp is **not reliably enabled** on the hub spaces
|
|
84
|
+
and `TASK_TYPE_EPIC_ID` / `TASK_TYPE_OPPORTUNITY_ID` remain `null` — so that guard is
|
|
85
|
+
effectively **inert** in practice; structural (space + top-level) identification is what
|
|
86
|
+
actually runs (see Gotchas, and `./clickup-subtask-activity.md`).
|
|
83
87
|
3. Custom fields are read by name via `extractCustomFields()` (one pass, last-non-null-wins,
|
|
84
88
|
trimmed) → mapped to a target list id → reconciled by `applyMultiListPlacement()`:
|
|
85
89
|
POST the target if absent, DELETE every other in-scope list (DELETE failures are caught
|
|
@@ -160,6 +164,7 @@ Read-only / setup actions, invoked via `curl -X POST https://worker.togahub.com/
|
|
|
160
164
|
`RESULT: PASS`), `RegisterOpportunityWebhook` and `RegisterHubGuardWebhooks` (one-time setup).
|
|
161
165
|
|
|
162
166
|
## Change history
|
|
167
|
+
- 2026-07-14 — Correction: `TASK_TYPE_EPIC_ID` / `TASK_TYPE_OPPORTUNITY_ID` are still `null` and the Task-Types ClickApp is not reliably enabled on the hub spaces, so `enforceHubPlacement`'s custom-Task-Type keying is inert; structural (space + top-level) identification is what actually runs. Cross-linked the new subtask-activity feature (same hub spaces/pipeline). (ajean)
|
|
163
168
|
- 2026-06-23 — Added the hub-placement guard (`enforceHubPlacement` on `taskCreated`/`taskMoved`): re-anchors misplaced Epics/Opportunities to their hub by custom Task Type + home space, backfills Business Unit/Project Category, and notifies. Added `DiscoverTaskTypes`, `RegisterHubGuardWebhooks`, `TestHubGuardMappings`. Corrected the earlier "Task Types not enabled" note. (ajean)
|
|
164
169
|
- 2026-06-09 — Documented ClickUp secondary multi-list routing (hybrid native-automation + worker design, three space handlers, self-trigger guard). (jcardinal)
|
|
165
170
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ClickUp Subtask Activity → Parent Opportunity/Epic Comments
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: draft
|
|
9
|
+
updated: 2026-07-14
|
|
10
|
+
owners: [ajean]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Clickup/Subtask.php
|
|
13
|
+
- worker2/Worker/Clickup.php
|
|
14
|
+
- dbchanges2/Team/2026-07-14a - Add ClickupSubtaskActivity ledger.sql
|
|
15
|
+
related:
|
|
16
|
+
- ./clickup-project-routing.md
|
|
17
|
+
- ../architecture.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
Surfaces **subtask** progress, completion, and discussion on the top-level **Opportunity**
|
|
23
|
+
or **Epic** it rolls up to, so a deal/project owner sees activity where they actually look.
|
|
24
|
+
On a `clickup` webhook for a subtask (a task with a non-empty `parent`) of event
|
|
25
|
+
`taskStatusUpdated` or `taskCommentPosted`, the worker walks the parent chain to the
|
|
26
|
+
top-level ancestor and, if that ancestor is an Opportunity (Opportunity/RFP Hub space
|
|
27
|
+
`90113928591`) or an Epic (Project Hub space `90113939341`), posts **one** comment on the
|
|
28
|
+
ancestor. Implemented in `_Worker_Clickup_Subtask` (extends `_Worker_Clickup`), fanned out
|
|
29
|
+
from a pre-switch block in `_Worker_Clickup::Webhook()`.
|
|
30
|
+
|
|
31
|
+
> **Status: not yet deployed / not live-verified.** PRs open: worker2 #110, dbchanges2 #427.
|
|
32
|
+
> The dbchanges2 migration must run **before or with** the worker deploy (the worker reserves
|
|
33
|
+
> a ledger row before every post).
|
|
34
|
+
|
|
35
|
+
## Key files / entry points
|
|
36
|
+
|
|
37
|
+
- `worker2/Worker/Clickup/Subtask.php` — `_Worker_Clickup_Subtask` (NEW), all subtask
|
|
38
|
+
fan-out / ancestor-walk / dedup / post logic.
|
|
39
|
+
- `worker2/Worker/Clickup.php` — a **pre-switch fan-out block** in `_Worker_Clickup::Webhook()`
|
|
40
|
+
detects a subtask event and dispatches `_Worker::runTask('Clickup/Subtask/Process',
|
|
41
|
+
['taskId', 'event', 'data' => history_items])`. The block is wrapped in a best-effort
|
|
42
|
+
`try/catch (Throwable)` and **deliberately does not `return`** — it falls through to the
|
|
43
|
+
existing `taskStatusUpdated` / `taskCommentPosted` switch, so sprint / work-type handling is
|
|
44
|
+
unaffected.
|
|
45
|
+
- `dbchanges2/Team/2026-07-14a - Add ClickupSubtaskActivity ledger.sql` (NEW) — creates the
|
|
46
|
+
`ClickUpSubtaskActivity` dedup ledger table in the **Team** DB.
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
1. Webhook arrives → `_Worker_Clickup::Webhook()`. The pre-switch block short-circuits **early
|
|
51
|
+
on the subtask's own space** before any parent walk (a subtask lives in the same space as its
|
|
52
|
+
ancestor, so a non-hub space is discarded before spending API calls).
|
|
53
|
+
2. `Clickup/Subtask/Process` walks up the `parent` chain to the top-level (no-parent) ancestor
|
|
54
|
+
and confirms it is an Opportunity or Epic **structurally** — by home space id + top-level —
|
|
55
|
+
mirroring `handleEpicUpdate` / `handleOpportunityUpdate`. **Not** by custom Task Type: the
|
|
56
|
+
Task-Types ClickApp is not reliably enabled on the hub spaces (see Gotchas).
|
|
57
|
+
3. Triggers:
|
|
58
|
+
- **Status** — post when the subtask status moves into `in progress`, `roadblocked`, or
|
|
59
|
+
`on hold` (matched by **lowercase status name**, consistent with existing status
|
|
60
|
+
comparisons in `Worker/Clickup.php`), OR into any completed status (matched by **status
|
|
61
|
+
TYPE** in `done`/`closed` via the inherited `COMPLETE_STATUS_TYPES`).
|
|
62
|
+
- **Comment** — any user comment posted on the subtask is mirrored to the ancestor as
|
|
63
|
+
`"💬 <author> commented on subtask …"`.
|
|
64
|
+
4. Every post carries a trailing `(automated)` marker. Comments that already carry that marker
|
|
65
|
+
are **never** mirrored (loop guard).
|
|
66
|
+
5. Posts use `_Component_Api_Clickup::send('POST', '/task/{id}/comment',
|
|
67
|
+
['comment_text' => …, 'notify_all' => false])`; task lookups reuse
|
|
68
|
+
`_Worker_Clickup::getTaskDetails()` (in-process static cache);
|
|
69
|
+
`_Worker_Clickup_Project::RATE_LIMIT_DELAY_US` paces the parent walk.
|
|
70
|
+
|
|
71
|
+
### Dedup ledger (the authoritative guard)
|
|
72
|
+
|
|
73
|
+
Before posting, the worker **reserves a ledger row** (`INSERT` into `ClickUpSubtaskActivity`)
|
|
74
|
+
so the `UNIQUE(subtaskId, activityKey)` constraint — not a check-then-act window — is the
|
|
75
|
+
authoritative dedup guard against re-delivered webhooks. If the comment POST fails, the row is
|
|
76
|
+
**released** (`DELETE`) so a retry can re-attempt. The `activityKey` is:
|
|
77
|
+
- `status:<name>` for a watched named status,
|
|
78
|
+
- `status:completed` for a completion,
|
|
79
|
+
- `comment:<clickupCommentId>` for a mirrored comment — falling back to the history-item id,
|
|
80
|
+
then a per-event date, then `md5(text)`, so identical comment texts stay distinct.
|
|
81
|
+
|
|
82
|
+
Ledger access follows the `ClickUpHealthState` pattern from `_Worker_Clickup_Health`: raw
|
|
83
|
+
`_Query` on `_underscore::DB_TEAM` with `_Database::escape()`.
|
|
84
|
+
|
|
85
|
+
## Data model
|
|
86
|
+
|
|
87
|
+
`ClickUpSubtaskActivity` in the **Team** DB (dbchanges2 migration `2026-07-14a`):
|
|
88
|
+
`id`, `uuid`, `subtaskId VARCHAR(32)`, `activityKey VARCHAR(255)`, `dtPosted`, with
|
|
89
|
+
`UNIQUE(subtaskId, activityKey)`. All other state lives in ClickUp.
|
|
90
|
+
|
|
91
|
+
## Client variations
|
|
92
|
+
|
|
93
|
+
None — internal automation for Agilant's ClickUp workspace, uniform across clients.
|
|
94
|
+
|
|
95
|
+
## Gotchas / known issues
|
|
96
|
+
|
|
97
|
+
- **Ancestor identification is STRUCTURAL, not by Task Type.** The Task-Types ClickApp is not
|
|
98
|
+
reliably enabled on the hub spaces, and `_Worker_Clickup_Project`'s `TASK_TYPE_EPIC_ID` /
|
|
99
|
+
`TASK_TYPE_OPPORTUNITY_ID` remain `null` (inert). Identify Opportunities/Epics by home space
|
|
100
|
+
id + top-level, exactly like the routing handlers — do not rely on `custom_item_id` here.
|
|
101
|
+
- **Completion matches by status TYPE (`done`/`closed`), not name** — it survives label
|
|
102
|
+
changes. The other watched statuses match by lowercase **name** (`in progress`,
|
|
103
|
+
`roadblocked`, `on hold`).
|
|
104
|
+
- **Loop guard is the `(automated)` marker** — every automated post carries it, and any comment
|
|
105
|
+
carrying it is skipped on mirror. Removing/altering the marker text re-opens the mirror loop.
|
|
106
|
+
- **Fan-out never returns** — it falls through to the existing switch on purpose; keep it that
|
|
107
|
+
way or existing sprint/work-type handling breaks.
|
|
108
|
+
- **Migration ordering** — deploy the dbchanges2 `2026-07-14a` migration before/with the worker;
|
|
109
|
+
the worker reserves a ledger row on every post and will error without the table.
|
|
110
|
+
|
|
111
|
+
## Change history
|
|
112
|
+
- 2026-07-14 — Created (draft, not yet deployed): subtask status/completion/comment mirroring to the parent Opportunity/Epic via `_Worker_Clickup_Subtask`, guarded by the `ClickUpSubtaskActivity` UNIQUE-key dedup ledger. PRs worker2 #110 / dbchanges2 #427. (ajean)
|
|
113
|
+
|
|
114
|
+
## Related docs
|
|
115
|
+
|
|
116
|
+
- [ClickUp Project & Opportunity Multi-List Routing](./clickup-project-routing.md) — same hub spaces, same webhook pipeline, same structural identification.
|
|
117
|
+
- [Worker (worker2) Architecture](../architecture.md) — always-HTTP-200, commit-before-SQS worker contract.
|
|
@@ -6,13 +6,15 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-14
|
|
10
10
|
owners: [bala]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Etilize/ItemTranslations.php
|
|
13
13
|
related:
|
|
14
14
|
- ./creating-worker-actions.md
|
|
15
15
|
- ./etilize-catalog-item-import.md
|
|
16
|
+
- ../../api2/features/language-translation-layer.md
|
|
17
|
+
- ../../../clients/compass-canada/features/french-item-feature-translations.md
|
|
16
18
|
---
|
|
17
19
|
|
|
18
20
|
## Summary
|
|
@@ -35,6 +37,28 @@ Routes are `Etilize/ItemTranslations/<Method>` (see
|
|
|
35
37
|
— translates **all** items that have a `c_etilizeProductId`.
|
|
36
38
|
2. `ImportItemsLanguageByPartNumbers(string $client, string $languageCode, array $partNumbers, ?string $languageName = null, ?string $catalog = null)`
|
|
37
39
|
— translates only the given part numbers (uses a `partNumber IN (...)` filter).
|
|
40
|
+
3. `BackfillEtilizeAttributeIds(...)` — populates `c_etilizeAttributeId` (on `Features`) and
|
|
41
|
+
`c_etilizeAttributeGroupId` (on `ItemCategoryFeatureGroups`) from Etilize `en_us`, so features and
|
|
42
|
+
groups can be matched to their Etilize attribute id. Must run **before** feature-translation import
|
|
43
|
+
so each French value lands on the right feature/group.
|
|
44
|
+
|
|
45
|
+
## Feature-text translation (item features, not just Item title/desc)
|
|
46
|
+
|
|
47
|
+
Beyond `ItemTranslations` (Item title/description/longDescription), `importItemLanguage` now also
|
|
48
|
+
writes **feature** translations via `importItemFeatureTranslations` → `translateAttributeGroup` /
|
|
49
|
+
`translateAttribute`, into the `FeatureTranslations`, `ItemCategoryFeatureGroupTranslations`, and
|
|
50
|
+
`ItemFeatureTranslations` sidecar tables (see the shared
|
|
51
|
+
[Language Translation Layer](../../api2/features/language-translation-layer.md) for the read-side
|
|
52
|
+
metadata wiring). Matching is by Etilize **attribute id** (`c_etilizeAttributeId`) and **attribute
|
|
53
|
+
group id** (`c_etilizeAttributeGroupId`), which is why the backfill action above must run first.
|
|
54
|
+
|
|
55
|
+
- **Group translation is scoped per-item, not catalog-wide.** `findGroupIdsForItemByEtilizeAttributeGroupId`
|
|
56
|
+
joins through the item's own `ItemFeatures → ItemCategoryFeatures → groups` so only the groups that
|
|
57
|
+
item actually uses are translated. Doing it catalog-wide would write thousands of unrelated group rows.
|
|
58
|
+
- Constants: `ETILIZE_LOCALE_ENGLISH = 'en_us'`, `ETILIZE_GENERAL_ATTRIBUTE_GROUP_ID = '35'`,
|
|
59
|
+
`FEATURE_VALUE_MAX_LENGTH = 254`.
|
|
60
|
+
- `RefreshItemsFromEtilize` is idempotent — it reuses existing features/groups by name (and by
|
|
61
|
+
item+feature), so re-running does not duplicate.
|
|
38
62
|
|
|
39
63
|
## How it works
|
|
40
64
|
|
|
@@ -61,8 +85,21 @@ Items **not published in the requested locale are skipped**.
|
|
|
61
85
|
- Only items with a `c_etilizeProductId` are translatable — items without an Etilize id
|
|
62
86
|
are silently skipped.
|
|
63
87
|
- English is intentionally never written here; it lives in the `Items` table.
|
|
88
|
+
- **Use `_Query::getAffectedRows()`, not `rowsAffected()`.** There is no `rowsAffected()` method
|
|
89
|
+
(the CLAUDE.md doc was wrong); calling it throws "Call to undefined method".
|
|
90
|
+
- **OOM on a full (~200-item) run.** `_Database` caches every SELECT in `$_queryCache`, so a large
|
|
91
|
+
batch grows unbounded. Call `_Database::useQueryCache(false)` and `gc_collect_cycles()` **per item**
|
|
92
|
+
inside the batch loops.
|
|
93
|
+
- **`clampFeatureValue` must use `mb_strlen` / `mb_substr` with `'UTF-8'`**, never byte-based
|
|
94
|
+
`strlen`/`substr`, or a French accent gets cut mid-character (`FEATURE_VALUE_MAX_LENGTH = 254`).
|
|
64
95
|
|
|
65
96
|
## Change history
|
|
97
|
+
- 2026-07-13 — Added `BackfillEtilizeAttributeIds` action and extended `importItemLanguage` to import
|
|
98
|
+
item **feature** translations (`importItemFeatureTranslations` → `translateAttributeGroup` /
|
|
99
|
+
`translateAttribute`) into the Feature/group/item-feature sidecar tables; groups scoped per-item via
|
|
100
|
+
`findGroupIdsForItemByEtilizeAttributeGroupId` to avoid catalog-wide writes. Durable gotchas:
|
|
101
|
+
`getAffectedRows()` (no `rowsAffected()`); disable `_Database` query cache + `gc_collect_cycles()`
|
|
102
|
+
per item to avoid OOM on full runs; `mb_`-based value clamp for accents. (bala)
|
|
66
103
|
- 2026-06-26 — Documented `_Worker_Etilize_ItemTranslations`: per-language Etilize
|
|
67
104
|
`getProduct` import into `ItemTranslations` (title=desc type 2, longDescription=type 1),
|
|
68
105
|
locale derived from language code, COALESCE upsert that never overwrites with blanks,
|
package/knowledge/INDEX.md
CHANGED
|
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
20
|
- **_underscore** (_Underscore) _(framework core)_ — 31 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
|
-
- **worker2** (Worker) —
|
|
21
|
+
- **worker2** (Worker) — 29 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)
|
|
24
24
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
+
| [French (fr-CA) Item Feature Translations (Compass Canada)](features/french-item-feature-translations.md) | 2.0 | Renders item **feature** text on the Compass Canada French storefront — feature names, feature-group headers (e.g. | _underscore/Model/Compass/Canada/Feature.php, _underscore/Model/Compass/Canada/ItemCategoryFeatureGroup.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, worker2/Worker/Etilize/ItemTranslations.php, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql, dbchanges2/Client_CompassCanada/2026-07-13 - FeatureAttributeCustomFields.sql, dbchanges2/Client_CompassCanada/2026-07-13 - DedupeItemFeaturesAndGroups.sql, dbchanges2/Client_CompassCanada/2026-07-13 - SeedFrenchFeatureTranslations.sql |
|
|
5
6
|
| [Grand & Toy ASN Import (Compass Canada)](features/grand-and-toy-asn-import.md) | 2.0 | Imports Grand & Toy (G&T) Advance Shipping Notices for Compass Canada. | worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/Canada/AdvanceShippingNotice.php, dbchanges2/Client_CompassCanada/ |
|
|
6
7
|
| [Compass Canada](profile.md) | 2.0 | Compass Canada is the Canadian arm of the Compass account — a separate TOGA tenant, related to but distinct from Compass USA. | |
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: French (fr-CA) Item Feature Translations (Compass Canada)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: _Underscore
|
|
5
|
+
client: compass-canada
|
|
6
|
+
type: client-feature
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-07-14
|
|
9
|
+
owners: ["bala"]
|
|
10
|
+
files:
|
|
11
|
+
- _underscore/Model/Compass/Canada/Feature.php
|
|
12
|
+
- _underscore/Model/Compass/Canada/ItemCategoryFeatureGroup.php
|
|
13
|
+
- _underscore/Model/Client/FeatureTranslation.php
|
|
14
|
+
- _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php
|
|
15
|
+
- _underscore/Model/Client/ItemFeatureTranslation.php
|
|
16
|
+
- worker2/Worker/Etilize/ItemTranslations.php
|
|
17
|
+
- dbchanges2/Client/2026-07-13a - FeatureTranslations.sql
|
|
18
|
+
- dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql
|
|
19
|
+
- dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql
|
|
20
|
+
- dbchanges2/Client_CompassCanada/2026-07-13 - FeatureAttributeCustomFields.sql
|
|
21
|
+
- dbchanges2/Client_CompassCanada/2026-07-13 - DedupeItemFeaturesAndGroups.sql
|
|
22
|
+
- dbchanges2/Client_CompassCanada/2026-07-13 - SeedFrenchFeatureTranslations.sql
|
|
23
|
+
related:
|
|
24
|
+
- ../profile.md
|
|
25
|
+
- ../../../2.0/apps/api2/features/language-translation-layer.md
|
|
26
|
+
- ../../../2.0/apps/worker2/features/etilize-item-translation-import.md
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Summary
|
|
30
|
+
Renders item **feature** text on the Compass Canada French storefront — feature names, feature-group
|
|
31
|
+
headers (e.g. "Processeur", "Mémoire", "Chipset"), and feature values — in fr-CA. It layers the shared
|
|
32
|
+
metadata-driven sidecar translation infrastructure ([Language Translation Layer](../../../2.0/apps/api2/features/language-translation-layer.md))
|
|
33
|
+
on top of a French **import** from Etilize ([Etilize Item Translation Import](../../../2.0/apps/worker2/features/etilize-item-translation-import.md)),
|
|
34
|
+
matched to the right feature/group by Etilize **attribute id**. fr-CA is `languageId 2`.
|
|
35
|
+
|
|
36
|
+
This replaces the old approach where the `worker/crons/toga2/compasscanada/Imports2.0/Import_items.php`
|
|
37
|
+
cron wrote French straight into the **base** fields and minted new group UUIDs every run — corrupting
|
|
38
|
+
English and producing thousands of duplicate features/groups.
|
|
39
|
+
|
|
40
|
+
## Compass-Canada-specific pieces
|
|
41
|
+
- **Custom match fields** (Compass Canada only, shipped via `dbchanges2/Client_CompassCanada/`, models
|
|
42
|
+
under `_underscore/Model/Compass/Canada/`):
|
|
43
|
+
- `c_etilizeAttributeId` on `Feature` (`Model/Compass/Canada/Feature.php`)
|
|
44
|
+
- `c_etilizeAttributeGroupId` on `ItemCategoryFeatureGroup` (`Model/Compass/Canada/ItemCategoryFeatureGroup.php`)
|
|
45
|
+
These let the importer match an English feature/group to its Etilize attribute id so the correct
|
|
46
|
+
French value lands on the right feature. Populate them first with the worker2
|
|
47
|
+
`BackfillEtilizeAttributeIds` action.
|
|
48
|
+
- **fr-CA = `languageId 2`** (see also profile: `UserGlobalSettings.settingId = 2`, `en` / `fr-CA`).
|
|
49
|
+
- The shared sidecar tables (`FeatureTranslations`, `ItemCategoryFeatureGroupTranslations`,
|
|
50
|
+
`ItemFeatureTranslations`) and Core Records 343/344/345 + RecordFields 2442–2457 are documented in the
|
|
51
|
+
shared layer doc; only the custom fields and the seed/dedup data are Compass-Canada-specific.
|
|
52
|
+
|
|
53
|
+
## Rollout order (dev/beta done, prod pending)
|
|
54
|
+
1. Deploy the translation schema (sidecars + ACL + Core RecordFields) with the correct **env-specific**
|
|
55
|
+
Core Record/RecordField ids.
|
|
56
|
+
2. **Dedup** existing features/groups (see below).
|
|
57
|
+
3. `BackfillEtilizeAttributeIds` — populate the custom match fields from Etilize `en_us`.
|
|
58
|
+
4. Import French translations (worker2 Etilize feature-translation import).
|
|
59
|
+
5. Seed any remaining French feature translations.
|
|
60
|
+
6. **Retire** the old `worker/crons/toga2/compasscanada/Imports2.0/Import_items.php` cron.
|
|
61
|
+
|
|
62
|
+
Prod rollout is deferred until explicitly requested; the dev/beta build (beta DB) is complete.
|
|
63
|
+
|
|
64
|
+
## Dedup of duplicate features/groups
|
|
65
|
+
- **Root cause:** the old `Import_items.php` cron did unconditional POSTs and minted new group UUIDs on
|
|
66
|
+
every run → ~7700 duplicate features and ~2100 duplicate groups on dev; prod counts ~12665→4932
|
|
67
|
+
features and ~2390→263 groups.
|
|
68
|
+
- **Migration technique (team practice):** the dedup migration must **not** use `CREATE TEMPORARY
|
|
69
|
+
TABLE` — use inline **derived-table subqueries**. MySQL 8 materializes them and avoids error 1093
|
|
70
|
+
(can't reopen the table being updated).
|
|
71
|
+
- **Keep-which-row differs by env:** the **dev** variant prefers the already-translated duplicate —
|
|
72
|
+
`COALESCE(MIN(CASE WHEN translated THEN id END), MIN(id))`; the **prod** variant keeps the **lowest
|
|
73
|
+
id**.
|
|
74
|
+
|
|
75
|
+
## Gotchas / known issues
|
|
76
|
+
- **Sidecar/source collation mismatch.** Sidecar tables are `utf8mb4_unicode_ci` (matching
|
|
77
|
+
`ItemTranslations`); the source fields are `utf8mb4_0900_ai_ci`. Any ad-hoc query comparing across the
|
|
78
|
+
two needs `COLLATE utf8mb4_bin` or MySQL throws a collation-mismatch error.
|
|
79
|
+
- **Etilize attribute ids of the form `34xxxxxx` are NOT corrupted** (false alarm resolved). A manual
|
|
80
|
+
XML showing `32xxxxxx` came from a different Etilize view; the worker's Etilize endpoint returns
|
|
81
|
+
`34xxxxxx` consistently for both backfill and import, so translations match. Do not "fix" them.
|
|
82
|
+
- **A shared `c_etilizeProductId` across two part numbers can be a data error.** On dev, partNumber
|
|
83
|
+
`BT6L3UC#ABA` had the wrong shared `c_etilizeProductId 1083084905` (shared with `B4TM0UC#ABA`); prod's
|
|
84
|
+
correct value is `1086408433`. Fixed by a single `UPDATE` — this was a data error, not a code bug.
|
|
85
|
+
|
|
86
|
+
## Change history
|
|
87
|
+
- 2026-07-13 — Built the fr-CA item-feature translation path end-to-end on dev/beta: shared feature
|
|
88
|
+
sidecar tables + Core wiring, Compass-only `c_etilizeAttributeId` / `c_etilizeAttributeGroupId` custom
|
|
89
|
+
fields, worker2 backfill + feature-translation import, dedup of ~7700 dup features / ~2100 dup groups,
|
|
90
|
+
French seed. Corrected the `34xxxxxx` attribute-id false alarm and fixed the `BT6L3UC#ABA` shared
|
|
91
|
+
`c_etilizeProductId` data error. Prod rollout + retiring the old `Import_items.php` cron deferred. (bala)
|
|
92
|
+
|
|
93
|
+
## Related docs
|
|
94
|
+
- [Compass Canada profile](../profile.md)
|
|
95
|
+
- [Language Translation Layer](../../../2.0/apps/api2/features/language-translation-layer.md)
|
|
96
|
+
- [Etilize Item Translation Import](../../../2.0/apps/worker2/features/etilize-item-translation-import.md)
|
package/package.json
CHANGED