toga-ai 1.0.224 → 1.0.225

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.
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-26
9
+ updated: 2026-06-29
10
10
  owners: [dfranks]
11
11
  files:
12
12
  - test/@dave/checker.php
@@ -174,6 +174,22 @@ by reconciling a chosen tranDate range directly against NetSuite.
174
174
  stays bounded under the existing 1G `memory_limit`. (This corrected a prior docblock claim that `$sqls`
175
175
  was never accumulated globally.)
176
176
 
177
+ - **PROFIT is now compared for all THREE categories, not just Sales.** `checker.php` and `fixer.php` were
178
+ previously profit-aware only for Sales; they now compute and compare profit for **Open Orders** and
179
+ **Opportunities** too.
180
+ - **Open-order profit** mirrors fixer's per-line economics: `qtyOpen * (rate - unitCost)`,
181
+ `unitCost = costestimate/quantity` (divide-by-zero-safe), **round-then-sum per line** to match the stored
182
+ `decimal(14,2)` `OpenOrderItems.profit` column.
183
+ - **Opportunity profit** = the legacy importer's `Σ(amount − costEstimate)` over qualifying lines = SuiteQL
184
+ `Σ(-foreignamount + costestimate)`; **revenue** stays the header `projectedtotal` (pulled line-level via
185
+ `LEFT JOIN` + `MAX(projectedtotal)`). It **must EXCLUDE item-group parent lines** — SuiteQL
186
+ `itemtype = 'Group'` ↔ `App_Model_Forecast2_Item::NETSUITE_TRANSACTION_TYPE__ITEM_GROUP`, which the
187
+ importer skips. A dedicated `OPP_EXCLUDED_ITEMTYPES` constant (= the ORD exclusion list + `'Group'`)
188
+ carries this.
189
+ - **`fixer.fixOpportunities()` previously did NOT write `projectedProfit` at all.** It now computes
190
+ `projectedProfit` from the `listOpportunities()` itemList (`Σ(amount − costEstimate)`, skipping ITEM_GROUP
191
+ and items missing from the `Items` lookup, rounded to cents) so the FIX can actually resolve any
192
+ opportunity-profit drift the FIND flags (see the compute-identically invariant below).
177
193
  - **checker → fixer handoff is WINDOW-based, not id-based (decision).** checker hands fixer a tight day
178
194
  window and lets fixer **re-find** the discrepant ids inside it, rather than passing exact ids. Rationale:
179
195
  maker≠checker independence (fixer re-derives the drift), and checker has already shrunk the window, so a
@@ -269,6 +285,40 @@ None — Forecast2 is a single shared dataset.
269
285
  (NS 852.06 vs FC 852.07). **Correction to any prior note:** the open-orders **2025-04 "unlocalized month" was
270
286
  this round-then-sum noise, NOT NS-live-vs-Forecast-replica timing jitter** — the rounding fix eliminated it,
271
287
  disproving the earlier replica-jitter hypothesis.
