toga-ai 1.0.425 → 1.0.427
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.
- package/knowledge/1.0/apps/worker/features/forecast2-netsuite-reconciliation.md +44 -1
- package/knowledge/2.0/apps/_underscore/features/forecast-sale-import.md +24 -1
- package/knowledge/2.0/apps/_underscore/features/per-client-database-connections.md +24 -1
- package/knowledge/2.0/apps/api2/INDEX.md +1 -0
- package/knowledge/2.0/apps/api2/features/record-scripts.md +160 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -1
- package/knowledge/2.0/apps/worker2/features/monitoring-framework.md +39 -2
- package/knowledge/2.0/apps/worker2/features/team-sprint-management.md +29 -2
- package/knowledge/INDEX.md +1 -1
- package/knowledge/sessions/2026-07-23-compass-retrofix2-sql-generator-jcardinal.md +69 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-23
|
|
10
10
|
owners: [dfranks, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- test/@dave/checker.php
|
|
@@ -303,6 +303,36 @@ by reconciling a chosen tranDate range directly against NetSuite.
|
|
|
303
303
|
the sale-import doc). The salesRep dimension itself is the JE line custom column `custcol_sales_rep_line`
|
|
304
304
|
(wired under TRUE-79862).
|
|
305
305
|
|
|
306
|
+
- **Delete-safety is a targeted BY-ID RE-FETCH from NetSuite, NOT a `deletedrecord` system-log
|
|
307
|
+
lookup — mirror this in any new reconciler.** The fixer never blind-deletes a Forecast row just
|
|
308
|
+
because a windowed pull didn't return it. Its delete path (`fixer.php` Sales path ~773-891, and
|
|
309
|
+
`checker.php`) works by **re-fetching each discrepant transaction by its internal id** from
|
|
310
|
+
NetSuite via the local `fetchSalesBulk()` shim (`WHERE transaction IN (...)` SuiteQL, same shape
|
|
311
|
+
as the legacy `listSales()`): it upserts the lines NetSuite returns for that id and **deletes only
|
|
312
|
+
the FC lines NetSuite no longer returns for that specific id**. If NS returns **nothing** for the
|
|
313
|
+
id, the txn is gone → its FC rows are deleted. This by-id re-fetch is the concrete implementation
|
|
314
|
+
of the team's **"no blind auto-delete"** rule — it guards against a transient window/read miss
|
|
315
|
+
masquerading as a deletion (a `lastmodifieddate` window or a paged full-window scan can transiently
|
|
316
|
+
omit a row; a per-id GET/IN-list re-confirms it). A NetSuite **`deletedrecord` system-log gate is
|
|
317
|
+
NOT existing behavior** and should not be invented as the delete confirmation — the authoritative
|
|
318
|
+
signal is "the by-id re-fetch returns no lines."
|
|
319
|
+
- **Why an independent tranDate-range reconciliation is MANDATORY, not just a nicety — NetSuite event
|
|
320
|
+
capture cannot be guaranteed by any configuration.** Beyond the sublist line-field inline-edit blind
|
|
321
|
+
spot (below), a broader class of NetSuite changes fires **no** User Event SuiteScript at all, so the
|
|
322
|
+
AMQ enqueuer never runs, no webhook is posted, and `lastmodifieddate` is frequently **not** bumped:
|
|
323
|
+
- **Bulk / mass updates and CSV imports** — a CSV import only runs server SuiteScript (and workflows)
|
|
324
|
+
when **"Run Server SuiteScript and Trigger Workflows"** is enabled, which is **OFF by default**.
|
|
325
|
+
- **Changes made BY another script or workflow** — a UserEvent script **cannot be triggered by
|
|
326
|
+
another UserEvent script or a workflow**, so a server-side edit never re-fires the enqueuer UE.
|
|
327
|
+
|
|
328
|
+
Consequence: such edits drift `Forecast.Sales` (and any webhook-synced NetSuite data) **silently and
|
|
329
|
+
invisibly to BOTH** the real-time webhook path **and** a `lastmodifieddate`-windowed pull cron —
|
|
330
|
+
strictly broader than the already-known inline sublist line-field case. Durable implication: because
|
|
331
|
+
event capture is not guaranteeable, **correctness requires an independent reconciliation that reads
|
|
332
|
+
NetSuite by `tranDate` range and matches on internal id** (exactly what `checker`/`fixer` do) — this
|
|
333
|
+
is the load-bearing reason the reconciliation backstop exists, not an optimization. (Oracle docs:
|
|
334
|
+
server scripting on CSV import `section_4676525683`; how UE scripts are executed `section_1512409310`.)
|
|
335
|
+
|
|
306
336
|
## Data model
|
|
307
337
|
|
|
308
338
|
`Forecast.Sales`, `Forecast.OpenOrderItems` on the **core2** cluster
|
|
@@ -572,6 +602,19 @@ None — Forecast2 is a single shared dataset.
|
|
|
572
602
|
|
|
573
603
|
## Change history
|
|
574
604
|
|
|
605
|
+
- 2026-07-23 — **Recorded WHY the reconciliation backstop is mandatory + the by-id re-fetch
|
|
606
|
+
delete-safety mechanism** (TRUE-80262 planning; no code shipped, dfranks). Documented the broader
|
|
607
|
+
NetSuite event-capture blind spot beyond the inline sublist edit: **bulk/mass updates and CSV
|
|
608
|
+
imports fire no UE** (CSV runs server SuiteScript only when "Run Server SuiteScript and Trigger
|
|
609
|
+
Workflows" is on — OFF by default), and a **UE cannot be triggered by another UE/workflow**, so
|
|
610
|
+
script/workflow-driven edits drift `Forecast.Sales` invisibly to BOTH the webhook path and a
|
|
611
|
+
`lastmodifieddate`-windowed cron (and often don't bump `lastmodifieddate`). Durable implication:
|
|
612
|
+
event capture can't be guaranteed by any NetSuite config, so an independent `tranDate`-range
|
|
613
|
+
reconciliation matched on internal id is required for correctness (Oracle docs `section_4676525683`
|
|
614
|
+
/ `section_1512409310`). Also documented the fixer/checker **delete-safety = targeted by-id re-fetch**
|
|
615
|
+
(`fetchSalesBulk` shim; delete FC lines NS no longer returns for that id) as the concrete "no blind
|
|
616
|
+
auto-delete" implementation — a `deletedrecord` system-log gate is NOT existing behavior and must not
|
|
617
|
+
be invented by a future reconciler. (dfranks)
|
|
575
618
|
- 2026-07-21 — **Documented `reconcile_drift_2023plus.php` + the SOAP-era double-line bug (2023)**
|
|
576
619
|
(folded in from a retired project-local CLAUDE.md). The SOAP-era importer ran twice for some
|
|
577
620
|
invoice batches and inserted each invoice's lines twice under consecutive-but-different line
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-23
|
|
10
10
|
owners: [dfranks]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Component/Forecast/SaleImport/SaleImport.php
|
|
@@ -360,6 +360,21 @@ evidence: two invoices whose ONLY change that day was a cost line-edit had **zer
|
|
|
360
360
|
entries, while 8 other cost-edited invoices that also had normal create/edit activity **did** fire
|
|
361
361
|
and synced. This is recurring/multi-user (~12/day; 35 `MCOSTESTIMATE` edits in 3 days by 9 users).
|
|
362
362
|
|
|
363
|
+
**Broader than the inline line-edit — a whole CLASS of NetSuite changes fires no UserEvent, so no
|
|
364
|
+
webhook is posted (and `lastmodifieddate` is often not bumped):**
|
|
365
|
+
- **Bulk imports / mass updates** and **CSV imports** — a CSV import runs server SuiteScript (and
|
|
366
|
+
workflows) **only** when **"Run Server SuiteScript and Trigger Workflows"** is enabled, which is
|
|
367
|
+
**OFF by default**; so the AMQ enqueuer UE never runs for a bulk/CSV load.
|
|
368
|
+
- **Changes made BY another script or workflow** — a **UserEvent script cannot be triggered by
|
|
369
|
+
another UserEvent script or by a workflow**, so a server-side edit never re-fires the enqueuer.
|
|
370
|
+
|
|
371
|
+
So event capture **cannot be guaranteed by any NetSuite configuration** — these edits drift
|
|
372
|
+
`Forecast.Sales` (and any webhook-synced NetSuite data) **silently and invisibly to BOTH** the
|
|
373
|
+
real-time webhook **and** a `lastmodifieddate`-windowed pull cron. This is the durable reason the
|
|
374
|
+
independent tranDate-range reconciliation (`checker`/`fixer`, see the reconciliation doc) is
|
|
375
|
+
**mandatory, not optional**. (Oracle docs: CSV server scripting `section_4676525683`; how UE scripts
|
|
376
|
+
are executed `section_1512409310`.)
|
|
377
|
+
|
|
363
378
|
**Corollary gotcha: `lastmodifieddate` is NOT a reliable change signal for line-cost edits** — a
|
|
364
379
|
line-field inline edit leaves it untouched, so **both** the webhook pipeline **and** any
|
|
365
380
|
`lastmodifieddate`-windowed `list*()`/cron sync miss it entirely. **Only reconciliation**
|
|
@@ -574,6 +589,14 @@ success from a `Forecast.Sales` row alone.
|
|
|
574
589
|
- The cron's sign handling is not portable here — see Sign convention.
|
|
575
590
|
|
|
576
591
|
## Change history
|
|
592
|
+
- 2026-07-23 — **Broadened the AMQ event-capture blind spot beyond inline line-edits** (TRUE-80262
|
|
593
|
+
planning; no code shipped, dfranks). Recorded that **bulk/mass updates and CSV imports** fire no UE
|
|
594
|
+
(CSV runs server SuiteScript only when "Run Server SuiteScript and Trigger Workflows" is on — OFF by
|
|
595
|
+
default) and that a **UE cannot be triggered by another UE or a workflow** — so script/workflow/bulk
|
|
596
|
+
edits post no webhook and often don't bump `lastmodifieddate`, drifting `Forecast.Sales` invisibly to
|
|
597
|
+
both the webhook path and a `lastmodifieddate`-windowed cron. Durable implication: NetSuite event
|
|
598
|
+
capture is not guaranteeable by any config, making the independent tranDate-range reconciliation
|
|
599
|
+
mandatory (Oracle docs `section_4676525683` / `section_1512409310`). (dfranks)
|
|
577
600
|
- 2026-07-14 — **JE lines now resolve `customerId` (was hard-coded null) + corrected the
|
|
578
601
|
engine's repo location to worker2** (TRUE-80129, dfranks). `buildJournalEntryRows` reads
|
|
579
602
|
`JE_LINE_CUSTOMER_FIELD = 'entity'` — a **native** JE-line reference field (not a `custcol_*`,
|
|
@@ -7,7 +7,7 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-07-23
|
|
10
|
-
owners: ["dfranks", "jcardinal", "mhammontree", "apeterson"]
|
|
10
|
+
owners: ["dfranks", "jcardinal", "mhammontree", "apeterson", "kyalamarthi"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Database.php
|
|
13
13
|
- _underscore/ApiRequest.php
|
|
@@ -75,6 +75,22 @@ per-client), region-aware (1=us-east-1, 2=us-west-2, 3=eu-west-1) with per-regio
|
|
|
75
75
|
registration is region-aware in `api2/Controller/Index.php`; the alias const is in `api2/_.php`.
|
|
76
76
|
`Records.ttlCache` (TINYINT UNSIGNED, default 15) governs cache TTL.
|
|
77
77
|
|
|
78
|
+
### The `Team` schema rides the Core cluster (`DB_TEAM`)
|
|
79
|
+
|
|
80
|
+
The **`Team` schema** (tables `Tasks`, `Sprints`; models `_Model_Team_Task` / `_Model_Team_Sprint`
|
|
81
|
+
with `const DATABASE = _underscore::DB_TEAM`) is **not a first-class api2 database** — it is not a
|
|
82
|
+
value of `Core.Records.aclDatabase` (that enum is only `'CORE'` / `'CLIENT'`). It physically
|
|
83
|
+
resolves to the **core cluster**: `DB_TEAM` **reads** route to `reader1.core.database.togahub.com`
|
|
84
|
+
and **writes** to `writer.core.database.togahub.com`. So a `DB_TEAM` SELECT auto-routes to the core
|
|
85
|
+
reader, and a Record backed by the `Team` schema must be registered with **`aclDatabase = 'CORE'`**
|
|
86
|
+
(see [Record Scripts](../../api2/features/record-scripts.md)).
|
|
87
|
+
|
|
88
|
+
**Where DB hosts come from — the split:** Core and other **non-client** cluster hosts (Core, the
|
|
89
|
+
`Team`-schema-bearing core cluster, etc.) come from the api2 `Config/*.ini` **`[database]`
|
|
90
|
+
section**, not from `DatabaseHosts`. **Per-client** hosts come from the `Core.DatabaseHosts` table,
|
|
91
|
+
resolved by `_Database::registerClientDatabases()`. (Credential/host *values* are not reproduced
|
|
92
|
+
here — they live in `Config/*.ini`.)
|
|
93
|
+
|
|
78
94
|
## Gotchas / known issues
|
|
79
95
|
|
|
80
96
|
- **The "logs DB write trap" (local dev).** A laptop usually imports only `Client_<Id>`, not
|
|
@@ -108,6 +124,13 @@ registration is region-aware in `api2/Controller/Index.php`; the alias const is
|
|
|
108
124
|
|
|
109
125
|
## Change history
|
|
110
126
|
|
|
127
|
+
- 2026-07-23 — Documented that the **`Team` schema** (`Tasks`/`Sprints`, `_Model_Team_*`,
|
|
128
|
+
`DB_TEAM`) is not a first-class api2 DB (not in `Records.aclDatabase`, which is CORE/CLIENT
|
|
129
|
+
only) and physically resolves to the **core cluster** (reads → `reader1.core…`, writes →
|
|
130
|
+
`writer.core…`), so `DB_TEAM` SELECTs auto-route to the core reader and a `Team`-backed Record
|
|
131
|
+
registers as `aclDatabase = 'CORE'`. Also recorded the host-resolution split: Core/non-client
|
|
132
|
+
hosts come from api2 `Config/*.ini` `[database]`; per-client hosts come from `DatabaseHosts`
|
|
133
|
+
via `registerClientDatabases()`. (kyalamarthi)
|
|
111
134
|
- 2026-07-23 — Documented the shared **Core Logs** (`DB_LOGS`) analogue of the logs trap: its schema
|
|
112
135
|
name is resolved from a `Core.Database` row (`id = CORE_LOGS_DATABASE_ID`) in
|
|
113
136
|
`api2/Controller/Index.php::api()`, so a name-mismatched local Logs DB still reads as missing; and
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
| [Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)](features/encrypted-user-uuid-auth-handoff.md) | `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**: given an encrypted `{client, user}` UUID pair, it mints a fr | api2/Component/Api/CrossClient/CrossClient.php |
|
|
8
8
|
| [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text — `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql |
|
|
9
9
|
| [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php |
|
|
10
|
+
| [Record Scripts (computed/aggregate /v2 endpoints — the authoring contract)](features/record-scripts.md) | In api2 you almost never write a controller. | api2/Component/Api/V2/V2.php, _underscore/Model/Team/Sprint.php |
|
|
10
11
|
| [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
|
|
11
12
|
| [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
|
|
12
13
|
| [TableView row-filtering via apiWhereClause (options.where grammar, end to end)](features/tableview-apiwhereclause-row-filtering.md) | `TableViews.apiWhereClause` (TEXT, nullable) is the sanctioned, code-free way to restrict or exclude rows from a 2.0 table view. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TableView.php, toga2-supply/src/api/toga.ts |
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Record Scripts (computed/aggregate /v2 endpoints — the authoring contract)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: api2
|
|
5
|
+
project: API
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-23
|
|
10
|
+
owners: ["kyalamarthi"]
|
|
11
|
+
files:
|
|
12
|
+
- api2/Component/Api/V2/V2.php
|
|
13
|
+
- _underscore/Model/Team/Sprint.php
|
|
14
|
+
related:
|
|
15
|
+
- ./scripted-api-post-body-args.md
|
|
16
|
+
- ./tableview-apiwhereclause-row-filtering.md
|
|
17
|
+
- ../architecture.md
|
|
18
|
+
- ../../_underscore/features/per-client-database-connections.md
|
|
19
|
+
- ../../worker2/features/team-sprint-management.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
In api2 you almost never write a controller. The generic **/v2 engine** serves models from
|
|
25
|
+
metadata (Records / RecordFields / ACL rows), so ordinary CRUD is a *data* change, not code.
|
|
26
|
+
For a payload the metadata CRUD path cannot express — a **computed value, an aggregate, or a
|
|
27
|
+
purpose-built dashboard tile** — the mechanism is a **Record Script**: a `public static`
|
|
28
|
+
method on the record's `_Model` whose return value becomes the `/v2` envelope `data`. This is
|
|
29
|
+
the company-standard way to add a read-only, non-CRUD endpoint.
|
|
30
|
+
|
|
31
|
+
This doc is the **authoring contract** for a Record Script (the general pattern and how to
|
|
32
|
+
register one). For the POST + JSON-request-body variant (large argument values), see
|
|
33
|
+
[POST + JSON-body args for scripted APIs](./scripted-api-post-body-args.md).
|
|
34
|
+
|
|
35
|
+
Motivating example: `_Model_Team_Sprint::committedTile()` — a read-only dashboard tile over
|
|
36
|
+
the internal TOGa IQ sprint (`Team` schema) data. **Decision recorded:** internal
|
|
37
|
+
sprint/dashboard read endpoints are served from **api2 via a Record Script**, not by building
|
|
38
|
+
a bespoke controller or standing the data up inside the talos / TOGa IQ app.
|
|
39
|
+
|
|
40
|
+
## The method contract
|
|
41
|
+
|
|
42
|
+
A Record Script is a static method on the record's model — for the example above, the
|
|
43
|
+
signature is `public static function committedTile(&$api, int|string $sprint): object` on
|
|
44
|
+
`_underscore/Model/Team/Sprint.php`. Its return value becomes the `/v2` envelope `data`.
|
|
45
|
+
|
|
46
|
+
Rules the engine enforces (see `getRecordScriptPhpMethod()` + the dispatch in
|
|
47
|
+
`processRoutePairs()` inside `api2/Component/Api/V2/V2.php`):
|
|
48
|
+
|
|
49
|
+
- **First parameter is `&$api`** — the engine always sets `$args['api'] = $this` last, so the
|
|
50
|
+
method receives the live V2 engine instance (for auth context, `internalApiRequest()`, etc.).
|
|
51
|
+
- **Remaining parameters are named** and are filled from the **query string**. Each query-string
|
|
52
|
+
key maps to the same-named PHP parameter; unmatched named args land in a `...$args` variadic
|
|
53
|
+
if the method declares one. (`transactionId` and `api` keys are reserved and never mapped.)
|
|
54
|
+
- **The return value is the envelope `data`.** Return an object/array; do not echo, and do not
|
|
55
|
+
return the bare success/error envelope — the engine wraps it.
|
|
56
|
+
- Invocation is effectively `$model::$phpMethod(...$args)`.
|
|
57
|
+
|
|
58
|
+
## Running SQL inside a Record Script
|
|
59
|
+
|
|
60
|
+
Record Scripts commonly run their own query (via `new \_Query($sql, \_underscore::DB_TEAM)`
|
|
61
|
+
then `->fetchRow()`) rather than going through model CRUD. Two framework realities to respect:
|
|
62
|
+
|
|
63
|
+
- **`_Query` has no bound-parameter support.** There is no `?`/`:name` binding. Every value
|
|
64
|
+
you place into the query text must be sanitized by hand **before** it is assembled into the
|
|
65
|
+
SQL: pass strings through `\_Database::escape()` and cast numerics with `(int)`. Record-script
|
|
66
|
+
arguments arrive straight from the query string (user input), so this sanitization is
|
|
67
|
+
mandatory — an unsanitized value in the query text is a SQL-injection hole. Treat
|
|
68
|
+
`\_Database::escape()` + `(int)` casting as the required substitute for the prepared
|
|
69
|
+
statements `_Query` does not offer.
|
|
70
|
+
- **Query cache for "live" tiles.** A dashboard tile that must reflect current data should call
|
|
71
|
+
`\_Database::useQueryCache(false)` before the query and **restore the previous flag**
|
|
72
|
+
afterward (capture the value it returns and set it back), so it doesn't serve stale cached
|
|
73
|
+
rows and doesn't leave caching globally disabled for the rest of the request.
|
|
74
|
+
- `DB_TEAM` SELECTs auto-route to the **core cluster reader** — see
|
|
75
|
+
[per-client database connections](../../_underscore/features/per-client-database-connections.md)
|
|
76
|
+
for why the `Team` schema resolves to core.
|
|
77
|
+
|
|
78
|
+
## Registering / enabling an endpoint (metadata across two databases)
|
|
79
|
+
|
|
80
|
+
Registering a Record Script is a **metadata change split across two databases plus a
|
|
81
|
+
per-client ACL grant** — it is not a code deploy. The route is
|
|
82
|
+
**`GET /v2/<recordRoute>/<scriptRoute>`**.
|
|
83
|
+
|
|
84
|
+
**Data model:**
|
|
85
|
+
|
|
86
|
+
- **`Core.Records`** (the route ↔ model row). Its **`aclDatabase` enum is only `'CORE'` or
|
|
87
|
+
`'CLIENT'`** — there is no `TEAM` value, so a `Team`-schema-backed record registers as
|
|
88
|
+
**`aclDatabase = 'CORE'`** (the `Team` schema physically lives on the core cluster).
|
|
89
|
+
- **`Core.RecordScripts`** — `uuid`, `recordId` (FK → `Core.Records`), `method` (e.g. `GET`),
|
|
90
|
+
`route` (the script route), `phpMethod` (the static method name). **`UNIQUE(recordId, method,
|
|
91
|
+
route)`.**
|
|
92
|
+
- **`Client.AclRecordScripts`** — model `_Model_Client_AclRecordScript`, on `DB_CLIENT`:
|
|
93
|
+
`recordScriptId` (FK → `Core.RecordScripts`) + `roleId` (FK → `Client.Roles`). This is the
|
|
94
|
+
ACL grant, and it lives **per client** — so **record-script authorization is per-client**:
|
|
95
|
+
granting a role in one client does not grant it in another.
|
|
96
|
+
|
|
97
|
+
At dispatch the engine calls `getRecordScriptPhpMethod(record, httpMethod, route, roleIds)`,
|
|
98
|
+
which joins `Core.RecordScripts` to the caller's `Client.AclRecordScripts` rows for their
|
|
99
|
+
roles; no matching grant → the script does not run for that caller.
|
|
100
|
+
|
|
101
|
+
**Where the rows go:** seed them in **dbchanges2** as dated migration files — the
|
|
102
|
+
`Core.Records`/`Core.RecordScripts` rows under the `Core` folder, and the `AclRecordScripts`
|
|
103
|
+
grant under the per-client `Client_<Name>` folder. See the
|
|
104
|
+
[api2 architecture — metadata CRUD engine](../architecture.md) for the surrounding Records
|
|
105
|
+
metadata model.
|
|
106
|
+
|
|
107
|
+
## Data source note (core DB hosts/credentials)
|
|
108
|
+
|
|
109
|
+
The core cluster hosts and credentials this endpoint reads through are **not** documented here;
|
|
110
|
+
they live in **api2 `Config/*.ini`** (`[database]` section). Do not paste those values into
|
|
111
|
+
knowledge or code.
|
|
112
|
+
|
|
113
|
+
## Client variations
|
|
114
|
+
|
|
115
|
+
None — this is engine behavior. Per-client access is entirely controlled by which roles have
|
|
116
|
+
an `AclRecordScripts` grant in each client DB.
|
|
117
|
+
|
|
118
|
+
## Gotchas / known issues
|
|
119
|
+
|
|
120
|
+
- **No bound params in `_Query`.** Sanitize with `\_Database::escape()` (strings) and `(int)`
|
|
121
|
+
casts (numerics) for every value placed into the query text — query-string args are user
|
|
122
|
+
input.
|
|
123
|
+
- **Restore the query-cache flag.** `useQueryCache(false)` is a global toggle; capture the
|
|
124
|
+
prior value and restore it, or you disable caching for the rest of the request.
|
|
125
|
+
- **`aclDatabase` has no `TEAM`.** A `Team`-schema record registers as `'CORE'`; picking
|
|
126
|
+
`'CLIENT'` sends the engine looking in the wrong cluster.
|
|
127
|
+
- **Registration is per-environment and split.** A missing `Core.RecordScripts` row → the route
|
|
128
|
+
isn't a script (falls through to normal CRUD / 404-shaped behavior); a missing
|
|
129
|
+
`AclRecordScripts` grant → the caller's roles can't run it. Both rows must exist in every
|
|
130
|
+
environment the API serves.
|
|
131
|
+
- **The committed-tile definition is intentionally not the sprint-scoring definition.**
|
|
132
|
+
`committedTile()` uses the **Power BI / current-state** definition of done/committed
|
|
133
|
+
(`dtDone IS NOT NULL` + `workTypeNow`), which is *deliberately different* from TOGa IQ's
|
|
134
|
+
canonical sprint scoring (`statusNow IN (STATUS_IN__DONE)` + `workTypeAtLock`). See
|
|
135
|
+
[Team Sprint Management](../../worker2/features/team-sprint-management.md) before assuming a
|
|
136
|
+
tile agrees with sprint scores.
|
|
137
|
+
|
|
138
|
+
## Change history
|
|
139
|
+
|
|
140
|
+
- 2026-07-23 — Initial documentation of the Record Script authoring contract (static
|
|
141
|
+
`fn(&$api, ...namedParams)` on the record model; query-string → named args; return → `/v2`
|
|
142
|
+
envelope `data`), the SQL specifics (`_Query` has no bound params → `_Database::escape()` +
|
|
143
|
+
int-cast; `useQueryCache(false)` + restore for live tiles), and the registration metadata
|
|
144
|
+
(`Core.Records` `aclDatabase` CORE/CLIENT only, `Core.RecordScripts` UNIQUE(recordId,method,
|
|
145
|
+
route), per-client `Client.AclRecordScripts` grant). Motivated by
|
|
146
|
+
`_Model_Team_Sprint::committedTile()`, a read-only internal sprint dashboard tile — recording
|
|
147
|
+
the decision that internal sprint/dashboard reads are served from api2 via a Record Script
|
|
148
|
+
rather than the talos / TOGa IQ app. (kyalamarthi)
|
|
149
|
+
|
|
150
|
+
## Related docs
|
|
151
|
+
|
|
152
|
+
- [POST + JSON-body args for scripted APIs](./scripted-api-post-body-args.md) — the POST /
|
|
153
|
+
JSON-request-body variant for large argument values.
|
|
154
|
+
- [api2 architecture](../architecture.md) — the metadata-driven /v2 CRUD engine that Record
|
|
155
|
+
Scripts extend.
|
|
156
|
+
- [Per-Client Database Connections](../../_underscore/features/per-client-database-connections.md)
|
|
157
|
+
— why `DB_TEAM` resolves to the core cluster.
|
|
158
|
+
- [Team Sprint Management & Reporting](../../worker2/features/team-sprint-management.md) — the
|
|
159
|
+
canonical sprint scoring definition the committed tile deliberately diverges from.
|
|
160
|
+
</content>
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
| [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 |
|
|
29
29
|
| [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 |
|
|
30
30
|
| [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | `_Worker_Team_Transcripts` runs a fully automated, cron-driven pipeline that ingests raw Teams transcripts into the **Talos / TOGa IQ** AWS Bedrock knowledge ba | worker2/Worker/Team/Transcripts.php, worker2/bin/sync-knowledge-bases.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, 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, dbchanges2/Team/2026-07-08a, dbchanges2/Team/2026-07-09a, dbchanges2/Team/2026-07-10a |
|
|
31
|
-
| [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, dbchanges2/Core/CronJobs (SprintLockScheduled seed) |
|
|
31
|
+
| [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, _underscore/Model/Team/Sprint.php, dbchanges2/Core/CronJobs (SprintLockScheduled seed) |
|
|
32
32
|
| [Teams Meeting Transcript Export](features/teams-transcript-export.md) | > **SUPERSEDED (2026-07-09) — the S3-staging model below is history.** `Export` is now a thin > **GRAPH-DIRECT** cron poller: it no longer archives raw VTT to ` | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
|
|
33
33
|
| [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 |
|
|
34
34
|
| [WJE Freshservice Sync (worker2)](features/wje-freshservice-sync.md) | WJE ("WJE IT", helpdesk `wje.freshservice.com`) is a **Freshservice**-based help-desk client whose tickets, contacts, assets, groups, categories, and canned res | worker2/Worker/Wje.php, _underscore/Component/Api/Wje/Wje.php, _underscore/Model/Wje/Ticket.php, _underscore/Model/Wje/TicketNote.php, _underscore/Model/Wje/Contact.php, _underscore/Model/Wje/Unit.php, _underscore/Model/Wje/TicketTeam.php, _underscore/Model/Wje/TicketCategory.php, _underscore/Model/Wje/AssetType.php, _underscore/Model/Wje/PredefinedReply.php, library/app/api/wje.php, worker/crons/toga2/wje/import_supporting_records.php, worker/crons/toga2/wje/sync_togasupply_wje.php, worker/crons/notifications/reports/wje/wje_common.php, library/app/systemmonitor/wje.php, dbchanges2/Client_Wje/2024-10-04 - WjeOnboarding.sql |
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [mhammontree]
|
|
9
|
+
updated: 2026-07-23
|
|
10
|
+
owners: [mhammontree, dfranks]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Monitor.php
|
|
13
13
|
- worker2/Worker/Monitors/
|
|
@@ -54,6 +54,35 @@ orchestrator) · runtime config in the `Core.Monitors` table (no redeploy).
|
|
|
54
54
|
Reused with **no changes**: `Core.CronJobs`, `Core.WorkerJobs`, the `WorkerCronScheduler`
|
|
55
55
|
Lambda, the EB worker tier + SQS delivery (see [worker2 architecture](../architecture.md)).
|
|
56
56
|
|
|
57
|
+
## Where monitor / data-quality results belong (design placement)
|
|
58
|
+
|
|
59
|
+
worker2 has **two** monitoring patterns and neither writes to the central **`Logs` Issues/Events**
|
|
60
|
+
tables. That is deliberate — **`Logs.Issues`/`Logs.Events` are strictly for application
|
|
61
|
+
errors/exceptions** that escalate to ClickUp or email (business-routed by `IssueEmailAddresses`
|
|
62
|
+
presence; see the escalation-cron work). **Periodic health-check, integration-health, and
|
|
63
|
+
data-quality RESULTS do NOT go there** — they belong in the Monitor framework:
|
|
64
|
+
|
|
65
|
+
- **Pattern A — internal DB-driven state machine** (this doc): a `Core.Monitors` row +
|
|
66
|
+
`_Worker_Monitor::Run(monitorId)` (`worker2/Worker/Monitor.php`) invokes a child `phpClass::Run()`
|
|
67
|
+
that returns `{isOk, message}`; the orchestrator manages ok/alert state, `consecutiveOkCount` /
|
|
68
|
+
`requiredConsecutiveOks` anti-flap, and sends alert/reminder/recovery mail via
|
|
69
|
+
`_Worker_Notification_Email::Send`, persisting state back to `Monitors`.
|
|
70
|
+
- **Pattern B — "dumb reporter, smart monitor"** (see
|
|
71
|
+
[OneUptime push-metric monitors](./oneuptime-worker2-monitoring.md)): a child measures one metric
|
|
72
|
+
and POSTs a JSON body to a OneUptime incoming-request (heartbeat) monitor; the **threshold/alerting
|
|
73
|
+
lives in OneUptime** (tunable without a deploy). This is the right home for integration-health /
|
|
74
|
+
data-quality checks.
|
|
75
|
+
|
|
76
|
+
So when deciding where a recurring check/reconciliation **result** goes, the answer is the Monitor
|
|
77
|
+
framework (Pattern B / OneUptime for integration-health and data-quality), **not** Issues/Events.
|
|
78
|
+
Key Pattern-B idioms (durable): the worker decides the threshold and emits a **string token**
|
|
79
|
+
OneUptime matches (`"alarm":"HIGH"` → offline/incident, because OneUptime can only string-match a
|
|
80
|
+
pushed body, not compare numbers); on a data-source failure POST an explicit **`status:error`** so
|
|
81
|
+
OneUptime distinguishes "metric high" from "checker is blind"; the `_ApiRequest` push uses
|
|
82
|
+
`setLogging(false)` + `setThrowExceptionsOnFailure(false)` so a failed ping never fails the job or
|
|
83
|
+
depends on the Logs DB; the heartbeat URL is a **push credential — never log it**; and the
|
|
84
|
+
human-readable result is the return string persisted in `WorkerJobs.output`.
|
|
85
|
+
|
|
57
86
|
## How it works
|
|
58
87
|
|
|
59
88
|
One `Core.CronJobs` row per monitor (`action = 'Monitor/Run'`, `parameters =
|
|
@@ -237,6 +266,14 @@ clients' data flows (Compass, Prudential, AIG, Rate, …) but live as separate c
|
|
|
237
266
|
HTML email + dashboard deep-links · anti-flap on the alarm side.
|
|
238
267
|
|
|
239
268
|
## Change history
|
|
269
|
+
- 2026-07-23 — **Recorded the design-placement decision: monitor / data-quality RESULTS belong in the
|
|
270
|
+
Monitor framework (Pattern B / OneUptime for integration-health & data-quality), NOT in the central
|
|
271
|
+
`Logs` Issues/Events tables** (which are strictly for escalating application errors/exceptions).
|
|
272
|
+
Summarized both patterns side by side and the durable Pattern-B idioms (worker decides the threshold
|
|
273
|
+
and emits a string token OneUptime matches; explicit `status:error` on data-source failure; the push
|
|
274
|
+
is `setLogging(false)`+`setThrowExceptionsOnFailure(false)` so a failed ping never fails the job; the
|
|
275
|
+
heartbeat URL is a push credential — never logged; result string persisted in `WorkerJobs.output`).
|
|
276
|
+
TRUE-80262 planning; no code shipped. (dfranks)
|
|
240
277
|
- 2026-06-29 — Built the first child monitor, `_Worker_Monitors_RateEntitlement` (TRUE-79129) + change-set `2026-06-29a`; added the log-scan worked example, the `_underscore` outbound-API-log detection pattern, and confirmed child details (orchestrator never calls `initialize()` so children self-register non-Core connections; no commit on read-only `Run()`; heredoc cannot interpolate `self::CONST`). Re-flagged that `Core.Monitors` is still local-only — the new change-set hard-fails where the table is absent. (mhammontree)
|
|
241
278
|
- 2026-06-10 — Documented the v1.0 monitoring framework (orchestrator, `Core.Monitors` table, recovery-side anti-flap state machine, child contract). First child monitor + staging/prod migration still pending. (mhammontree)
|
|
242
279
|
|
|
@@ -6,10 +6,11 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-07-23
|
|
10
|
+
owners: ["jcardinal", "kyalamarthi"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Team/Sprint.php
|
|
13
|
+
- _underscore/Model/Team/Sprint.php
|
|
13
14
|
- dbchanges2/Core/CronJobs (SprintLockScheduled seed)
|
|
14
15
|
related:
|
|
15
16
|
- ../architecture.md
|
|
@@ -228,8 +229,34 @@ backfills `workTypeAtLock` the first time a task is seen).
|
|
|
228
229
|
Do **not** overload one map for both jobs. This pattern is replicated identically in
|
|
229
230
|
`SprintLock()`, `CaptureSprintEnd()`, and `CaptureSprintDaily()` — fix all three together.
|
|
230
231
|
|
|
232
|
+
## Two coexisting "committed / done" definitions (reconcile before comparing)
|
|
233
|
+
|
|
234
|
+
There are **two intentionally-different definitions** of committed/done for a sprint task, and
|
|
235
|
+
they do not agree:
|
|
236
|
+
|
|
237
|
+
1. **Canonical sprint-scoring definition** — used by the scoring model above (and by the
|
|
238
|
+
reusable scoring helpers `_pointsByWorkType` / `_tasksByWorkType` on **`_Model_Team_Sprint`**
|
|
239
|
+
in `_underscore`, `Model/Team/Sprint.php`): a task is *done* when
|
|
240
|
+
`statusNow IN (STATUS_IN__DONE)` and its category is taken from **`workTypeAtLock`** (the
|
|
241
|
+
frozen at-lock baseline). This is what reliability/integrity scoring measures against.
|
|
242
|
+
2. **Power BI / current-state definition** — used by dashboard tiles that must match the Power BI
|
|
243
|
+
numbers: *done* is **`dtDone IS NOT NULL`** and category is **`workTypeNow`** (current state,
|
|
244
|
+
not the at-lock snapshot). The api2 Record Script
|
|
245
|
+
[`_Model_Team_Sprint::committedTile()`](../../api2/features/record-scripts.md) deliberately
|
|
246
|
+
uses this definition.
|
|
247
|
+
|
|
248
|
+
Neither is "wrong" — they answer different questions (scored performance vs. live current state).
|
|
249
|
+
If a tile or report must **agree with TOGa IQ's canonical scoring**, reconcile it to definition
|
|
250
|
+
(1); otherwise expect current-state tiles to diverge from sprint scores.
|
|
251
|
+
|
|
231
252
|
## Change history
|
|
232
253
|
|
|
254
|
+
- 2026-07-23 — Recorded that two intentionally-different "committed/done" definitions coexist:
|
|
255
|
+
the canonical sprint-scoring definition (`statusNow IN (STATUS_IN__DONE)` + `workTypeAtLock`,
|
|
256
|
+
incl. the reusable `_pointsByWorkType`/`_tasksByWorkType` helpers on `_Model_Team_Sprint` in
|
|
257
|
+
`_underscore`) vs. the Power BI / current-state definition (`dtDone IS NOT NULL` +
|
|
258
|
+
`workTypeNow`) used by dashboard tiles such as the api2 `committedTile()` Record Script.
|
|
259
|
+
Added `_underscore/Model/Team/Sprint.php` to this doc's files. (kyalamarthi)
|
|
233
260
|
- 2026-07-21 — Added `SprintLockScheduled()` action + a `Core.CronJobs` seed
|
|
234
261
|
(`0 10 * * 3`, every Wednesday 10 AM Central, action `Team/Sprint/SprintLockScheduled`,
|
|
235
262
|
maxExecutionTime 600) to run **Sprint Lock automatically at the start of each biweekly
|
package/knowledge/INDEX.md
CHANGED
|
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
19
19
|
|
|
20
20
|
- **_underscore** (_Underscore) _(framework core)_ — 37 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 31 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
|
-
- **api2** (API) —
|
|
22
|
+
- **api2** (API) — 14 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
24
24
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
25
25
|
- **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: session
|
|
3
|
+
slug: compass-retrofix2-sql-generator
|
|
4
|
+
title: Compass retroactive data-fix v2 — SQL generator rewrite
|
|
5
|
+
author: jcardinal
|
|
6
|
+
repos: [test, worker, library, _underscore]
|
|
7
|
+
framework: "2.0"
|
|
8
|
+
client: compass-usa
|
|
9
|
+
status: active
|
|
10
|
+
created: 2026-07-23
|
|
11
|
+
updated: 2026-07-23
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Session: compass-retrofix2-sql-generator
|
|
15
|
+
**Date:** 2026-07-23
|
|
16
|
+
**Project/Repo:** test/@jeff/compass (script), operating on Client_Compass (2.0)
|
|
17
|
+
**Task:** Rewrite the Compass USA retroactive data-fix as a NEW standalone script that GENERATES SQL to a flat file (never writes the DB), fixing SO/PO item existence + the two item-link bridges bottom-up, then recursing all ItemFulfillments up to the Compass SO — replacing the old direct-write `retrofix.php`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What WORKED
|
|
22
|
+
- **New script built & lint-clean:** `D:\WWW\test\@jeff\compass\retrofix2.php` (`php -l` passes). Standalone CLI: `require_once('_.php'); App_Framework_Sandbox::initialize();`, read-only DB alias `db_prod2_compass`, writes SQL to `retrofix2.phase1.sql` / `retrofix2.phase2.sql`.
|
|
23
|
+
- **Two-pass PHASE model** (`const PHASE = 1|2`): Phase 1 generates item-link SQL → developer APPLIES it → re-run with PHASE=2 (now reads corrected bridges) → generates IF-recursion SQL. Chosen because Phase 2's up-mapping walks the item bridges Phase 1 fixes, and generate-only SQL can't see un-applied fixes.
|
|
24
|
+
- **Chain resolution** (`getChainForSo`): developer's LEFT-JOIN header query, deduped into distinct id sets per level (compassPoIds/odpSoIds/odpPoIds/agilantSoIds + odpPo→odpSo, compassPo→odpSo pairings). Validated against prod: a Compass SO can have MULTIPLE Compass POs; recent orders legitimately have partial chains.
|
|
25
|
+
- **Phase 1 L1 group logic** mirrors `library/app/api/toga2.php:778-897,1216-1352` in id-space: re-pulls Agilant SO from NetSuite (`App_NetSuite::getSalesOrder` by `c_netsuiteInternalSalesOrderId`), builds `memberItemId=>[groupItemId]` via `getItemDetails(...,'itemGroup',false)` on group-header lines (line has neither `rate` nor `amount`), matches Agilant SOI→ODP POI by (item OR group-item, quantity) non-consuming (1 POI → many SOIs). Memoizes getItemDetails per NS internal id.
|
|
26
|
+
- **Phase 2** ports `_underscore/Model/Client/ItemFulfillment.php::reconcileUpstreamLevel` as SQL: header walk, item walk grouped-by-upstream-SOI with bundle scaling `min(rawSum, rawSum*upQtyOrdered/downOrderedSum)`, broken-bridge guard, create/link upstream IF+IFI, propagate units + 3 tracking bridges (shared unitId/trackingNumberId), carries planned rows forward in-memory across the Agilant→ODP→Compass climb. New rows use generated uuid + `(SELECT id … WHERE uuid=…)` refs.
|
|
27
|
+
- **emit() guard** hard-blocks any write to `TrackingNumbers` or `Units` (only exact-table match; bridge tables like `ItemFulfillmentItemUnits_TrackingNumbers` correctly allowed).
|
|
28
|
+
- **Bug fixes this session (all lint-verified):**
|
|
29
|
+
- L4 false inserts — root cause: existing-links query used `WHERE b.purchaseOrderItemId IN (<Compass PO HEADER ids>)`; fixed to join `PurchaseOrderItems … purchaseOrderId IN (compassPoIds)`. Confirmed against SA133672 (SO 108392) which no longer emits its two spurious inserts.
|
|
30
|
+
- Phase 2 `ifIdExpr` "Undefined array key realId" — IFI handles use `ifiRealId`/`ifiUuid`, IF handles use `realId`/`uuid`; helper now accepts both.
|
|
31
|
+
- MySQL 1093 (self-referential UPDATE) — `ifIdExpr` now wraps uuid lookups in a derived table `(SELECT id FROM (SELECT id FROM t WHERE uuid=…) AS _rf2_<hash>)`; inline upstream-IF-link expr routed through it too.
|
|
32
|
+
- MySQL 1451 (FK on stale deletes) — deletes now strictly bottom-up: IFIU_TrackingNumbers → IFIU → ItemFulfillmentItems_TrackingNumbers → ItemFulfillmentItems (and unit-tracking → unit for leftover units).
|
|
33
|
+
- Order header now prints date: `# SA123456 (2026-06-15)` (worklist selects dateOrder) for resume tracking.
|
|
34
|
+
|
|
35
|
+
## What did NOT work — DO NOT RETRY THESE
|
|
36
|
+
- **Relying on the plain `try/catch(Throwable)` around NetSuite calls to catch the SSL drop** ("SoapClient::__doRequest(): SSL: An existing connection was forcibly closed by the remote host"). The warning is intercepted by TOGA's GLOBAL `App_Error::handleError → handleException`, which (Sentry active) reports and can terminate the instance / not re-throw cleanly — so the retry loop never got a reliable shot. FIX APPLIED: `nsCall` now installs a LOCAL `set_error_handler` (throws ErrorException) around each attempt and `restore_error_handler()` after — so the warning is caught locally and retried. Do not go back to relying on the global handler.
|
|
37
|
+
- **Old `retrofix.php` itemId-only "purity" test** for item-group links — would delete legitimate group links (1 ODP PO item ↔ many Agilant SO items whose itemIds differ). Superseded by the NetSuite group-map approach. Do not reintroduce pure itemId matching at L1.
|
|
38
|
+
|
|
39
|
+
## Not tried yet (candidates for next session)
|
|
40
|
+
- **End-to-end verification not yet done:** the idempotency re-run test (apply Phase 1 → re-run → expect empty) and the engine-parity check for a Flow-B order have NOT been run.
|
|
41
|
+
- **L4 Compass-SO↔Compass-PO bundle links** are intentionally NOT auto-created — bundle/config lines (`c_isConfiguration`/`parentSalesOrderItemId`/`bundleId`) are only logged as SKIP. Needs the bundle-split logic from `worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php` before enabling.
|
|
42
|
+
- Confirm the fresh NetSuite retry fix (local error handler) actually survives a real SSL drop in a full batch run.
|
|
43
|
+
- Phase 2 possible over-emission: already-linked IFIs may get redundant (harmless) upstream-link UPDATEs across multi-level climbs — validate on the parity test.
|
|
44
|
+
- IF `number` generation (`F#####` via MAX+1) collision risk vs concurrent prod IF creation — apply Phase 2 SQL promptly.
|
|
45
|
+
|
|
46
|
+
## Current file state
|
|
47
|
+
| File | Status | Notes |
|
|
48
|
+
|------|--------|-------|
|
|
49
|
+
| D:\WWW\test\@jeff\compass\retrofix2.php | Created, lint-clean, working | Full 2-phase generator; all reported bugs fixed; NetSuite local-error-handler retry (5× backoff 3/6/9/12s) |
|
|
50
|
+
| D:\WWW\test\@jeff\compass\retrofix2.phase1.sql | Generated (test window) | Output artifact; L4 spurious-insert bug fixed since last gen — regenerate |
|
|
51
|
+
| D:\WWW\test\@jeff\compass\retrofix2.phase2.sql | Generated (test window) | Output artifact; 1093/1451 fixed since last gen — regenerate |
|
|
52
|
+
| C:\Users\JCardinal\.claude\plans\i-need-to-continue-declarative-kahan.md | Created | Approved implementation plan (context, phases, open items, verification) |
|
|
53
|
+
| D:\WWW\test\@jeff\compass\retrofix.php | Unchanged | Old direct-write version, kept for reference |
|
|
54
|
+
|
|
55
|
+
## Decisions made
|
|
56
|
+
- **Generate SQL, never write the DB** (developer applies manually) — user requirement; SELECTs + NetSuite GETs only.
|
|
57
|
+
- **Two-pass PHASE constant** over single-pass in-memory bridge model — simpler/safer, matches rollout order (verify links first, then fulfillments). Rejected: single-pass modelling expected bridges in memory (too complex/error-prone to verify).
|
|
58
|
+
- **NetSuite group resolution via live `getItemDetails`** (per user direction) rather than any persisted TOGA bundle table — re-pulls each Agilant SO from NetSuite.
|
|
59
|
+
- **Flat SQL file, no transactions, `# SA###### (date)` comments** per user preference.
|
|
60
|
+
- **L4 conservative** (insert pure links, log suspected bundles, delete only exact dups) — avoids destroying legit bundle links pending the script-1 bundle logic.
|
|
61
|
+
|
|
62
|
+
## Blockers
|
|
63
|
+
None blocking. Open dependency: Phase 2 requires the Phase 1 SQL to be APPLIED to the target DB first (two-pass), and full E2E verification is still pending.
|
|
64
|
+
|
|
65
|
+
## Exact next step
|
|
66
|
+
> Regenerate Phase 1 over a small test window (set `START_DATE`/`END_DATE` to ~1 day, `PHASE=1`) in `D:\WWW\test\@jeff\compass\retrofix2.php`, run it (live NetSuite + prod-read), then run the idempotency check: apply `retrofix2.phase1.sql` to a LOCAL Client_Compass copy, re-run PHASE=1, and confirm the second output is empty. Then repeat for PHASE=2.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
_Saved by /session-save on 2026-07-23_
|
package/package.json
CHANGED