toga-ai 1.0.265 → 1.0.267

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.
@@ -7,6 +7,6 @@
7
7
  | [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to the Tools app — URL routing, CSRF, and DB access through `App_Databa | tools/_/app/nav.php, tools/mvc/get.php |
8
8
  | [Tools Persona-Gated Navigation (App_Nav)](features/persona-gated-navigation.md) | `App_Nav` is the Tools app's two-level, **persona-gated** navigation. | tools/_/app/nav.php, tools/mvc/get.php |
9
9
  | [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/initiate/get.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php, tools/mvc/get.php, tools/config.production.ini, tools/config.local.ini |
10
- | [Talos Knowledge Base Admin UI (KB Documents + Vocabulary)](features/talos-kb-documents-admin.md) | A Tools (1.0) admin UI to browse/fix the Talos knowledge-base documents and manage the transcript-cleanup vocabulary — without a deploy. | tools/mvc/talos/kb-documents/get.php, tools/mvc/talos/kb-documents/post.php, tools/mvc/talos/vocabulary/get.php, tools/mvc/talos/vocabulary/post.php, tools/_/app/talos/s3.php, tools/_/app/worker.php, tools/_/app/nav.php |
10
+ | [Talos Knowledge Base Admin UI (KB Documents + Vocabulary)](features/talos-kb-documents-admin.md) | A Tools (1.0) admin UI to browse/fix the Talos knowledge-base documents and manage the transcript-cleanup vocabulary — without a deploy. | tools/mvc/talos/kb-documents/get.php, tools/mvc/talos/kb-documents/post.php, tools/mvc/talos/vocabulary/get.php, tools/mvc/talos/vocabulary/post.php, tools/_/app/talos/s3.php, tools/_/app/worker.php, tools/_/app/nav.php, tools/config.production.ini, tools/config.alpha.ini |
11
11
  | [Talos Pricing UI (Onboarding, Dashboard, Benchmarks, Cost Factors + Estimator)](features/talos-pricing-ui.md) | The 1.0 (tools app) face of the **Talos Pricing Platform** — a "Talos Pricing" nav folder with four pages plus a client-side estimate engine. | tools/_/app/nav.php, tools/_/app/talos/estimator.php, tools/mvc/talos/onboarding/get.php, tools/mvc/talos/onboarding/post.php, tools/mvc/talos/pricing/get.php, tools/mvc/talos/benchmarks/get.php, tools/mvc/talos/factors/get.php, tools/mvc/talos/factors/post.php, tools/assets/css/style.css |
12
12
  | [Deploying Tools to Elastic Beanstalk (PHP 8.5 / Amazon Linux 2023)](workflows/deploy-to-elastic-beanstalk-al2023.md) | How the **Tools** 1.0 app boots on Elastic Beanstalk running `PHP 8.5 on 64bit Amazon Linux 2023/4.13.1 (aarch64)`. | tools/.ebextensions/004_http_to_https.config, tools/.ebextensions/006_mount-s3fs.config, tools/.ebextensions/007_setup_export_cache_folders.config, tools/.ebextensions/008_setup_ldap.config, tools/.ebextensions/009_setup_phpini.config, tools/.ebextensions/020_setup_git_libraries.config, tools/.ebextensions/050_register_instance_to_shared_application_load_balancer.config, tools/ebs/git.json |
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-30
9
+ updated: 2026-07-02
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - tools/mvc/talos/kb-documents/get.php
@@ -16,6 +16,8 @@ files:
16
16
  - tools/_/app/talos/s3.php
17
17
  - tools/_/app/worker.php
18
18
  - tools/_/app/nav.php
19
+ - tools/config.production.ini
20
+ - tools/config.alpha.ini
19
21
  related:
20
22
  - ./mvc-data-access-patterns.md
21
23
  - ./persona-gated-navigation.md
@@ -37,12 +39,27 @@ Technology*) exposes two pages.
37
39
  - KB list derived from the **actual S3 sub-folders** under `development-team/` (title-cased,
38
40
  e.g. `office-depot` → "Office Depot") — not from `Team.KnowledgeBases`.
39
41
  - Lists approved docs excluding `*.metadata.json`; columns Meeting Date (`n/j/y`) / Title /
40
- Size / Last modified (`n/j/y g:i A`).
42
+ Size / Last modified (`n/j/y g:i A`). **Last-modified is converted from S3's UTC to
43
+ America/Chicago (US Central) before formatting**; the sort key stays UTC ISO-8601.
41
44
  - Clickable rows open an overlay **modal** with a type-aware preview: text inline; PDF via a
42
45
  presigned S3 URL in an iframe; Office files via the Google Docs viewer (presigned URL; MS
43
46
  Office viewer noted as an alternative); unsupported → download/delete only.
44
- - Download + delete are icon buttons in the modal. **Delete enqueues the worker `SyncKb`
45
- re-sync** so Bedrock reflects the removal.
47
+ - Modal actions are icon buttons: download, **move**, **edit** (text docs only), and delete.
48
+ **Delete / move / edit each enqueue the worker `SyncKb` re-sync** so Bedrock reflects the change.
49
+ - **Move** (right-arrow icon) opens a small centered dialog (KB picker + Move/Cancel) to re-file a
50
+ mis-classified doc from one KB to another. `App_Talos_S3::moveApprovedDocument` copies the `.txt`
51
+ and its `.metadata.json` sidecar to the destination via **get+put (NOT server-side `CopyObject` —
52
+ the togaiq key is denied CopyObject even same-bucket)**, rewrites the sidecar's KB-slug attribute
53
+ (`metadataAttributes.client.value.stringValue`), then deletes the originals. It enqueues a
54
+ **single** `SyncKb("source,dest")` job (worker2 syncs both sequentially — Bedrock allows one
55
+ in-flight ingestion per KB). Helpers `copyWithinBucket` + `putObjectBody` added.
56
+ - **Edit** (pencil icon, text docs only) enters edit mode in the modal: a title text box, a
57
+ **Find & Replace** tool ported from the old `test/team/talos/kb_processor.php` (Find/Replace,
58
+ Match case, Whole word only, Replace All with match count), and a contents textarea, with
59
+ Save/Cancel top-right. The `update` action + `App_Talos_S3::updateApprovedDocument` rewrites the
60
+ body and, if the title changed (`sanitizeTitle`), renames the object to
61
+ `"YYYY-MM-DD - {title}.txt"` (date preserved), moves+updates the sidecar filename attribute,
62
+ deletes the old key, and enqueues a `SyncKb` re-sync.
46
63
 
47
64
  ### `/talos/vocabulary` — cleanup vocabulary manager
48
65
  Single tabbed page: Prompt Template, Applications, Clients, People, Business Terms, Examples,
@@ -53,16 +70,29 @@ prompt/replacement tables that the worker2 pipeline reads at runtime.
53
70
 
54
71
  ## Helpers
55
72
 
56
- - **`App_Talos_S3`** (`_/app/talos/s3.php`) — S3 client; list KB slugs; list/count approved;
57
- presigned URL; get/delete; slug sanitize+lowercase; approved-prefix path guard.
73
+ - **`App_Talos_S3`** (`_/app/talos/s3.php`) — S3 client; list KB slugs; list/count approved
74
+ (UTC→Central for display); presigned URL; get/delete; `moveApprovedDocument`;
75
+ `updateApprovedDocument`; `sanitizeTitle`; `copyWithinBucket`; `putObjectBody`; slug
76
+ sanitize+lowercase; approved-prefix path guard. **`client()` credential resolution:** prefers a
77
+ dedicated **`[talos]`** config section (the **togaiq**-capable key), falling back to `[aws]`, then
78
+ the SDK default chain — because `App_Talos_S3` only ever talks to **togaiq**, so it needs the
79
+ togaiq-account key (the tools `[aws]` key is the toga-private key, which can cross-account *read*
80
+ togaiq but not *write* it).
58
81
  - **`App_Worker::enqueue()`** (`_/app/worker.php`) — inserts a `Core.WorkerJobs` row and sends
59
- the `{"workerJobId": id}` SQS message, so a 1.0 app can enqueue worker2 jobs.
82
+ the `{"workerJobId": id}` SQS message, so a 1.0 app can enqueue worker2 jobs. Requires the
83
+ `[worker]` (`queue_url`/`queue_region`) and `[aws]` (main-account key for SQS `SendMessage`)
84
+ config sections.
60
85
 
61
86
  ## Deploy prerequisites
62
87
 
63
88
  - `aws/aws-sdk-php` installed in `tools/vendor` (S3 + SQS).
64
- - New `tools/config.*.ini` sections (none exist yet): `[database_core2]` (writable 2.0 Core,
65
- dbname `Core`), `[worker]` (`queue_url` / `queue_region`), `[aws]` (production).
89
+ - `tools/config.production.ini` / `config.alpha.ini` sections (now added):
90
+ - **`[talos]`** — the **togaiq**-account S3 key (`App_Talos_S3` write path); required or move/edit/
91
+ delete fail with `AccessDenied`.
92
+ - **`[worker]`** — worker2 production SQS `queue_url` / `queue_region` (for `App_Worker::enqueue`).
93
+ - **`[aws]`** — main-account key for SQS `SendMessage` (toga-private key; cross-account reads
94
+ togaiq but cannot write it — that's why `[talos]` exists).
95
+ - `[database_toga2core]` — writable 2.0 Core (already present).
66
96
  - Apply the dbchanges2 `Team/2026-06-30a..e` + `Core/2026-06-30a` migrations in order.
67
97
  - Verify the Bedrock KB region / data-source name against the live account (TODOs in code).
68
98
 
@@ -70,12 +100,27 @@ prompt/replacement tables that the worker2 pipeline reads at runtime.
70
100
 
71
101
  - **KB list is derived from S3, not the DB.** The browse UI enumerates live S3 folders under
72
102
  `development-team/`; `Team.KnowledgeBases` is not the source of truth for what's shown.
73
- - **Delete is not just an S3 delete** — it also enqueues a worker `SyncKb` job to re-sync the
74
- Bedrock data source.
75
- - No new secret literals introduced; credentials come from the new config sections above.
103
+ - **Delete / move / edit are not just S3 ops** — each enqueues a worker `SyncKb` job to re-sync
104
+ the Bedrock data source.
105
+ - **`App_Talos_S3` needs the togaiq-account key (`[talos]`).** The tools `[aws]` key is the
106
+ toga-private key: it can cross-account *read* togaiq but **cannot write** it, so move/edit/delete
107
+ fail `AccessDenied` without a `[talos]` section.
108
+ - **Never use server-side `CopyObject` for togaiq** — the togaiq key is denied `CopyObject` even
109
+ within the same bucket; move/edit copy via get+put.
110
+ - **Move/edit re-sync must be one `SyncKb("source,dest")` job**, not two parallel jobs — Bedrock
111
+ allows only one in-flight ingestion per KB and parallel syncs race.
112
+ - No new secret literals introduced; credentials live in the config sections above (documented by
113
+ section/account, never by value).
76
114
 
77
115
  ## Change history
78
116
 
117
+ - 2026-07-02 — Added **move** (re-file a doc between KBs) and **edit** (title + Find&Replace + body,
118
+ ported from the old `kb_processor.php`) to the doc modal, each via get+put (togaiq denies
119
+ `CopyObject`) and each enqueuing a single `SyncKb("source,dest")` re-sync. Fixed togaiq
120
+ `AccessDenied`: `App_Talos_S3::client()` now uses a dedicated `[talos]` (togaiq-account) key; added
121
+ `[talos]`, `[worker]`, and `[aws]` sections to `config.production.ini`/`config.alpha.ini` so
122
+ `App_Worker::enqueue` (SQS) works. Last-modified now displayed in US Central (converted from S3
123
+ UTC). (jcardinal)
79
124
  - 2026-06-30 — Built the Talos KB admin UI: `/talos/kb-documents` (S3-derived KB list, approved
80
125
  doc browser with type-aware preview modal, download + delete-with-resync) and `/talos/vocabulary`
81
126
  (tabbed prompt/vocabulary/replacements manager with transactional full-set reconciliation;
@@ -13,7 +13,7 @@
13
13
  | [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. | _underscore/Model/Core/Model.php |
14
14
  | [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
15
15
  | [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 |
16
- | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql |
16
+ | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
17
17
  | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
18
18
  | [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
19
19
  | [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-19
9
+ updated: 2026-07-02
10
10
  owners: [jcardinal, rgirish]
11
11
  files:
12
12
  - _underscore/Model/Client/ItemFulfillment.php
@@ -16,6 +16,7 @@ files:
16
16
  - _underscore/Model/Compass/AdvanceShippingNotice.php
17
17
  - dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql
18
18
  - dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql
19
+ - dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql
19
20
  related:
20
21
  - ../architecture.md
21
22
  - ../../api2/architecture.md
@@ -102,12 +103,42 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
102
103
  upstream mirror. A later reconciling PUT cleans up stale records via set comparison. Full
103
104
  delete handling would need a `preDelete` mechanism + `PRE/DELETE` interceptor rows.
104
105
  - **Transfer-order fulfillments are explicitly excluded from the upstream walk.** `reconcileUpstreamLevel()` returns `null` immediately when the downstream IF has `transferOrderId` set (no `salesOrderId`). This is intentional for standalone TOs (GroWrk v1). When SO→TO→SO chain scenarios are needed, Phase 5 of the GroWrk transfer order plan extends this method to walk `TransferOrders_SalesOrders` bridge tables — see `clients/growrk/features/transfer-order-flow.md`.
106
+ - **Broken SOI↔POI bridge → empty header-only duplicate IFs (root cause, guard added
107
+ 2026-07-02).** The item walk maps a downstream IFI up via
108
+ `IFI.salesOrderItemId → PurchaseOrderItems_SalesOrderItems → SalesOrderItems_PurchaseOrderItems`.
109
+ When that upstream `SalesOrderItems_PurchaseOrderItems` bridge is **missing/broken** (the same
110
+ integrity class as the ~116 off-by-one Compass orders — see the Compass
111
+ `asn-to-item-fulfillment` doc), `$desiredItems` resolves to **zero** even though the downstream
112
+ IF has items. Pre-guard, step 4's eager resolve-or-create then POSTed an **empty header-only
113
+ upstream IF** (e.g. Compass SA133377 got duplicate empty customer-facing IFs 71724/71725
114
+ alongside the legit direct-ASN IF 70635). **Guard:** `reconcileUpstreamLevel` now, after
115
+ building `$desiredItems`, if it is empty **AND** the downstream IF has >0 ItemFulfillmentItems,
116
+ `error_log()`s a diagnostic and returns `null` (skips the upstream mirror) — distinguished from
117
+ the legitimate header-first POST case (downstream IF genuinely has 0 items), which is unchanged.
118
+ - **Duplicate customer-facing IF: the engine only adopts an upstream IF it created itself, never a
119
+ pre-existing one (known gap, adoption DEFERRED).** `reconcileUpstreamLevel` finds an existing
120
+ upstream IF only via `upstreamItemFulfillmentId`; it does **not** adopt a pre-existing IF that
121
+ already sits on the upstream SO from another flow (e.g. a direct-ASN-created IF). So a broken
122
+ bridge could yield two customer-facing IFs on the same SO (SA133377: direct-ASN IF 70635 + a
123
+ mirror-created duplicate). **Naive adoption is dangerous and was intentionally not implemented:**
124
+ step 5's delete-stale step deletes upstream IFIs with no matching downstream group, so adopting
125
+ an IF while the item walk is empty/broken would **DELETE the adopted IF's legitimate line items**.
126
+ Requires a CTO design review before implementing adoption semantics. The 2026-07-02 broken-bridge
127
+ guard prevents the duplicate cascade for this failure mode without needing adoption.
105
128
  - **Out of scope:** NetSuite sync of upstream IFs; transfer-order fulfillments
106
129
  (`transferOrderId`) are skipped.
107
130
  - **Performance:** every child write re-runs a full upstream walk (read-heavy, but each
108
131
  upstream record is written at most once — idempotent). Fine for normal fulfillment sizes.
109
132
 
110
133
  ## Change history
134
+ - 2026-07-02 — Added a **broken-bridge guard** to `reconcileUpstreamLevel`: when `$desiredItems`
135
+ is empty but the downstream IF has items (broken SOI↔POI bridge), it logs and returns null
136
+ instead of creating an empty header-only duplicate upstream IF. Root-caused on Compass SA133377
137
+ (SO 107609), where a broken bridge + a pre-existing direct-ASN IF produced duplicate empty
138
+ customer-facing IFs (71724/71725) and left the correct serial/tracking off the customer IF;
139
+ data repaired by `dbchanges2/Client_Compass/2026-07-02a`. Companion fix — adopting a pre-existing
140
+ upstream IF instead of creating a duplicate — was **DEFERRED** pending CTO review (the delete-stale
141
+ step would delete the adopted IF's line items when the item walk is empty). (jcardinal)
111
142
  - 2026-06-19 — Documented explicit TO exclusion in reconcileUpstreamLevel; cross-linked GroWrk transfer order plan. (rgirish)
112
143
  - 2026-06-08 — Documented the Recursive Item Fulfillments engine (interceptor-driven upstream fulfillment mirroring, bundle scaling, reconcile loop). (jcardinal)
113
144
 
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-06-23
9
+ updated: 2026-07-02
10
10
  owners: [jcardinal, mhammontree]
11
11
  files:
12
12
  - Core/
@@ -198,6 +198,41 @@ WHERE
198
198
  - Keep the single statement well under `max_allowed_packet` (64 MB default) — a few thousand
199
199
  rows is comfortably fine.
200
200
 
201
+ ## Self-referencing DELETE — wrap the subquery in a derived table
202
+
203
+ MySQL forbids a `DELETE` (or `UPDATE`) whose `WHERE` subquery reads **the same table being
204
+ modified** — it fails with **error 1093: "You can't specify target table 'X' for update in
205
+ FROM clause."** This bites cleanup migrations that delete duplicate rows by selecting which
206
+ rows to keep from the very table being pruned.
207
+
208
+ The fix is to **materialize the subquery through an extra derived-table layer**, forcing MySQL
209
+ to snapshot the row set before the delete runs:
210
+
211
+ ```sql
212
+ -- WRONG — error 1093, ItemFulfillments is both the DELETE target and read in the subquery
213
+ DELETE FROM ItemFulfillments
214
+ WHERE id NOT IN (
215
+ SELECT MIN(id)
216
+ FROM ItemFulfillments
217
+ GROUP BY salesOrderItemId, trackingSerial
218
+ );
219
+
220
+ -- CORRECT — wrap the inner SELECT in a derived table so it is materialized first
221
+ DELETE FROM ItemFulfillments
222
+ WHERE id NOT IN (
223
+ SELECT keepId FROM (
224
+ SELECT MIN(id) AS keepId
225
+ FROM ItemFulfillments
226
+ GROUP BY salesOrderItemId, trackingSerial
227
+ ) AS keep
228
+ );
229
+ ```
230
+
231
+ The extra `SELECT … FROM ( … ) AS keep` wrapper is the whole idiom: MySQL evaluates the inner
232
+ query into a temporary derived table, so the outer `DELETE` no longer "sees" a live read of
233
+ its own target. Used in `2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql` to prune
234
+ duplicate `ItemFulfillments` rows.
235
+
201
236
  ## Relationship to the rest of 2.0
202
237
 
203
238
  `dbchanges2` is registered as a **2.0 core repo** (`role: core` in `registry.json`) — it is
@@ -21,7 +21,7 @@
21
21
  | [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
22
22
  | [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
23
23
  | [Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)](features/talos-pricing-automation.md) | The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model` and tools `talos-pricing-ui` docs for the other halves). | worker2/Worker/Talos/Pricing.php, worker2/Database/TalosPricingCrons.sql |
