toga-ai 1.0.601 → 1.0.602

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.
@@ -35,7 +35,7 @@
35
35
  | [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/Model.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
36
36
  | [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
37
37
  | [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. | _underscore/Model/Client/Logs/Record.php, _underscore/Model/Client/Logs/RecordField.php, _underscore/Model/Client/Logs/CustomRecordField.php |
38
- | [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 |
38
+ | [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, dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql |
39
39
  | [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
40
40
  | [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, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.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, api2/Component/Api/V2/V2.php |
41
41
  | [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-02
9
+ updated: 2026-08-18
10
10
  owners: [jcardinal, rgirish]
11
11
  files:
12
12
  - _underscore/Model/Client/ItemFulfillment.php
@@ -17,6 +17,7 @@ files:
17
17
  - dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql
18
18
  - dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql
19
19
  - dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql
20
+ - dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql
20
21
  related:
21
22
  - ../architecture.md
22
23
  - ../../api2/architecture.md
@@ -55,8 +56,10 @@ fire those records' interceptors) no-op. A `while` loop walks UP one level at a
55
56
  1. Load downstream IF; no `salesOrderId` (transfer order) → return null (out of scope).
56
57
  2. Resolve upstream SO via the **header walk** (bridge tables); none → null (top of chain).
57
58
  3. Build desired upstream items: map each downstream IFI's SOI up via the **item walk**,
58
- GROUP BY upstream SOI, sum fulfilled qty, then **scale into upstream order units** for
59
- bundle decomposition (see Gotchas).
59
+ GROUP BY upstream SOI, sum fulfilled qty, then **scale into upstream order units** — but
60
+ only for genuine multi-component bundles, keyed on **distinct downstream items** (see
61
+ Gotchas → Bundle scaling). A single distinct downstream item (incl. accepted over-ship)
62
+ mirrors unscaled.
60
63
  4. Resolve-or-create the upstream IF (**eager** — header-only POST still builds the chain):
61
64
  load existing via `upstreamItemFulfillmentId` and PUT changed header fields, or POST a
62
65
  new IF with mirrored header + upstream SO then PUT the downstream IF's link.
@@ -95,9 +98,36 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
95
98
  `…Put.sql` for POST/PUT) against that env's Core DB — the PHP won't fire on PUT otherwise.
96
99
  - **`depth: -1` always** on orchestration calls. `depth => 0` means *unlimited* FK
97
100
  traversal → OOM. Never use 0.
98
- - **Bundle scaling:** `upstreamQty = totalDownstreamFulfilled × upstreamOrderedQty /
99
- totalDownstreamOrderedQty`, **capped at the raw sum** so the factor can only reduce
100
- (roll-up), never inflate. A qty-1 bundle of 6 fully-shipped components → upstream qty 1.
101
+ - **Bundle scaling is gated on DISTINCT DOWNSTREAM ITEMS, not downstream SOI lines
102
+ (corrected 2026-08-18).** A "Recipe" query, per upstream SOI, takes **one ordered-qty slot
103
+ per distinct downstream `itemId`** (MAX ordered qty across duplicate same-item sourcing
104
+ lines) and counts the downstream lines. Scaling
105
+ (`upstreamQty = totalDownstreamFulfilled × upstreamOrderedQty / Σ distinctDownstreamOrderedQty`,
106
+ **capped at the raw sum** so it can only reduce, never inflate) applies **only when
107
+ `distinctDownstreamItemCount > 1`** — a genuine multi-component bundle. A qty-1 bundle of 6
108
+ fully-shipped distinct components still → upstream qty 1 (6×1/6); 3×2/6=1 preserved. When a
109
+ single distinct downstream item is sourced (incl. an accepted over-shipment split across
110
+ several downstream SOs/SOIs), the raw fulfilled qty mirrors up **unscaled** (round() applied
111
+ symmetrically on this branch too).
112
+ - **Same-item over-shipment used to mis-scale into fractions (root cause, fixed 2026-08-18).**
113
+ Pre-fix the denominator summed ordered qty over *distinct downstream SOI lines*. A vendor
114
+ (e.g. Office Depot / ODP) can create **multiple downstream SalesOrders for the SAME item off
115
+ ONE upstream PO line** (accepted over-ship / re-send); each duplicate is another distinct
116
+ downstream SOI, inflating the denominator and shrinking the factor. Compass SA135471's hero
117
+ SOI (item 2682, qty 1) mapped to two ODP SOIs (same item, each qty 1) via one POI, so factor
118
+ = 1/2 and each of two customer IFs showed **qty 0.50** ("Quantity Fulfilled" 0.5 + 0.5). Two
119
+ physically distinct serials had shipped — a real over-ship, not a phantom duplicate. Fix:
120
+ distinct-item denominator (above) mirrors the raw qty (→ 2). Data repaired by
121
+ `dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql`.
122
+ - **Mixed topology (a real bundle whose component is ALSO sourced across multiple downstream
123
+ lines) is logged, not silently averaged.** When the Recipe's `lineCount > distinctItemCount`,
124
+ the engine `error_log()`s a diagnostic instead of averaging — same log-don't-swallow rule as
125
+ the broken-bridge guard.
126
+ - **Diagnosing over-ship vs. phantom duplicate:** traverse the bridge — upstream SOI →
127
+ `SalesOrderItems_PurchaseOrderItems` → POI → `PurchaseOrders_SalesOrders` → downstream ODP
128
+ SOs (`customerId=1`); compare downstream vs. upstream `itemId` (**same itemId ⇒ over-ship /
129
+ duplicate sourcing, NOT a bundle**); then check `ItemFulfillmentItemUnits` serials — distinct
130
+ serials ⇒ genuine over-shipment, identical ⇒ phantom duplicate.
101
131
  - **DELETE propagation is NOT implemented.** The engine sets `$outData = null` on DELETE so
102
132
  post-delete hooks never fire; a standalone downstream DELETE doesn't immediately remove its
103
133
  upstream mirror. A later reconciling PUT cleans up stale records via set comparison. Full
@@ -131,6 +161,19 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
131
161
  upstream record is written at most once — idempotent). Fine for normal fulfillment sizes.
132
162
 
133
163
  ## Change history
164
+ - 2026-08-18 — **Bundle scaling now discriminates on distinct downstream items, not SOI lines
165
+ (CTO-approved "Option B").** `reconcileUpstreamLevel`'s Recipe query takes one ordered-qty
166
+ slot per distinct downstream `itemId` and scales only when `distinctDownstreamItemCount > 1`;
167
+ a single distinct item (incl. accepted over-ship split across multiple downstream SOs)
168
+ mirrors the raw fulfilled qty unscaled. Mixed topology (recipe lineCount > distinctItemCount)
169
+ is `error_log`'d, not averaged. Added `(int)` casts on the method's trusted-PK SQL
170
+ interpolations and round() symmetry on the unscaled branch. Root-caused on Compass prod
171
+ SA135471: the hero item (2682, qty 1) sourced via two ODP SalesOrders off one PO line showed
172
+ "Quantity Fulfilled" 0.5 + 0.5 across two customer IFs (old denominator summed distinct SOI
173
+ lines → factor 1/2); two distinct serials confirmed a genuine over-ship. Data repaired by the
174
+ idempotent retro migration `dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql`
175
+ (IFI 302121 / 302126 quantity 0.50 → 1.00). Deploy order: `_underscore` code first, then the
176
+ SQL against the Compass DB. Neither deployed at capture time. (jcardinal)
134
177
  - 2026-07-02 — Added a **broken-bridge guard** to `reconcileUpstreamLevel`: when `$desiredItems`
135
178
  is empty but the downstream IF has items (broken SOI↔POI bridge), it logs and returns null
136
179
  instead of creating an empty header-only duplicate upstream IF. Root-caused on Compass SA133377
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.601",
3
+ "version": "1.0.602",
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",