toga-ai 1.0.167 → 1.0.168

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.
@@ -3,6 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
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 |
6
7
  | [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/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.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 |
7
8
  | [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 |
8
9
  | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
@@ -0,0 +1,107 @@
1
+ ---
2
+ title: ACL Permission Chain (Record & Field Authorization)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - api2/Component/Api/V2/V2.php
13
+ - _underscore/Model/Core/Page.php
14
+ - dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql
15
+ - dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql
16
+ ---
17
+
18
+ ## Summary
19
+
20
+ Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete
21
+ a record is decided by rows across **four linked tables**, not by code. Granting access by
22
+ inserting only an `AclRecordPermissions` row is the single most common mistake — the permission
23
+ silently does nothing without the rest of the chain. A complete grant requires **all four** tables,
24
+ plus `AclFieldPermissions` for the fields to be readable/writable.
25
+
26
+ ## The complete chain (all four are required)
27
+
28
+ ```
29
+ AclRecordPermissions → AclLogicGroups → AclLogicGroupExpressions → AclRecordExpressions
30
+ (the role × record (operator AND/OR, (binds a group to an (the SQL test,
31
+ CRUD grant) parent group tree) expression) e.g. '1' = all)
32
+ ```
33
+
34
+ 1. **`AclRecordPermissions`** — the grant: `recordId`, `roleId`, `allowCreate/Read/Update/Delete`
35
+ (optional `appId`, `indirectRecordId`). Unique on `(appId, recordId, roleId, indirectRecordId)`.
36
+ 2. **`AclLogicGroups`** — at least one root group per permission: `aclRecordPermissionId` (FK),
37
+ `operator` **`enum('AND','OR') NOT NULL`** (this column is mandatory and easy to forget),
38
+ optional `parentAclLogicGroupId` for nested groups.
39
+ 3. **`AclLogicGroupExpressions`** — binds a logic group to a record expression: `aclLogicGroupId`
40
+ (FK), `aclRecordExpressionId` (FK). Unique on `(aclLogicGroupId, aclRecordExpressionId)`.
41
+ 4. **`AclRecordExpressions`** — the actual SQL test: `recordId`, `slug`, `description`,
42
+ `sqlExpression` (TEXT). Unique on `(recordId, slug)`. The conventional "always allow" row is
43
+ `slug = 'all'`, `sqlExpression = '1'`.
44
+
45
+ **Field visibility — `AclFieldPermissions`.** Record-level access alone does **not** expose
46
+ fields. Each field needs a row: `recordFieldId`, `roleId`, `isWritable` (0 = read-only, 1 =
47
+ writable). Without it the field is omitted from responses (and rejected on write).
48
+
49
+ ## Where ACL rows live: Core vs. Client database
50
+
51
+ `Core.Records.aclDatabase` decides which database holds the ACL rows for that record:
52
+
53
+ - **`aclDatabase = 'CORE'`** → ACL rows live in the **Core** database.
54
+ - **`aclDatabase = 'CLIENT'`** → ACL rows live in **each client's own database**. A migration
55
+ granting access therefore goes in `dbchanges2/Client/` (or a specific `Client_<Name>/`) so it
56
+ runs against every client DB.
57
+
58
+ **Role ids differ per client DB**, so never hardcode a `roleId` in a `CLIENT` ACL migration —
59
+ resolve it with a subselect, e.g. the Base role:
60
+
61
+ ```sql
62
+ roleId = (SELECT id FROM Roles WHERE `name` = 'Base')
63
+ ```
64
+
65
+ ## Canonical example (grant the Base role full CRUD on a record)
66
+
67
+ ```sql
68
+ INSERT INTO AclRecordPermissions SET
69
+ uuid = '…', recordId = 331, roleId = (SELECT id FROM Roles WHERE `name` = 'Base'),
70
+ allowCreate = 1, allowRead = 1, allowUpdate = 1, allowDelete = 1;
71
+
72
+ INSERT INTO AclRecordExpressions SET
73
+ uuid = '…', recordId = 331, slug = 'all', description = 'All', sqlExpression = '1';
74
+
75
+ INSERT INTO AclLogicGroups SET
76
+ uuid = '…', aclRecordPermissionId = (SELECT MAX(id) FROM AclRecordPermissions), operator = 'AND';
77
+
78
+ INSERT INTO AclLogicGroupExpressions SET
79
+ uuid = '…', aclLogicGroupId = (SELECT MAX(id) FROM AclLogicGroups),
80
+ aclRecordExpressionId = (SELECT id FROM AclRecordExpressions WHERE recordId = 331 AND slug = 'all');
81
+
82
+ -- and one AclFieldPermissions row per exposed field (recordFieldId, roleId, isWritable)
83
+ ```
84
+
85
+ ## How it is enforced
86
+
87
+ `_Component_Api_V2::buildLookups()` bulk-loads these tables (from the Core and/or Client ACL
88
+ database per `Records.aclDatabase`) into in-memory lookups once per request; `processRoutePairs()`
89
+ then checks the caller's roles against `AclRecordPermissions` (→ `EZ-1` if no grant) and computes
90
+ readable/writable fields from `AclFieldPermissions`. `_Model_Core_Page::meta()` reads the same
91
+ tables to return per-page ACL to the frontend.
92
+
93
+ ## Checklist (so the chain is never half-built)
94
+
95
+ - [ ] `AclRecordPermissions` row for the role × record with the right CRUD flags.
96
+ - [ ] `AclRecordExpressions` row (`slug='all'`, `sqlExpression='1'` for unconditional access).
97
+ - [ ] `AclLogicGroups` row with `operator` set (AND/OR) referencing the permission.
98
+ - [ ] `AclLogicGroupExpressions` row binding the group to the expression.
99
+ - [ ] `AclFieldPermissions` rows for every field that must be readable/writable.
100
+ - [ ] For `aclDatabase = 'CLIENT'` records: rows go in client DB(s); resolve `roleId` by subselect.
101
+
102
+ ## Change history
103
+
104
+ - **2026-06-23** — Documented after repeatedly missing the logic-group/expression rows when
105
+ granting access to new records. Created alongside the `item-translations` record (331) ACL,
106
+ whose grant in `dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql` is a worked example of
107
+ the full chain for a `CLIENT`-aclDatabase record targeting the Base role.
@@ -4,6 +4,7 @@
4
4
  |-----|---------|-------|
5
5
  | [Worker (worker2) Architecture](architecture.md) | Worker (repo `worker2`) is an AWS Elastic Beanstalk **Worker Tier** application that processes background jobs. | worker2/Controller/Index.php, worker2/Worker/, worker2/LambdaFunctions/, _underscore/Worker.php |
6
6
  | [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 |
7
+ | [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 |
7
8
  | [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 |
8
9
  | [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 |
9
10
  | [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Notification/Email.php, dbchanges2/Core/2026-05-21 - Monitors.sql |
@@ -0,0 +1,80 @@
1
+ ---
2
+ title: ClickUp Work Type Automation (Committed / Conditional / Stretch)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - worker2/Worker/Clickup.php
13
+ - worker2/Tests/Worker/ClickupWorkTypeTest.php
14
+ related:
15
+ - ./clickup-project-routing.md
16
+ ---
17
+
18
+ ## Summary
19
+ The ClickUp webhook handler (`_Worker_Clickup`) automatically maintains each task's **Work
20
+ Type** custom field — `Committed`, `Conditional`, or `Stretch` — based on the task's
21
+ dependencies, its status, and its due date. The core rule: a task that should be Committed is
22
+ demoted to **Conditional** while it still has an unfinished dependency, and promoted back to
23
+ **Committed** once all its dependencies are complete. The logic lives in
24
+ `updateTaskAndDependencies()`.
25
+
26
+ ## Key files / entry points
27
+ - `Worker/Clickup.php` → `Webhook($payload, $headers)` — webhook entry; routes by
28
+ `$payload->event`.
29
+ - `updateTaskAndDependencies($taskId, $dependsOnTaskId = null)` — the work-type engine.
30
+ - `getWorkType($taskDetails)` — reads the current Work Type custom field value.
31
+ - `isStatusComplete(string $statusType): bool` — true when a ClickUp status type is terminal.
32
+ - `getTaskDetails($taskId)` — cached `GET /task/{id}` (static per-invocation cache).
33
+
34
+ ## How it works
35
+ 1. A ClickUp webhook hits the worker; `Webhook()` switches on `$payload->event`.
36
+ 2. `updateTaskAndDependencies()` is invoked from the `taskCreated`, `taskUpdated`, and
37
+ `taskStatusUpdated` events. For `taskStatusUpdated` the call is **gated to terminal
38
+ statuses** (`isStatusComplete()`) so the dependent re-evaluation cascade only runs when a
39
+ task actually completes — not on every status change.
40
+ 3. For the task, it builds `$waitingOn` — the list of dependencies (`$dependency->task_id ==
41
+ $taskId`) whose status is **not** complete. A dependency counts as complete when its status
42
+ `type` is one of `COMPLETE_STATUS_TYPES` (`done` **or** `closed`).
43
+ 4. It also computes `$isStalledTask` (status in `on hold` / `awaiting client` / `roadblocked`).
44
+ 5. Decision:
45
+ - `Committed` + (`$waitingOn` non-empty OR stalled) → set **Conditional** (+ comment).
46
+ - `Conditional` + `$waitingOn` empty + not stalled → set **Committed** (+ comment).
47
+ - `Stretch` with a due date inside the current sprint → set Committed/Conditional as above.
48
+ 6. When a task is depended on by another (`$dependency->depends_on == $taskId`), it recurses
49
+ into the blocking direction so completing a task re-evaluates the tasks it was blocking. The
50
+ `$dependsOnTaskId` argument breaks two-node cycles.
51
+
52
+ ## Data model
53
+ Reads/writes the `Team` database (`Tasks`, `Developers`, `Tasks_Developers`, `Sprints`) and the
54
+ ClickUp REST API. The Work Type itself lives in ClickUp as a custom field
55
+ (`CLICK_UP_CUSTOM_FIELD_ID__WORKTYPE`), not in the DB.
56
+
57
+ ## Client variations
58
+ None — internal team/sprint tooling, uniform across clients.
59
+
60
+ ## Gotchas / known issues
61
+ - **Two terminal status types.** ClickUp has both `done` and `closed` terminal status types. A
62
+ completed dependency may be `closed`, not `done`. The completion check must treat **both** as
63
+ finished — see `COMPLETE_STATUS_TYPES`. Checking only `== 'done'` leaves dependents stuck on
64
+ Conditional after their blocker is closed.
65
+ - **Status changes must trigger re-evaluation.** A dependency completing fires
66
+ `taskStatusUpdated` on the *dependency*, not the blocked task. The `taskStatusUpdated` case
67
+ must call `updateTaskAndDependencies()` (it recurses into dependents) or the auto-promotion
68
+ back to Committed never fires on its own.
69
+ - **No PHPUnit harness** in worker2. The regression test
70
+ `Tests/Worker/ClickupWorkTypeTest.php` is a plain-PHP script (reflection on
71
+ `isStatusComplete`) run with `php Tests/Worker/ClickupWorkTypeTest.php`.
72
+
73
+ ## Change history
74
+ - 2026-06-23 — Fixed dependents staying Conditional after a blocker completed: completion check
75
+ now treats `closed` as terminal alongside `done`, and `taskStatusUpdated` re-evaluates
76
+ dependents (gated to terminal statuses). Added `COMPLETE_STATUS_TYPES`, `isStatusComplete()`,
77
+ and a plain-PHP regression test. (jcardinal)
78
+
79
+ ## Related docs
80
+ - [ClickUp Project & Opportunity Multi-List Routing](./clickup-project-routing.md)
@@ -15,8 +15,8 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
15
15
 
16
16
  ## 2.0 framework
17
17
 
18
- - **_underscore** (_Underscore) _(framework core)_ — 10 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
19
- - **worker2** (Worker) — 9 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
18
+ - **_underscore** (_Underscore) _(framework core)_ — 11 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
19
+ - **worker2** (Worker) — 10 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
20
20
  - **api2** (API) — 4 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
21
21
  - **dbchanges2** (Database Changes) _(framework core)_ — 1 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
22
22
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.167",
3
+ "version": "1.0.168",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",