288
+ - **NULL `rate` on cost-only open-order lines silently drops them from a NS profit SUM → permanent phantom
289
+ profit drift FIX can never resolve.** NetSuite open-order **cost-only** lines (zero revenue — e.g.
290
+ freight/handling sub-items) can carry a **NULL `rate`**. The FIND profit SQL computed `tl.rate - unitCost`;
291
+ in SQL `NULL - x = NULL`, so `ROUND(...)` returned NULL and the whole line **silently dropped out of
292
+ `SUM(...)`**, inflating the NS-side profit. But fixer's FIX (PHP) and the legacy importer treat a NULL rate
293
+ as **0** (`(float)$rate ?? 0`), so the stored Forecast2 profit was already correct. The two conventions
294
+ disagreed, so FIND kept flagging profit drift (e.g. Δprofit **$0.54** on many identical **$12.20** SOs;
295
+ large **$1,365–$9,500** deltas on **$0-revenue** SOs) that FIX could never close — every FIX reported
296
+ `ins=0/upd=0/del=0` because the line was already right. **Revenue never exposed it** (those lines are $0
297
+ revenue either way: NULL-dropped on NS vs 0-stored on FC both sum the same), so it surfaced **only in the
298
+ profit dimension**. **Fix:** wrap rate as `NVL(tl.rate, 0)` in the open-order **revenue AND profit**
299
+ expressions so FIND uses the same "NULL rate = 0" convention as FIX/importer. Applied in **both**
300
+ `checker.php` (openorders `nsSub`) and `fixer.php` (`findOpenOrderDiscrepancies`). After the fix all three
301
+ categories report ✓ in sync — there was never real drift, so **no production write was needed**.
302
+ **Diagnostic that nailed it:** a per-line probe dumping NS `transactionline`
303
+ (`lineid`/`item`/`itemtype`/`qty`/`billed`/`rate`/`costestimate` + recomputed profit) alongside the stored
304
+ `OpenOrderItems` rows, which revealed the NULL `rate` on the cost-only lines. (This is a *distinct*
305
+ convention-mismatch from the round-then-sum gotcha above — there the divergence was rounding; here it is
306
+ NULL handling. Both are "FIND and FIX must compute identically.")
307
+ - **INVARIANT: checker FIND, fixer FIND, and fixer FIX must compute a given dimension IDENTICALLY, or `--fix`
308
+ chases phantoms forever.** Any divergence in inclusion filter, rounding, or NULL handling between the three
309
+ passes manifests as a discrepancy FIND flags and FIX refuses to resolve (`ins=0/upd=0/del=0`), looping
310
+ forever. Both the round-then-sum and the NULL-rate gotchas above are instances. When adding/changing a
311
+ compared dimension, change all three sites in lockstep. `listOpportunities()`
312
+ (`library/app/api/netsuite/rest.php`) returns line items with `amount = -foreignamount` and
313
+ `costEstimate = -costestimate`, so the REST-itemList sum and the SuiteQL line-sum are **algebraically
314
+ identical** — that equivalence is what lets fixer's FIX (REST itemList) and FIND (SuiteQL) agree on
315
+ opportunity profit.
316
+ - **PHASE 3 post-fix re-check ignores `--category` and re-runs ALL three FIND passes (known inefficiency,
317
+ NOT yet fixed).** `fixer.php` PHASE 3 (post-fix re-check) re-runs all three category FIND passes
318
+ unconditionally, ignoring `--category` / `$runCats`. The **Sales** per-transaction aggregate SuiteQL
319
+ (~694K-row Sales table, paginated 1000/page) is the heaviest op in the tool, so even a category-scoped run
320
+ (e.g. openorders-only) triggers a **minute-plus full Sales re-scan that looks like a hang**. Suggested fix
321
+ (not applied): gate each PHASE 3 FIND on `in_array($cat, $runCats)`.
272
322
  - **Compare money at 2 decimals.** DB columns store 2dp but PHP `revenue - cost` carries float
273
323
  dust (`313.6` vs `313.60000001`); raw `!=` produced thousands of phantom UPDATEs that re-wrote
274
324
  identical values (1,839 on one open-orders run). Both tools now compare `round((float)$x, 2)`.
@@ -360,6 +410,23 @@ None — Forecast2 is a single shared dataset.
360
410
 
361
411
  ## Change history
362
412
 
413
+ - 2026-06-29 — **Found/fixed the NULL-rate phantom open-order profit drift; extended PROFIT comparison to all
414
+ three categories.** Cost-only NS open-order lines (zero revenue) can have a **NULL `rate`**; the FIND
415
+ profit SQL did `tl.rate - unitCost` and `NULL - x = NULL` silently dropped the line from `SUM`, inflating
416
+ NS profit, while FIX/importer treat NULL rate as 0 — a permanent phantom (Δprofit $0.54 on identical $12.20
417
+ SOs; $1,365–$9,500 on $0-revenue SOs) FIX reported `ins/upd/del=0` on forever. Revenue never showed it ($0
418
+ either way). **Fix:** `NVL(tl.rate,0)` in the open-order revenue AND profit expressions in **both**
419
+ `checker.php` (openorders `nsSub`) and `fixer.php` (`findOpenOrderDiscrepancies`); all three categories then
420
+ ✓ in sync, no prod write needed. Localized via a per-line NS `transactionline` probe. Also: checker/fixer
421
+ now compare **profit for Open Orders and Opportunities**, not just Sales — open-order profit
422
+ `qtyOpen*(rate-unitCost)` round-then-sum per line; opportunity profit `Σ(amount−costEstimate)` excluding
423
+ `itemtype='Group'` (= `ITEM_GROUP`, new `OPP_EXCLUDED_ITEMTYPES` constant), revenue = header
424
+ `projectedtotal`; `fixer.fixOpportunities()` now writes `projectedProfit` (previously never did) from the
425
+ `listOpportunities()` itemList. Recorded the cross-toolchain **invariant**: checker FIND, fixer FIND, and
426
+ fixer FIX must compute each dimension identically or `--fix` chases phantoms. **Known inefficiency (not
427
+ fixed):** `fixer.php` PHASE 3 post-fix re-check re-runs all three FIND passes ignoring `--category`, so a
428
+ scoped run still triggers the heavy ~694K-row Sales re-scan (looks like a hang); suggested gate on
429
+ `in_array($cat,$runCats)`. (dfranks)
363
430
  - 2026-06-26 — **Added `checker.php` (moment-fingerprint drift detector) + `looper.php` (loop runner), and
364
431
  found/fixed the open-orders round-then-sum rounding gotcha.** `checker.php` confirms sync in seconds and
365
432
  localizes real drift via a 5-number per-bucket fingerprint (`n,rev,profit,idSum,idSq`; power-sums mod the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.224",
3
+ "version": "1.0.225",
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",