toga-ai 1.0.221 → 1.0.222

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.
@@ -5,7 +5,7 @@
5
5
  | [Worker (1.0 Framework) Architecture](architecture.md) | `worker` is the legacy (**1.0** `App_` framework) **background-job tier**. | worker/index.php, worker/_/app/framework.php, worker/crons/, worker/schedules/, worker/ebs/cron.worker.php, worker/.ebextensions/035_cron.worker.config |
6
6
  | [Compass MA Sales Order Exception Report](features/compass-ma-sales-order-exception-report.md) | A worker cron that emails operations the "Compass Refresh Exception Report" — Compass `MA%` sales orders whose corresponding Office Depot (ODP) sales order has | worker/crons/toga2/compass/workflow/7_generate_ma_sales_order_exception_report.php |
7
7
  | [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php |
8
- | [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
8
+ | [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/checker.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
9
9
  | [NetSuite → TOGa Supply Per-Client Sync (thin wrappers)](features/netsuite-togasupply-per-client-sync.md) | Syncs NetSuite transactions (sales orders, purchase orders, invoices, item receipts, item fulfillments, inventory adjustments) into each TOGa Supply (2.0) clien | worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_canon.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql |
10
10
  | [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily cron (9:00 PM) that emails Prudential and Dell stakeholders an Excel report of all devices shipped that day, including tracking number, serial number, emp | worker/crons/notifications/reports/send_shipments_for_the_day.php |
11
11
  | [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer (`worker/crons/toga2/netsuite/`). | worker/crons/toga2/netsuite/sync_togasupply.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql |
@@ -6,9 +6,11 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-25
9
+ updated: 2026-06-26
10
10
  owners: [dfranks]
11
11
  files:
12
+ - test/@dave/checker.php
13
+ - test/@dave/looper.php
12
14
  - test/@dave/reconcile_netsuite_totals.php
13
15
  - test/@dave/fixer.php
14
16
  - test/@dave/analyze_netsuite_forecast_diff.php
@@ -42,6 +44,37 @@ by reconciling a chosen tranDate range directly against NetSuite.
42
44
 
43
45
  ## Key files / entry points
44
46
 
47
+ - `checker.php [--from --to] [--category sales|openorders|opportunities|all] [--verbose] [--no-banner] [--fix --prod]`
48
+ — **hyper-fast drift detector** using a **moment-fingerprint drill-down** (default window = YTD).
49
+ Instead of materializing every row on both sides and diffing (the OOM trap that gates `reconcile`'s
50
+ per-txn diffs), each side computes a **5-number fingerprint PER TIME BUCKET** entirely in-DB in one
51
+ `GROUP BY`: `n` = COUNT of non-zero-measure records; `rev` = `ROUND(SUM(revenue),2)`;
52
+ `profit` = `ROUND(SUM(profit),2)` (Sales only); `idSum` = `MOD(SUM(MOD(id,P)),P)`;
53
+ `idSq` = `MOD(SUM(MOD(id*id,P)),P)`, where `P = 2147483647` (2³¹−1 Mersenne prime). `(rev,profit)`
54
+ catch **value drift**; `(n,idSum,idSq)` are a **power-sum fingerprint of the id SET** that catches the
55
+ **compensating case** a plain SUM total can't see (a missing txn masked by an extra txn of equal value).
56
+ Power-sums are pure integer arithmetic, so they compute **identically on Oracle-flavored SuiteQL and
57
+ MySQL** (no cross-engine hash-portability problem); `MOD` by the prime keeps the running sums inside
58
+ 64-bit. Compared **top-down, descending only where fingerprints disagree**: L1 `GROUP BY` month → L2
59
+ `GROUP BY` day (only inside bad months) → L3 per-id list (`--verbose`, reporting only). A clean system
60
+ **stops at L1 in seconds pulling no rows**. Match identity is the **NetSuite internal id**
61
+ (`netsuiteTransactionInternalId` / `netsuiteSalesOrderInternalId` / `netsuiteOpportunityInternalId`);
62
+ date + category are only **scoping/localization**, never identity. Identity (count + id-moments) is
63
+ computed over **non-zero-measure rows only**, so checker's "in sync" verdict equals fixer.php's "nothing
64
+ to fix" **by construction** (a missing $0 row is a non-issue). On drift it hands `fixer.php` a **tight
65
+ day window** (window handoff, not id handoff — see decision below). `--fix` requires `--prod` (auto-runs
66
+ fixer on the localized window); a **bare run never writes**. Reads the prod core2 **read-replica**
67
+ directly (same pattern as `reconcile_netsuite_totals.php` / `fixer.php` — creds live in
68
+ `worker/config.worker.ini` / `CLAUDE.md`, not reproduced here).
69
+ - `looper.php [--interval=60] [--count=0] [--cmd "<verbatim>"] [--args "<extra checker flags>"] [--no-banner] [--no-fix]`
70
+ — **generic loop runner**, self-contained (no framework bootstrap; shells out via `passthru`). **DEFAULT
71
+ BEHAVIOR (bare `php looper.php`): loops `checker.php --no-banner --fix --prod` every 60s — i.e. it
72
+ continuously AUTO-CORRECTS PRODUCTION** (prints a bold warning Mode line; the once-only LOOPER ASCII
73
+ banner gets the subline "Looper / Checker / Fixer" in fix-prod mode). `--no-fix` gives a **read-only
74
+ detect loop**. `--count 0` = forever; `--cmd` loops a verbatim command instead of checker (no flags
75
+ added); `--args` appends extra checker flags. Suppresses checker's own per-run banner; prints per-run
76
+ dividers with run #, timestamp, exit code, elapsed. Rationale: continuous unattended drift monitoring +
77
+ auto-correction.
45
78
  - `reconcile_netsuite_totals.php [from] [to] [--verbose]` — category grand totals NS vs Forecast2
46
79
  (Sales, Sales Profit, Open Orders, Opportunities) with deltas. Read-only. Connects explicitly to
47
80
  the **prod core2 reader**; NetSuite via SuiteQL SUMs. `--verbose` additionally prints the
@@ -141,6 +174,18 @@ by reconciling a chosen tranDate range directly against NetSuite.
141
174
  stays bounded under the existing 1G `memory_limit`. (This corrected a prior docblock claim that `$sqls`
142
175
  was never accumulated globally.)
143
176
 
177
+ - **checker → fixer handoff is WINDOW-based, not id-based (decision).** checker hands fixer a tight day
178
+ window and lets fixer **re-find** the discrepant ids inside it, rather than passing exact ids. Rationale:
179
+ maker≠checker independence (fixer re-derives the drift), and checker has already shrunk the window, so a
180
+ re-scan is cheap. Id-passing only wins when drift is **sparse across a wide span** (it would skip fixer
181
+ re-scanning clean gaps). **Deferred (NOT built):** (a) an `--ids` skip-find path so checker passes exact
182
+ ids; (b) a `--since` "recency-pruned fingerprint" mode that uses NS `lastmodifieddate` to choose which
183
+ `trandate` buckets to fingerprint, walking backward until clean. ⚠ A `--since` mode has **structural blind
184
+ spots**: pure NS **deletions** (orphan Forecast rows) and **old untouched drift** are invisible to a
185
+ lastmodified scan, so the full `trandate` fingerprint stays the authoritative backstop. Forecast tables key
186
+ on `tranDate`, not NS `lastmodifieddate`, so a **symmetric** lastmodified fingerprint isn't possible — it
187
+ would require NS-pull-then-id-lookup or the recency-pruned approach.
188
+
144
189
  ## Data model
145
190
 
146
191
  `Forecast.Sales`, `Forecast.OpenOrderItems` on the **core2** cluster
@@ -206,6 +251,24 @@ None — Forecast2 is a single shared dataset.
206
251
  concentrated **Dec 2025–Apr 2026**, driven by NetSuite re-valuing `costestimate` on older
207
252
  invoices that no `lastmodifieddate`-based sync re-pulls. (Revenue reconciles to the penny; only
208
253
  profit drifts.)
254
+ - **Open-orders "round-then-sum" vs "sum-then-round" — the most broadly reusable rounding gotcha.**
255
+ `Forecast.OpenOrderItems.revenue`/`profit` is `decimal(14,2)`: Forecast stores each open-order **LINE's**
256
+ revenue (`qtyOpen × rate`) **rounded to cents on store, then sums** (`SUM(round(line,2))`). A NetSuite-side
257
+ aggregate that does `ROUND(SUM(qtyOpen*rate),2)` (**sum-then-round**) diverges by up to ~½¢ **per line** —
258
+ benign rounding noise that **scales with line count**, so **no fixed aggregate-dollar tolerance can separate
259
+ it from real drift** (a busy bucket's benign band can reach whole dollars). This produced an **unfixable
260
+ phantom**: `fixer.php`'s FIND flags such a penny SO, but its FIX compares **per-line**, finds every line
261
+ already correct to the cent, writes nothing — and the SO churns forever in a loop. **FIX: round each line to
262
+ cents BEFORE summing on the NS side** — `SUM(ROUND(((-tl.quantity) - NVL(tl.quantitybilled,0)) * tl.rate, 2))`
263
+ — so NS matches Forecast's `decimal(14,2)` storage exactly; any remaining diff is then genuinely different
264
+ inputs = real drift. Applied to **both** `checker.php` (open-orders `nsSub` SELECT + HAVING) and `fixer.php`
265
+ `findOpenOrderDiscrepancies`. **Affects ONLY open orders** (a computed `qty×rate` product); Sales/Opportunities
266
+ sum pre-rounded `foreignamount`/`projectedtotal` and were already exact. Verified: full 18-month open-orders
267
+ window went from "drift" to cents-exact in sync after the fix. The previously-reported open-orders deltas were
268
+ entirely this artifact, NOT data drift: the ~$0.04 `reconcile` delta, and the per-SO 1¢ on **SO 6870666**
269
+ (NS 852.06 vs FC 852.07). **Correction to any prior note:** the open-orders **2025-04 "unlocalized month" was
270
+ this round-then-sum noise, NOT NS-live-vs-Forecast-replica timing jitter** — the rounding fix eliminated it,
271
+ disproving the earlier replica-jitter hypothesis.
209
272
  - **Compare money at 2 decimals.** DB columns store 2dp but PHP `revenue - cost` carries float
210
273
  dust (`313.6` vs `313.60000001`); raw `!=` produced thousands of phantom UPDATEs that re-wrote
211
274
  identical values (1,839 on one open-orders run). Both tools now compare `round((float)$x, 2)`.
@@ -297,6 +360,20 @@ None — Forecast2 is a single shared dataset.
297
360
 
298
361
  ## Change history
299
362
 
363
+ - 2026-06-26 — **Added `checker.php` (moment-fingerprint drift detector) + `looper.php` (loop runner), and
364
+ found/fixed the open-orders round-then-sum rounding gotcha.** `checker.php` confirms sync in seconds and
365
+ localizes real drift via a 5-number per-bucket fingerprint (`n,rev,profit,idSum,idSq`; power-sums mod the
366
+ 2³¹−1 prime → engine-portable) drilled top-down month→day→id, descending only into mismatched buckets,
367
+ pulling no rows on a clean book; hands fixer a tight **day window** (window handoff, not id — decision
368
+ recorded). `--fix` requires `--prod`; bare run never writes. `looper.php` defaults to looping
369
+ `checker --no-banner --fix --prod` every 60s (continuous prod auto-correction; `--no-fix` = read-only).
370
+ **Rounding gotcha:** `OpenOrderItems` `decimal(14,2)` stores each line round-then-sum, so a NS-side
371
+ sum-then-round aggregate diverges up to ~½¢/line (an unfixable phantom that loops fixer forever); fixed by
372
+ rounding each NS line to cents before summing (`SUM(ROUND(qtyOpen*rate,2))`) in both `checker.php` and
373
+ `fixer.php` `findOpenOrderDiscrepancies` — open-orders only (Sales/Opps already exact). This also
374
+ **disproves the earlier replica-jitter hypothesis** for the open-orders 2025-04 unlocalized month — it was
375
+ the same per-line rounding. Deferred: `--ids` skip-find and a `--since` recency-pruned fingerprint (blind to
376
+ NS deletions + old untouched drift). (dfranks)
300
377
  - 2026-06-25 — **`trueup_open_orders` Step 4 now lists the exact per-statement SQL** (`<NS SO id> <SQL>`,
301
378
  one line per statement) for both live and `--dry-run` passes, so a dry-run is auditable and any
302
379
  unexpected insert/update/delete is traceable to its source order. Implemented via a global `$sqlLog`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.221",
3
+ "version": "1.0.222",
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",