24
- | [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | `_Worker_Team_Transcripts` (in addition to the upstream `Export` action — see [Teams Meeting Transcript Export](./teams-transcript-export.md)) now runs a fully | worker2/Worker/Team/Transcripts.php, worker2/bin/reprocess-transcripts.php, worker2/Config/production.ini, dbchanges2/Team/2026-06-30a, dbchanges2/Team/2026-06-30b, dbchanges2/Team/2026-06-30c, dbchanges2/Team/2026-06-30d, dbchanges2/Team/2026-06-30e, dbchanges2/Core/2026-06-30a |
24
+ | [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | `_Worker_Team_Transcripts` (in addition to the upstream `Export` action — see [Teams Meeting Transcript Export](./teams-transcript-export.md)) now runs a fully | worker2/Worker/Team/Transcripts.php, worker2/bin/reprocess-transcripts.php, worker2/bin/sync-knowledge-bases.php, worker2/Config/production.ini, dbchanges2/Team/2026-06-30a, dbchanges2/Team/2026-06-30b, dbchanges2/Team/2026-06-30c, dbchanges2/Team/2026-06-30d, dbchanges2/Team/2026-06-30e, dbchanges2/Core/2026-06-30a, dbchanges2/Core/2026-07-02a, dbchanges2/Team/2026-07-02a |
25
25
  | [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` (file `Worker/Team/Sprint.php`) is the engine behind TOGA's internal **development-sprint process and reporting**. | worker2/Worker/Team/Sprint.php |
26
26
  | [Teams Meeting Transcript Export](features/teams-transcript-export.md) | `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for Teams meeting transcripts produced by a set of organizers and archives t | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
27
27
  | [VAPI Webhook Handler (worker2 — AI-BDR end-of-call processing)](features/vapi-webhook-handler.md) | `_Worker_Vapi` ([worker2/Worker/Vapi.php](worker2/Worker/Vapi.php)) is the **PHP side of the AI-BDR call loop** — the webhook that receives VAPI's end-of-call r | worker2/Worker/Vapi.php, worker2/Worker/Ai/Bdr/Vapi.php |
@@ -6,11 +6,12 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-30
9
+ updated: 2026-07-02
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Team/Transcripts.php
13
13
  - worker2/bin/reprocess-transcripts.php
14
+ - worker2/bin/sync-knowledge-bases.php
14
15
  - worker2/Config/production.ini
15
16
  - dbchanges2/Team/2026-06-30a
16
17
  - dbchanges2/Team/2026-06-30b
@@ -18,6 +19,8 @@ files:
18
19
  - dbchanges2/Team/2026-06-30d
19
20
  - dbchanges2/Team/2026-06-30e
20
21
  - dbchanges2/Core/2026-06-30a
22
+ - dbchanges2/Core/2026-07-02a
23
+ - dbchanges2/Team/2026-07-02a
21
24
  related:
22
25
  - ./teams-transcript-export.md
23
26
  - ./creating-worker-actions.md
@@ -45,9 +48,13 @@ sidecar written to `s3://togaiq/development-team/{kb_slug}/approved/` → raw ar
45
48
  ## Key files / entry points
46
49
 
47
50
  - `Worker/Team/Transcripts.php` — actions `Scan`, `Process(string $sourceKey)`,
48
- `SyncKb(string $kbSlug)` (all alongside the existing `Export`).
51
+ `SyncKb(string $kbSlug)` (comma-separated slug list), `SyncKnowledgeBases(bool $apply=true)`
52
+ (all alongside the existing `Export`).
49
53
  - `bin/reprocess-transcripts.php` — standalone CLI backlog reprocessor.
50
- - `Config/production.ini` `[talos]` section — S3/Bedrock targets (see Configuration).
54
+ - `bin/sync-knowledge-bases.php` — CLI entry for the Bedrock→`Team.KnowledgeBases` registry
55
+ sync (`--dry-run` supported).
56
+ - `Config/production.ini` `[talos]` and `[teams]` sections — S3/Bedrock targets + the two
57
+ per-account S3 credential sets (see Configuration).
51
58
  - DB config + ledger tables in the `Team.*` schema (see Data model), authored in dbchanges2
52
59
  under `Team/2026-06-30a..e` + `Core/2026-06-30a`.
53
60
 
@@ -61,28 +68,56 @@ each via `_Worker::runTask('Team/Transcripts/Process', ['sourceKey' => …])`. S
61
68
 
62
69
  ### `Process(string $sourceKey)`
63
70
  Port of `kb_processor`, per transcript:
64
- 1. Fetch the raw VTT from S3 (`toga-private`, **us-west-2**).
71
+ 1. Fetch the raw VTT from S3 (`toga-private`, **us-west-2**). Parse **date + meeting title up
72
+ front** (previously parsed from the filename only *after* classification).
65
73
  2. Call the TOGa IQ AI endpoint `https://api.togaiq.com/api/ai/generate`, model
66
74
  `bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0`, with an **extended** `output_schema`
67
75
  returning **both** `knowledge_doc` **and** `kb_slug` — the AI picks the single best active KB
68
- from the supplied list, or `general`. The system prompt is **assembled at runtime** from DB
69
- tables: a template body with `{{APPLICATIONS}}`/`{{CLIENTS}}`/`{{PEOPLE}}`/`{{BUSINESS_TERMS}}`/`{{EXAMPLES}}`
70
- placeholders filled from `Team.TranscriptPromptTerms`.
76
+ from the supplied list, or `general`. The **meeting title is passed into**
77
+ `callAICleaningAPI` and surfaced as a `MEETING TITLE:` block; the classifier instruction
78
+ **prioritizes the title** (often the only KB signal on short transcripts). **Classification
79
+ rule: customer over company** — `toga-technology` is our *own* company, so a meeting involving
80
+ both a customer and TOGA staff must classify to the **customer** KB; pick `toga-technology`
81
+ only for purely-internal meetings; fall back to `general` when neither can be determined. The
82
+ system prompt is **assembled at runtime** from DB tables: a template body with
83
+ `{{APPLICATIONS}}`/`{{CLIENTS}}`/`{{PEOPLE}}`/`{{BUSINESS_TERMS}}`/`{{EXAMPLES}}`
84
+ placeholders filled from `Team.TranscriptPromptTerms`. The active template body also
85
+ **excludes personal/social/non-work content** and must never emit a "Personal Notes" section
86
+ (see Change history / the 2026-07-02a Team migration).
71
87
  3. Apply DB-driven text replacements from `Team.TranscriptReplacements` (`:c` case-sensitive,
72
88
  `:w` whole-word, `:cw`) via `preg_replace_callback` **so replacement backrefs stay literal**.
73
89
  4. Build the approved name `"{YYYY-MM-DD} - {Title Case}.txt"`.
74
- 5. Upload to `s3://togaiq/development-team/{kb_slug}/approved/` (**us-east-1**) plus a
75
- `{name}.txt.metadata.json` Bedrock sidecar.
76
- 6. Archive the raw VTT to `…/{kb_slug}/archive/{date}/`, then delete the source.
90
+ 5. Upload to `s3://togaiq/development-team/{kb_slug}/approved/` (**us-east-1**, togaiq account
91
+ client) plus a `{name}.txt.metadata.json` Bedrock sidecar.
92
+ 6. Archive the raw VTT to `…/{kb_slug}/archive/{date}/` via **get(raw) + put(talos)** (NOT
93
+ `CopyObject` — source and dest are in **different AWS accounts**, so cross-account
94
+ `CopyObject` is impossible), then delete the source.
77
95
  7. AWS Bedrock `startIngestionJob` on that KB's data source (retry 5×30s on
78
96
  `ConflictException` / throttling). **If `kb_slug === 'general'`, sync ALL active KBs.**
79
97
 
98
+ ### `SyncKnowledgeBases(bool $apply=true)` (cron)
99
+ Keeps `Team.KnowledgeBases` current with **Bedrock as the source of truth** — the table only
100
+ had the KBs that existed as S3 folders at build time (`general`, `office-depot`), so the AI
101
+ classifier could never pick a KB (e.g. `elite`) that wasn't already a candidate. Lists all
102
+ Bedrock KBs (`listKnowledgeBases`, via helper `listAllKnowledgeBases()`), filters to the
103
+ `{kb_prefix}*` prefix (`development-team-`), derives the slug, and **upserts**: new rows insert
104
+ `isActive=1` (`isGeneral=1` only for `general`), name title-cased, `bedrockKbId` cached; existing
105
+ rows keep their name/flags and only refresh `bedrockKbId`. **Nothing is deleted or deactivated.**
106
+ CLI entry `bin/sync-knowledge-bases.php` (`--dry-run`). Cron: `Core.CronJobs` row action
107
+ `Team/Transcripts/SyncKnowledgeBases`, schedule `0 5 * * *` (daily 05:00 CT), `maxExecutionTime`
108
+ 300 (dbchanges2 `Core/2026-07-02a`; depends on the 2026-05-14 `maxExecutionTime` migration).
109
+
80
110
  Idempotency + status are tracked in `Team.TranscriptProcessing`
81
111
  (`pending/cleaned/uploaded/archived/synced/failed`).
82
112
 
83
113
  ### `SyncKb(string $kbSlug)`
84
- On-demand single-KB re-sync (used by the Tools delete flow — see
114
+ On-demand re-sync used by the Tools delete/move/edit flows (see
85
115
  [Talos KB Documents Admin](../../../1.0/apps/tools/features/talos-kb-documents-admin.md)).
116
+ `$kbSlug` now accepts a **comma-separated slug list** and syncs each KB **sequentially** with
117
+ `KB_SYNC_DELAY_SECONDS` spacing. This exists because **Bedrock allows only one in-flight
118
+ ingestion per KB** — a move that fired two independent parallel `SyncKb` workers raced and one
119
+ was dropped (only the destination synced). The Tools move now enqueues a **single**
120
+ `SyncKb("source,dest")` job so both KBs sync in one job, in order.
86
121
 
87
122
  ### `bin/reprocess-transcripts.php` (CLI backlog reprocessor)
88
123
  Standalone CLI that bootstraps `_underscore`, lists existing raw transcripts under the
@@ -109,7 +144,10 @@ PKs `INT UNSIGNED` to match the existing Talos Team-table family.
109
144
  - **`Team.TranscriptPromptTemplate`** — `name, bodyText` (w/ placeholders), `instruction, model,
110
145
  temperature, maxTokens, timeoutSeconds, isActive`. One active row seeded from the `.ini` system prompt.
111
146
  - **`Team.KnowledgeBases`** — `slug, name, isGeneral, bedrockKbId, isActive`. Seeded `general` +
112
- `office-depot`. NOTE: the Tools browse UI derives the **live** KB list from S3 folders, not this table.
147
+ `office-depot`, but now **auto-kept-current from Bedrock** by the daily `SyncKnowledgeBases`
148
+ action (Bedrock is the source of truth; the table is a cache/allowlist of classifier
149
+ candidates). NOTE: the Tools *browse* UI still derives its live KB list from S3 folders, not
150
+ this table.
113
151
  - **`Team.TranscriptProcessing`** — `sourceKey UNIQUE(255), kbSlug, approvedKey, archiveKey,
114
152
  status` ENUM (`pending/cleaned/uploaded/archived/synced/failed`), `aiModel, failureReason,
115
153
  dtProcessed`. Pipeline ledger + idempotency.
@@ -122,10 +160,22 @@ PKs `INT UNSIGNED` to match the existing Talos Team-table family.
122
160
  `worker2/Config/production.ini` `[talos]`: `s3_bucket=togaiq`, `s3_region=us-east-1`,
123
161
  `bedrock_region=us-east-1`, `kb_prefix=development-team-`, `s3_root_path=development-team/`.
124
162
 
163
+ **Two S3 credential sets — one per AWS account (see the cross-account gotcha):**
164
+ - `[teams]` `s3_access_key_id` / `s3_secret_access_key` — the **toga-private** key (raw
165
+ transcripts, us-west-2). Read by `s3Client()` / `getS3Client()`.
166
+ - `[talos]` `s3_access_key_id` / `s3_secret_access_key` — the **togaiq** key (approved/archive
167
+ KB bucket, us-east-1). Read by `getTalosS3Client()` and `getBedrockClient()`.
168
+
169
+ Credential **values live only in the deployed `production.ini`** — documented here by section
170
+ and account, never by value. Pre-existing plaintext keys should be rotated (separate track).
171
+
125
172
  ## Gotchas / known issues
126
173
 
127
- - **Cross-region S3.** The raw bucket `toga-private` is **us-west-2** while `togaiq` is
128
- **us-east-1**. `Process()` uses two S3 clients; the archive step is a **cross-region copy**.
174
+ - **Cross-ACCOUNT S3 (root cause of repeated PutObject/CopyObject `AccessDenied`).** `toga-private`
175
+ (raw, us-west-2) and `togaiq` (KB bucket, us-east-1) live in **different AWS accounts** — **no
176
+ single IAM key can write to both.** Credentials are therefore **per-bucket**: `[teams]` key for
177
+ toga-private, `[talos]` key for togaiq. Because it spans accounts, **`CopyObject` is impossible**;
178
+ the archive step is a **get(raw) + put(talos)**, not a server-side copy.
129
179
  - **`[talos]` keys were added to `production.ini` only** — the beta/development configs still
130
180
  need them before the pipeline runs in those environments.
131
181
  - **`general` KB fans out.** A `Process()` classified as `general` (or a `general` sync) triggers
@@ -143,6 +193,15 @@ PKs `INT UNSIGNED` to match the existing Talos Team-table family.
143
193
 
144
194
  ## Change history
145
195
 
196
+ - 2026-07-02 — Fixed cross-**account** S3 creds: `toga-private` and `togaiq` are separate AWS
197
+ accounts, so credentials are now per-bucket (`[teams]` vs `[talos]` in `production.ini`) and the
198
+ archive step is get+put instead of the impossible cross-account `CopyObject`. Built
199
+ `SyncKnowledgeBases` (Bedrock→`Team.KnowledgeBases` upsert) + `bin/sync-knowledge-bases.php` and a
200
+ daily `0 5 * * *` cron so the classifier's candidate KB list stays current. `SyncKb` now takes a
201
+ comma-separated slug list and syncs sequentially (Bedrock allows one in-flight ingestion per KB;
202
+ parallel syncs raced). Classification: meeting title now passed to and prioritized by the AI, and
203
+ customer-over-company (a customer meeting stays with the customer even when TOGA staff attend).
204
+ Prompt template surgically updated to exclude personal/social content ("Personal Notes"). (jcardinal)
146
205
  - 2026-06-30 — Built the automated cron-driven ingestion pipeline (`Scan`/`Process`/`SyncKb`) +
147
206
  the `bin/reprocess-transcripts.php` backlog CLI, porting the manual `kb_processor.php`. Added
148
207
  the `Team.*` config/ledger schema (DB-editable replacements, prompt terms, prompt template, KB
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
- | [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql |
5
+ | [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
6
6
  | [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql |
7
7
  | [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
8
8
  | [Compass MITS PO Transmission to Vendors](features/mits-po-transmission-to-vendors.md) | 2.0 | The 1.0 worker cron `2_transmit_mits_purchase_orders_to_vendors.php` transmits Compass PurchaseOrders to their vendors (Office Depot, Strategic Systems, Compass | worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, library/app/client/compass.php |
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: compass-usa
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-06-30
8
+ updated: 2026-07-02
9
9
  owners: [jcardinal, bala]
10
10
  files:
11
11
  - _underscore/Model/Compass/AdvanceShippingNotice.php
@@ -18,6 +18,7 @@ files:
18
18
  - dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql
19
19
  - dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql
20
20
  - dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql
21
+ - dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql
21
22
  related:
22
23
  - ../../../2.0/apps/_underscore/features/recursive-item-fulfillments.md
23
24
  - ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
@@ -207,6 +208,18 @@ Compass Canada (`Model/Compass/Canada/`) is a separate sub-client. The Compass c
207
208
 
208
209
  ## Change history
209
210
  Dated one-liners, newest first.
211
+ - 2026-07-02 — Repaired SA133377 (SO 107609, line 1 MD7F4LL/A-S): the customer IF (IFI 276821)
212
+ had no serial and the wrong tracking (`522944658493` — the first of two tracking-only vendor
213
+ ASNs on PO 103641 that reconciled onto the direct-ASN IF 70635) instead of `873840444805`
214
+ (the real device unit 63108 / serial SKRX9Y4W537 arrived up the mirror chain from downstream
215
+ IF 71723). Migration `2026-07-02a` adds the missing IFIU + correct unit-level tracking, swaps
216
+ the stale header/item tracking, and retires the empty duplicate IF 71725 (idempotent, sql-reviewer
217
+ SAFE). Root cause was a broken `SalesOrderItems_PurchaseOrderItems` bridge on the downstream
218
+ shipped SO item (PO item 179306, no upstream bridge — same off-by-one integrity class as the
219
+ ~116 orders above) feeding the recursive mirror engine, which then created empty duplicate
220
+ customer-facing IFs; forward engine guard added in the
221
+ [Recursive Item Fulfillments](../../../2.0/apps/_underscore/features/recursive-item-fulfillments.md)
222
+ doc (2026-07-02). (jcardinal)
210
223
  - 2026-06-30 — `resolveOrCreateItemFulfillment` now resolves the shipped `ItemFulfillmentStage` by
211
224
  status slug and sets `itemFulfillmentStage` on the IF payload, so ASN-created fulfillments land
212
225
  shipped (not stage-less) on both Compass USA + Canada. Part of the platform-wide IF stage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.265",
3
+ "version": "1.0.267",
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",