toga-ai 1.0.272 → 1.0.274
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/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/forecast-sale-import.md +31 -7
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/automated-pr-merger-force-push-race.md +71 -0
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
10
10
|
| [Error Reporting — Issue/Event Aggregation (exceptionHandler)](features/error-reporting-issue-event.md) | `_underscore`'s global exception handler persists every uncaught exception into a two-table **Issue / Event** model in the **shared Core Logs DB** (`_underscore | _underscore/Error.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, dbchanges2/Logs/2026-07-06 - Issue and Event tables.sql |
|
|
11
11
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|
|
12
|
-
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | _underscore/Component/Forecast/SaleImport/SaleImport.php, _underscore/Component/Forecast/Db/Db.php, _underscore/Component/Api/Netsuite/Netsuite.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/test_fetchrecord_routes.php, test/@dave/verify_je_classification.php, test/@dave/probe_je_accounts.php, test/@dave/probe_je_shape.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
12
|
+
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | _underscore/Component/Forecast/SaleImport/SaleImport.php, _underscore/Component/Forecast/Db/Db.php, _underscore/Component/Api/Netsuite/Netsuite.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/test_fetchrecord_routes.php, test/@dave/verify_je_classification.php, test/@dave/probe_je_accounts.php, test/@dave/probe_je_shape.php, test/@dave/fixer.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
13
13
|
| [Item-Fulfillment Stage Lifecycle (picked/packed/shipped) & Order Status](features/item-fulfillment-stage-lifecycle-and-order-status.md) | Every ItemFulfillment (IF) now carries an explicit **stage** — picked → packed → shipped — resolved through `ItemFulfillmentStages → ItemFulfillmentStatuses` (m | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderStatus.php, _underscore/Model/Client/SalesOrderItem.php, _underscore/Model/Client/Item.php, _underscore/Model/Client/PurchaseOrderItem.php, library/app/api/toga2.php, dbchanges2/Client/2026-06-30a - BackfillNullStageItemFulfillmentsToShipped.sql, dbchanges2/Client/2026-06-30b - SalesOrderStatusesPickedPacked.sql, dbchanges2/Client/2026-06-30c - ItemFulfillmentStageIdNotNull.sql, dbchanges2/Client_CompassCanada/2026-06-30a - ItemFulfillmentLifecycleAndShippedBackfill.sql |
|
|
14
14
|
| [_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 |
|
|
15
15
|
| [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 |
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-07
|
|
10
10
|
owners: [dfranks]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/Forecast/SaleImport/SaleImport.php
|
|
@@ -29,6 +29,7 @@ files:
|
|
|
29
29
|
- test/@dave/verify_je_classification.php
|
|
30
30
|
- test/@dave/probe_je_accounts.php
|
|
31
31
|
- test/@dave/probe_je_shape.php
|
|
32
|
+
- test/@dave/fixer.php
|
|
32
33
|
- test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js
|
|
33
34
|
- test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js
|
|
34
35
|
related:
|
|
@@ -184,12 +185,21 @@ item-line upsert/reconcile machinery (`syncLines`/`guardedInsert`/`deleteRows`).
|
|
|
184
185
|
per-run **account-number allowlist isolates exactly the lines that carry the rep** — offset/
|
|
185
186
|
balancing lines omit class/entity/`custcol_sales_rep_line`, so they group to null and are
|
|
186
187
|
dropped. No schema/migration (`salesRepEmployeeId` already on `Forecast.Sales`).
|
|
187
|
-
- **JE lines carry
|
|
188
|
-
|
|
189
|
-
`
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
- **JE lines carry their item in a CUSTOM column `custcol_item_sku` — NOT the native item
|
|
189
|
+
field (corrected 2026-07-07, TRUE-79973).** The **native** `transactionline.item` /
|
|
190
|
+
REST `line.item` is **empty on every JE rev/cost line** (live probe 2026-07-07: empty on
|
|
191
|
+
all 12,293 2026 JE revenue/cost lines) — which is why earlier probing concluded "no item
|
|
192
|
+
column exists." The SKU actually lives **line-level** on each rev/cost GL line in
|
|
193
|
+
**`custcol_item_sku`**, shaped **identically to `custcol_sales_rep_line`**: a NetSuite
|
|
194
|
+
**reference object** `line.custcol_item_sku = {id, refName, links}` where `.id` (a JSON
|
|
195
|
+
string) is the item **internalId** (e.g. `"1103457"`). It maps to Forecast via
|
|
196
|
+
`lookupId('Items','netsuiteItemInternalId', .id)`. In SuiteQL it is `tl.custcol_item_sku`
|
|
197
|
+
returning the raw internal id — **filter/group with `> 0`, never `IS NOT NULL`** (same
|
|
198
|
+
~15x IS-NOT-NULL perf trap as `custcol_sales_rep_line`; see the SuiteQL custom-column doc).
|
|
199
|
+
This is the plug-in point for `JE_LINE_ITEM_FIELD` (previously null "no item column
|
|
200
|
+
exists"). Consequence: the earlier TRUE-79968/79970 design that grouped every kept JE line
|
|
201
|
+
under `itemId=NULL` is now **wrong** — kept lines carry a real item. The `entity`
|
|
202
|
+
(customer ref) dimension still exists in addition to item. Probe tool:
|
|
193
203
|
`test/@dave/probe_je_shape.php` (dumps line column shapes + custom columns).
|
|
194
204
|
- **No salesRep self-heal (unlike the item path).** `journalLineLookup()` for Employees has
|
|
195
205
|
**no `syncEmployee()` fallback** (the item path self-heals via
|
|
@@ -442,6 +452,20 @@ record is deleted in NetSuite.)
|
|
|
442
452
|
- The cron's sign handling is not portable here — see Sign convention.
|
|
443
453
|
|
|
444
454
|
## Change history
|
|
455
|
+
- 2026-07-07 — **CORRECTION: JE lines DO carry an item — in the custom column
|
|
456
|
+
`custcol_item_sku`, not the native item field** (TRUE-79973, dfranks). Live probe
|
|
457
|
+
2026-07-07 confirmed native `transactionline.item` / REST `line.item` is empty on **all
|
|
458
|
+
12,293** 2026 JE rev/cost lines, while the SKU is populated **line-level** on each rev/cost
|
|
459
|
+
GL line in `custcol_item_sku` as a reference object `{id, refName, links}` (`.id` = item
|
|
460
|
+
internalId, e.g. `"1103457"`), identical in shape to `custcol_sales_rep_line`. Maps via
|
|
461
|
+
`lookupId('Items','netsuiteItemInternalId', .id)`; SuiteQL exposes `tl.custcol_item_sku`
|
|
462
|
+
(raw id) — filter/group with `> 0`, not `IS NOT NULL` (same perf trap). This is the plug-in
|
|
463
|
+
point for `JE_LINE_ITEM_FIELD` (previously null "no item column exists") and **overturns**
|
|
464
|
+
the earlier TRUE-79968/79970 assumption that JE lines carry no item and group under
|
|
465
|
+
`itemId=NULL`. Touch-points for the backfill/import work:
|
|
466
|
+
`_underscore/Component/Forecast/SaleImport/SaleImport.php` (`JE_LINE_ITEM_FIELD`,
|
|
467
|
+
`journalLineLookup`, `buildJournalEntryRows`) and `test/@dave/fixer.php` (JE line SuiteQL).
|
|
468
|
+
(dfranks)
|
|
445
469
|
- 2026-06-30 — **JE import now keeps ONLY rep-bearing lines** (TRUE-79968, dfranks).
|
|
446
470
|
`buildJournalEntryRows` skips any GL line whose **raw** `custcol_sales_rep_line` is empty,
|
|
447
471
|
applied **per-line before** the (salesRep,item) bucketing: a no-rep JE → 0 Sales rows, a
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Worker (worker2) Architecture](architecture.md) | Worker (repo `worker2`) is an AWS Elastic Beanstalk **Worker Tier** application that processes background jobs. | worker2/Controller/Index.php, worker2/Worker/, worker2/LambdaFunctions/, _underscore/Worker.php |
|
|
6
|
+
| [Automated PR Merger — Concurrent Force-Push Clobber Race](features/automated-pr-merger-force-push-race.md) | The automated PR merger `_Worker_Team_GitHub::Merge` (`worker2` `Worker/Team/Github.php`) merges approved PRs to `_production` by **force-pushing from a clone t | Worker/Team/Github.php |
|
|
6
7
|
| [ClickUp Connectivity Watchdog](features/clickup-connectivity-watchdog.md) | A cron watchdog that emails when the ClickUp integration looks disconnected during business hours. | worker2/Worker/Clickup/Health.php, worker2/Database/ClickupHealthWatchdog.sql |
|
|
7
8
|
| [ClickUp Design Sprint Automation (Final Design Outcome)](features/clickup-design-sprint-automation.md) | `_Worker_Clickup_Design` is meant to drive the design-sprint workflow in ClickUp via the API, replacing a set of native ClickUp automations. | worker2/Worker/Clickup/Design.php, worker2/Worker/Clickup.php |
|
|
8
9
|
| [ClickUp GitHub-tab Auto-linking & Ticket-id Branch Naming](features/clickup-github-autolink.md) | How ClickUp surfaces branches/PRs/commits in a ticket's **GitHub tab**, and the branch / PR-title naming convention that triggers it. | |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Automated PR Merger — Concurrent Force-Push Clobber Race
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-06
|
|
10
|
+
owners: [dfranks]
|
|
11
|
+
files:
|
|
12
|
+
- Worker/Team/Github.php
|
|
13
|
+
related:
|
|
14
|
+
- ./clickup-github-autolink.md
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Summary
|
|
18
|
+
|
|
19
|
+
The automated PR merger `_Worker_Team_GitHub::Merge` (`worker2` `Worker/Team/Github.php`)
|
|
20
|
+
merges approved PRs to `_production` by **force-pushing from a clone taken before the read,
|
|
21
|
+
without rebasing**. When two merges run concurrently, the second force-push silently
|
|
22
|
+
overwrites the first merge's commit, orphaning it. This is a silent data-loss failure mode
|
|
23
|
+
in shared deploy tooling — it affects **every repo** merged through the automated merger,
|
|
24
|
+
not just one file.
|
|
25
|
+
|
|
26
|
+
## How it works (the failure mode)
|
|
27
|
+
|
|
28
|
+
- The merger clones/reads `_production`, applies the merge, then `git push --force`.
|
|
29
|
+
- Because the push is a `--force` from a stale clone with no rebase, a merge that landed
|
|
30
|
+
in the window between the read and the push is **overwritten** — its commit is no longer
|
|
31
|
+
an ancestor of `_production`.
|
|
32
|
+
- GitHub still reports the clobbered PR as **MERGED** (the merge happened; it was just
|
|
33
|
+
force-pushed away afterward), so **no error surfaces**. The changed files never reach the
|
|
34
|
+
deployed bundle.
|
|
35
|
+
- Real occurrence: a merged `worker2` webhook handler file was orphaned ~40s later by a
|
|
36
|
+
concurrent automated merge — MERGED on GitHub, absent from `_production`.
|
|
37
|
+
|
|
38
|
+
## Diagnostic — auditing for clobbered PRs
|
|
39
|
+
|
|
40
|
+
For every PR GitHub reports as MERGED into `_production`, test whether its merge commit is
|
|
41
|
+
still an ancestor of the branch:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
gh pr list --repo <repo> --base _production --state merged --json number,mergeCommit
|
|
45
|
+
git merge-base --is-ancestor <mergeCommitSHA> origin/_production # exit 0 = ancestor (fine)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If `--is-ancestor` is **false**, the PR was clobbered (orphaned). Then **confirm real loss**:
|
|
49
|
+
check whether the PR's changed files actually exist on `_production` today. A stale
|
|
50
|
+
force-push artifact can be non-ancestor yet have had its content re-applied by a later
|
|
51
|
+
merge — that case is benign.
|
|
52
|
+
|
|
53
|
+
## Fix direction (do NOT just add a rebase)
|
|
54
|
+
|
|
55
|
+
Rebasing alone is **insufficient**: it only narrows the check-then-act window. A `--force`
|
|
56
|
+
push after a rebase can still clobber a merge that lands between the rebase and the push.
|
|
57
|
+
|
|
58
|
+
Durable fix:
|
|
59
|
+
- **Drop `--force`** — use a plain `push` (or `--force-with-lease`) so a stale push is
|
|
60
|
+
**rejected as non-fast-forward** instead of silently overwriting.
|
|
61
|
+
- Add a **retry loop** (re-fetch → re-rebase → retry) on rejection, **or serialize merges
|
|
62
|
+
behind a lock**.
|
|
63
|
+
- Rebase makes the non-force push succeed on the happy path; **dropping `--force` is what
|
|
64
|
+
makes it safe.**
|
|
65
|
+
|
|
66
|
+
## Change history
|
|
67
|
+
|
|
68
|
+
- 2026-07-06 — Initial doc: documented the `_Worker_Team_GitHub::Merge` concurrent
|
|
69
|
+
force-push clobber race (MERGED-on-GitHub-but-orphaned, silent), the `git merge-base
|
|
70
|
+
--is-ancestor` audit procedure with the benign-artifact caveat, and the fix direction
|
|
71
|
+
(drop `--force`/use `--force-with-lease` + retry/lock, not rebase alone) (dfranks)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
## 2.0 framework
|
|
18
18
|
|
|
19
19
|
- **_underscore** (_Underscore) _(framework core)_ — 22 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
20
|
-
- **worker2** (Worker) —
|
|
20
|
+
- **worker2** (Worker) — 27 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
21
21
|
- **api2** (API) — 7 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
22
22
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
23
23
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
package/package.json
CHANGED