toga-ai 1.0.392 → 1.0.394
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/library/features/netsuite-suiteql-api-reference.md +14 -2
- package/knowledge/1.0/apps/worker/INDEX.md +1 -1
- package/knowledge/1.0/apps/worker/features/forecast2-netsuite-reconciliation.md +36 -2
- package/knowledge/2.0/apps/_underscore/features/netsuite-rest-client.md +43 -2
- package/knowledge/2.0/apps/dbchanges2/architecture.md +15 -1
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -1
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +25 -1
- package/knowledge/2.0/apps/worker2/features/team-sprint-management.md +28 -1
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["dfranks"]
|
|
9
|
+
updated: 2026-07-21
|
|
10
|
+
owners: ["dfranks", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/netsuite/rest.php
|
|
13
13
|
- library/ssl/netsuite_ec_key.pem
|
|
@@ -343,6 +343,13 @@ numbers. Budget hours for multi-year runs and launch them under `nohup`/`tmux`.
|
|
|
343
343
|
arrow functions (`fn() =>`), typed properties (`public int $x`), `??=`, `match()`, numeric
|
|
344
344
|
separators (`1_000`). Lint with `C:\xampp7\php\php.exe -l` before deploying — `C:\xampp8\php`
|
|
345
345
|
(PHP 8.0) is for running probes on the laptop only, **not** for compat checking.
|
|
346
|
+
- **⚠ Blast radius: a SINGLE PHP 7.4+ token here is a FULL outage, not a localized bug.** On
|
|
347
|
+
2026-06-10 one `static fn(...)` left in `library/app/api/netsuite/rest.php` **parse-errored
|
|
348
|
+
`App_Api_Netsuite_Rest` on class load** — since the parse failure happens at require time, it
|
|
349
|
+
took down **every** consumer of the class at once: **all** 5-minute REST sync crons crashed and
|
|
350
|
+
stayed down until the token was reverted. There is no partial degradation — a banned token
|
|
351
|
+
anywhere in this file crashes the whole NetSuite REST tier. Lint before every deploy; treat a
|
|
352
|
+
7.4+ token in `rest.php` as a production-down defect, not a style nit.
|
|
346
353
|
- Field/relationship availability varies by NetSuite account **and** record type — probe the live
|
|
347
354
|
account before assuming a column exists.
|
|
348
355
|
- **`IS NOT NULL` on a custom `transactionline` column inside an aggregate query is a ~15× planner
|
|
@@ -364,6 +371,11 @@ numbers. Budget hours for multi-year runs and launch them under `nohup`/`tmux`.
|
|
|
364
371
|
|
|
365
372
|
## Change history
|
|
366
373
|
|
|
374
|
+
- 2026-07-21 — **Recorded the "one PHP 7.4+ token = total outage" blast-radius lesson** on the PHP
|
|
375
|
+
7.2 compat gotcha (folded in from a retired project-local CLAUDE.md). On 2026-06-10 a single
|
|
376
|
+
`static fn(...)` in `rest.php` parse-errored `App_Api_Netsuite_Rest` on class load and crashed ALL
|
|
377
|
+
5-minute REST sync crons until reverted — a banned token here is production-down, not localized.
|
|
378
|
+
(jcardinal)
|
|
367
379
|
- 2026-07-07 — **Added the `systemnote` field-change forensics section and qualified the custom-column
|
|
368
380
|
perf rule** (Forecast2 SALES profit-drift forensics). `systemnote` filtered by `field`+`"date"` is
|
|
369
381
|
~1s vs ~279s by `recordid` alone; it carries `context` (`UIF`), `name` (acting user), `role`,
|
|
@@ -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/checker.php, worker2/Component/Forecast/SaleImport/SaleImport.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 |
|
|
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, worker2/Component/Forecast/SaleImport/SaleImport.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/reconcile_drift_2023plus.php, test/@dave/probe_invoice_gap_2026.php, test/@dave/probe_creditmemo_gap_detail.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, library/app/api/netsuite/rest.php, library/app/systemmonitor/netsuiteintegration.php |
|
|
10
10
|
| [OneUptime external uptime monitoring for 1.0 workers](features/oneuptime-worker-uptime-monitoring.md) | Every 1.0 worker box self-reports its liveness to an external OneUptime monitor once per minute by curl-POSTing to a per-worker "Incoming Request" heartbeat URL | library/app/worker.php, worker/crons/worker/worker_heartbeat.php |
|
|
11
11
|
| [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 |
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
10
|
-
owners: [dfranks]
|
|
9
|
+
updated: 2026-07-21
|
|
10
|
+
owners: [dfranks, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- test/@dave/checker.php
|
|
13
13
|
- worker2/Component/Forecast/SaleImport/SaleImport.php
|
|
@@ -16,6 +16,9 @@ files:
|
|
|
16
16
|
- test/@dave/fixer.php
|
|
17
17
|
- test/@dave/analyze_netsuite_forecast_diff.php
|
|
18
18
|
- test/@dave/trueup_sales.php
|
|
19
|
+
- test/@dave/reconcile_drift_2023plus.php
|
|
20
|
+
- test/@dave/probe_invoice_gap_2026.php
|
|
21
|
+
- test/@dave/probe_creditmemo_gap_detail.php
|
|
19
22
|
- test/@dave/trueup_open_orders.php
|
|
20
23
|
- test/@dave/loop_trueup_open_orders.php
|
|
21
24
|
- test/@dave/trueup_opportunities.php
|
|
@@ -98,6 +101,15 @@ by reconciling a chosen tranDate range directly against NetSuite.
|
|
|
98
101
|
NS_ONLY (missing from FC), FC_ONLY (stale/extra), DRIFT (value differs). Read-only.
|
|
99
102
|
- `trueup_sales.php --from --to [--chunk-days N] [--prod] [--dry-run]` — makes `Forecast.Sales`
|
|
100
103
|
match NetSuite for a tranDate range (insert/update/delete per line).
|
|
104
|
+
- `reconcile_drift_2023plus.php` — **transaction-level** (NOT line-level) reconciler for a
|
|
105
|
+
**known, small, discrepant set** of transactions. Distinct from the line-level trueup tools: for
|
|
106
|
+
**only the flagged transactions** it **DELETEs all lines then re-INSERTs them from REST**, writes
|
|
107
|
+
a **pre-image CSV backup** first, and is **dry-run by default** (`--commit` to write). Built to
|
|
108
|
+
close the SOAP-era double-line bug (below): it took the 2023 gap from **+$2.27M to +$31.8K**.
|
|
109
|
+
**Decision rule — which reconciler:** a **known small discrepant set** → `reconcile_drift_2023plus`
|
|
110
|
+
(transaction-level delete+reinsert); an **unknown full-window** drift → `trueup_sales`
|
|
111
|
+
(line-level insert/update/delete). **Diagnostic ladder to identify the set:**
|
|
112
|
+
`probe_sales_gap_direct` → `probe_invoice_gap_2026` → `probe_creditmemo_gap_detail`.
|
|
101
113
|
- `trueup_open_orders.php --from= --to= [--prod] [--dry-run] [--verbose] [--quiet] [--by-lastmodified]`
|
|
102
114
|
— same for `Forecast.OpenOrderItems` (currently-open SOs whose tranDate falls in range).
|
|
103
115
|
- **`--by-lastmodified`** windows **both** passes on `lastmodifieddate` instead of `tranDate`:
|
|
@@ -303,6 +315,17 @@ None — Forecast2 is a single shared dataset.
|
|
|
303
315
|
|
|
304
316
|
## Gotchas / known issues
|
|
305
317
|
|
|
318
|
+
- **SOAP-era double-line bug (2023) — lines inserted TWICE under consecutive-but-different line
|
|
319
|
+
numbers, evading the dup check.** The SOAP-era importer **ran twice for some invoice batches** and
|
|
320
|
+
inserted each invoice's lines **twice** under consecutive-but-*different* `lineNumber`s — so the
|
|
321
|
+
`(txnId, lineNumber)` duplicate check never caught it (the second copy had a different line
|
|
322
|
+
number). **Fingerprint:** Forecast revenue is **exactly 2× NetSuite**, **transaction counts
|
|
323
|
+
match** (it's line duplication, not extra transactions), and **FC line rows > NS line count**.
|
|
324
|
+
Affected **3 months in 2023** (transaction dates **May 1 / Jul 2 / Dec 7**). Fixed with
|
|
325
|
+
`reconcile_drift_2023plus.php` (transaction-level delete-all-lines-then-reinsert-from-REST), which
|
|
326
|
+
closed the 2023 gap from **+$2.27M to +$31.8K**. When a window shows exact-2× revenue with matching
|
|
327
|
+
txn counts and more FC lines than NS, suspect this pattern and use the transaction-level reconciler,
|
|
328
|
+
not the line-level trueup.
|
|
306
329
|
- **CROSS-TYPE CONTAMINATION: once JEs share `Forecast.Sales`, any FC Sales read missing a type scope
|
|
307
330
|
treats JE rows as rogue sales.** Discovered live (TRUE-79862): the SALES fix **deleted 74 JE rows
|
|
308
331
|
(8 JEs) as "stale sales"** because `findSalesDiscrepancies`'s FC query had **no type filter**, so JE
|
|
@@ -549,6 +572,17 @@ None — Forecast2 is a single shared dataset.
|
|
|
549
572
|
|
|
550
573
|
## Change history
|
|
551
574
|
|
|
575
|
+
- 2026-07-21 — **Documented `reconcile_drift_2023plus.php` + the SOAP-era double-line bug (2023)**
|
|
576
|
+
(folded in from a retired project-local CLAUDE.md). The SOAP-era importer ran twice for some
|
|
577
|
+
invoice batches and inserted each invoice's lines twice under consecutive-but-different line
|
|
578
|
+
numbers, evading the `(txnId, lineNumber)` dup check — fingerprint = FC revenue exactly 2× NS,
|
|
579
|
+
matching txn counts, FC line rows > NS lines; affected 3 months in 2023 (txn dates May 1 / Jul 2 /
|
|
580
|
+
Dec 7). `reconcile_drift_2023plus.php` reconciles a **known small set** at the **transaction level**
|
|
581
|
+
(DELETE all lines then re-INSERT from REST for only flagged txns; pre-image CSV backup; dry-run by
|
|
582
|
+
default, `--commit` to write), closing the 2023 gap from +$2.27M to +$31.8K. Recorded the decision
|
|
583
|
+
rule (known small set → `reconcile_drift_2023plus`; unknown full window → `trueup_sales`) and the
|
|
584
|
+
diagnostic ladder (`probe_sales_gap_direct` → `probe_invoice_gap_2026` →
|
|
585
|
+
`probe_creditmemo_gap_detail`). (jcardinal)
|
|
552
586
|
- 2026-07-14 — **`fixer.php` now resolves `customerId` on JE lines, mirroring the importer +
|
|
553
587
|
recorded the manual all-time backfill** (TRUE-80129, dfranks). `fixJournalEntries()` (the
|
|
554
588
|
hand-maintained mirror of `buildJournalEntryRows`) previously hard-coded `customerId = NULL`;
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["dfranks"]
|
|
9
|
+
updated: 2026-07-21
|
|
10
|
+
owners: ["dfranks", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/Api/Netsuite/Netsuite.php
|
|
13
13
|
related:
|
|
@@ -31,6 +31,40 @@ SuiteQL escaping rule — that callers in other repos depend on.
|
|
|
31
31
|
> (`\api\_Component_Api_Netsuite`) for its own use. Editing the api2 copy has **no effect** on the
|
|
32
32
|
> worker2/webhook path — change the `_underscore` one.
|
|
33
33
|
|
|
34
|
+
## Authentication & config
|
|
35
|
+
|
|
36
|
+
- **OAuth 2.0 certificate-signed JWT client assertion.** The adapter authenticates by signing a
|
|
37
|
+
JWT client assertion with a certificate registered on the NetSuite integration record — no
|
|
38
|
+
username/password, no stored bearer. Signing algorithm is **ES256 preferred** (EC private key),
|
|
39
|
+
with **RS256 as fallback**. The access token is **cached and auto-refreshed ~60s before expiry**,
|
|
40
|
+
so callers never manage token lifecycle.
|
|
41
|
+
- **All credentials come from the `[netsuite]` ini section — nothing is hardcoded.** Keys:
|
|
42
|
+
- `host` — the NetSuite REST host.
|
|
43
|
+
- `clientId` — the integration record's client id.
|
|
44
|
+
- `certificateId` — the integration record's certificate id (the JWT **`kid`**).
|
|
45
|
+
- `privateKeyPath` — filesystem path to the signing private key.
|
|
46
|
+
The per-developer block lives in `worker2/Config/dev-<name>-laptop.ini`. **Document only the key
|
|
47
|
+
names/locations — never paste the values.** (See the 1.0 `netsuite-suiteql-api-reference.md`
|
|
48
|
+
*Authentication* section for the sibling `App_Api_Netsuite_Rest` client, which reads the same
|
|
49
|
+
`[netsuite]` section via `_Config::netsuite()` in the 2.0 context.)
|
|
50
|
+
|
|
51
|
+
### Public API surface
|
|
52
|
+
|
|
53
|
+
- **`send($method, $route, $payload, $headers, $throwExceptionsOnError)`** — the general REST call.
|
|
54
|
+
Verb + route + JSON payload + optional extra headers; `$throwExceptionsOnError` toggles whether a
|
|
55
|
+
non-2xx raises or is returned. (Record writes go through `createRecord`/`send('PATCH', …)` above;
|
|
56
|
+
reads through `fetchRecord`/`send('GET', …)`.)
|
|
57
|
+
- **`RECORD_*` constant catalog** — canonical route constants so callers never hardcode record
|
|
58
|
+
paths: `SALES_ORDER`, `INVOICE`, `CASH_SALE`, `CASH_REFUND`, `CREDIT_MEMO`, `OPPORTUNITY`, `ITEM`,
|
|
59
|
+
`INVENTORY_ITEM`, `CUSTOMER`, `EMPLOYEE`, `VENDOR`, `PURCHASE_ORDER`.
|
|
60
|
+
- **`QUERY_SUITEQL`** — the SuiteQL query route; **`METADATA_CATALOG`** — the metadata/catalog route
|
|
61
|
+
(record & field metadata discovery).
|
|
62
|
+
- **`?expandSubResources=true`** — append to a record GET to inline sub-resources (sublists) in one
|
|
63
|
+
call rather than issuing follow-up requests.
|
|
64
|
+
- **Response normalization rule:** the underlying transport may hand back an already-decoded object
|
|
65
|
+
**or** a raw JSON string, so normalize every response with `is_string($x) ? json_decode($x) : $x`
|
|
66
|
+
before consuming it.
|
|
67
|
+
|
|
34
68
|
## Record writes — REST has no SOAP `baseRef->internalId`
|
|
35
69
|
|
|
36
70
|
### Create — `createRecord(string $route, array $payload): string`
|
|
@@ -121,6 +155,13 @@ doc.)
|
|
|
121
155
|
|
|
122
156
|
## Change history
|
|
123
157
|
|
|
158
|
+
- 2026-07-21 — **Documented the auth mechanism, config-key locations, `send()` signature, and the
|
|
159
|
+
`RECORD_*`/query/metadata constant catalog** (folded in from a retired project-local
|
|
160
|
+
`worker2/netsuite/CLAUDE.md`). Auth = OAuth 2.0 certificate-signed JWT client assertion
|
|
161
|
+
(ES256 preferred / RS256 fallback), token cached + refreshed ~60s before expiry; all credentials
|
|
162
|
+
from the `[netsuite]` ini section (`host`/`clientId`/`certificateId`=`kid`/`privateKeyPath`),
|
|
163
|
+
per-dev block in `worker2/Config/dev-<name>-laptop.ini`. Recorded `?expandSubResources=true` and
|
|
164
|
+
the `is_string($x) ? json_decode($x) : $x` response-normalization rule. (jcardinal)
|
|
124
165
|
- 2026-06-26 — **`fetchRecord` (generic NS REST GET + decode) moved onto this class** from
|
|
125
166
|
`_Component_Forecast_Db`, beside `createRecord`; 7 callers reprefixed (SaleImport ×2,
|
|
126
167
|
Opportunity ×3, SalesOrder ×2), git-grep clean + runtime-verified. Recorded the
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-21
|
|
10
10
|
owners: [jcardinal, mhammontree, bala]
|
|
11
11
|
files:
|
|
12
12
|
- Core/
|
|
@@ -173,6 +173,13 @@ its own header.)
|
|
|
173
173
|
the relevant clients list `<module>` in their `_modules.txt`.
|
|
174
174
|
5. Never edit or re-date an already-applied file — add a new dated file instead. Never put new
|
|
175
175
|
work in a `HISTORIC` folder.
|
|
176
|
+
6. **Never seed a `uuid` column with MySQL `UUID()`.** The 2.0 standard requires a fully-random
|
|
177
|
+
**v4** UUID; `UUID()` is time/MAC-based (v1) and violates it. Since a migration can't call
|
|
178
|
+
`_String::generateUuid()`, **pre-generate a v4 UUID literal** (e.g. a v4 generator) and
|
|
179
|
+
hardcode the literal string in the `VALUES` clause. One earlier migration
|
|
180
|
+
(`Core/2026-05-08 … CronJobs_WorkerCleanup_insert`) used `UUID()` — that is a known violation,
|
|
181
|
+
not a precedent to copy. First applied correctly in
|
|
182
|
+
`Core/2026-07-21b - Insert - SprintLockScheduled CronJob.sql`.
|
|
176
183
|
|
|
177
184
|
## Bulk data loads — batch, and stage large sets in a temp table
|
|
178
185
|
|
|
@@ -285,3 +292,10 @@ platform-wide infrastructure, not a single application. It has no code dependenc
|
|
|
285
292
|
folder names mirror the DB families (`Core`, `Logs`, `Client_<Tenant>`, `Logs_<Tenant>`)
|
|
286
293
|
defined in `2.0/apps/_underscore/architecture.md`, and its change files create/alter the
|
|
287
294
|
tables that `_Model_*` classes map to.
|
|
295
|
+
|
|
296
|
+
## Change history
|
|
297
|
+
- 2026-07-21 — Added rule #6 to *Adding a new change*: never seed a `uuid` column with MySQL
|
|
298
|
+
`UUID()` (time/MAC-based v1, violates the 2.0 v4-UUID standard); pre-generate a v4 UUID
|
|
299
|
+
literal and hardcode it in `VALUES`. `Core/2026-05-08 … CronJobs_WorkerCleanup_insert` is a
|
|
300
|
+
known violation, not a precedent; first applied correctly in
|
|
301
|
+
`Core/2026-07-21b - Insert - SprintLockScheduled CronJob.sql`. (jcardinal)
|
|
@@ -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 |
|
|
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) |
|
|
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 |
|
|
@@ -111,11 +111,29 @@ No Lambda/routing changes — just create the file:
|
|
|
111
111
|
2. Insert a `Core.CronJobs` row:
|
|
112
112
|
```sql
|
|
113
113
|
INSERT INTO Core.CronJobs (uuid, isActive, name, schedule, maxExecutionTime, action, parameters)
|
|
114
|
-
VALUES (
|
|
114
|
+
VALUES ('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx', 1, 'Daily Report Generation', '0 6 * * *', 300, 'Reports/Daily/Generate', NULL);
|
|
115
|
+
-- uuid MUST be a pre-generated v4 UUID literal, NOT MySQL UUID() (v1) — see dbchanges2 architecture.md rule #6
|
|
115
116
|
```
|
|
116
117
|
`schedule` is evaluated in **Central time**; `maxExecutionTime` is the watchdog timeout.
|
|
117
118
|
3. CronScheduler fires it at the next matching minute — no code wiring needed.
|
|
118
119
|
|
|
120
|
+
Adding a recurring job is **pure data**: inserting the `Core.CronJobs` row (uuid, isActive,
|
|
121
|
+
name, schedule, maxExecutionTime, action, parameters) is all that's needed. The CronScheduler
|
|
122
|
+
Lambda runs every minute, matches active schedules via `croniter`, and enqueues a `WorkerJobs`
|
|
123
|
+
job. There is **no Lambda/code change** beyond the row — but the referenced `action` must exist
|
|
124
|
+
in the deployed worker code, so ship the code deploy and the dbchanges2 cron-row migration
|
|
125
|
+
**together**.
|
|
126
|
+
|
|
127
|
+
> **Biweekly / non-cron schedules — run often, self-gate in PHP.** Cron (5-field) can express
|
|
128
|
+
> "every Wednesday" but **not** "every *other* Wednesday" (or any period cron can't name).
|
|
129
|
+
> Don't try to encode it in the schedule. Instead schedule the job at the coarser recurring
|
|
130
|
+
> interval it fits (e.g. `0 10 * * 3`) and have the action **self-gate against a date/state
|
|
131
|
+
> table** at the top of the method — run the real work only when the gate says today qualifies,
|
|
132
|
+
> otherwise return a skip message (captured to `Core.WorkerJobs.output`). Proven by
|
|
133
|
+
> `Team/Sprint/SprintLockScheduled`, which fires every Wednesday but only runs `SprintLock()`
|
|
134
|
+
> when a `Sprints` row ends yesterday — see
|
|
135
|
+
> [team-sprint-management](./team-sprint-management.md).
|
|
136
|
+
|
|
119
137
|
## Test any action directly
|
|
120
138
|
|
|
121
139
|
```bash
|
|
@@ -175,6 +193,12 @@ be reattempted.
|
|
|
175
193
|
commit-before-SQS transaction pattern that the worker relies on.
|
|
176
194
|
|
|
177
195
|
## Change history
|
|
196
|
+
- 2026-07-21 — Documented that adding a recurring job is **pure data** (a `Core.CronJobs` row;
|
|
197
|
+
CronScheduler Lambda matches via croniter every minute, no code wiring beyond the row — but
|
|
198
|
+
the action must be deployed), and the **biweekly self-gating pattern**: for schedules cron
|
|
199
|
+
can't express (e.g. every-other-Wednesday), run at a coarser cron interval and gate in PHP
|
|
200
|
+
against a date/state table, returning a skip otherwise. Proven by
|
|
201
|
+
`Team/Sprint/SprintLockScheduled`. (jcardinal)
|
|
178
202
|
- 2026-07-21 — `WorkerJobs.failureReason` column renamed to **`output`** and repurposed to store
|
|
179
203
|
successful execution results as well as failure text; success now writes the composed
|
|
180
204
|
"Successfully Executed" message (return value `json_encode`d for arrays/objects). (jcardinal)
|
|
@@ -6,10 +6,11 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-21
|
|
10
10
|
owners: ["jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Team/Sprint.php
|
|
13
|
+
- dbchanges2/Core/CronJobs (SprintLockScheduled seed)
|
|
13
14
|
related:
|
|
14
15
|
- ../architecture.md
|
|
15
16
|
- ./creating-worker-actions.md
|
|
@@ -64,6 +65,10 @@ A sprint runs ~14 days. Across the cycle these actions fire (scheduled as crons)
|
|
|
64
65
|
2. **~9:00 AM** — `SprintEnd` captures and scores the just-finished sprint, emails reports,
|
|
65
66
|
and (optionally) sends release notes.
|
|
66
67
|
3. **~9:45 AM (after launch ceremony)** — `SprintLock` freezes the new sprint's plan.
|
|
68
|
+
In production this is now invoked via the scheduled self-gating wrapper
|
|
69
|
+
`SprintLockScheduled` (see below), not by a cron that targets `SprintLock` directly —
|
|
70
|
+
because the launch day is **every other Wednesday** and cron cannot express a biweekly
|
|
71
|
+
schedule.
|
|
67
72
|
4. **Daily during the sprint** — `SprintDaily` emails leadership a progress dashboard.
|
|
68
73
|
|
|
69
74
|
Most methods accept an optional `$sprint` number; when omitted they resolve the relevant
|
|
@@ -89,6 +94,21 @@ alert. With `$generateSprintLockReport`, emits an Excel lock report of committed
|
|
|
89
94
|
unplanned/stretch points and tasks. When `$allowUnplannedTasks` is false, tasks appearing
|
|
90
95
|
after lock with an UNPLANNED work type are flagged.
|
|
91
96
|
|
|
97
|
+
### `SprintLockScheduled(): string`
|
|
98
|
+
A thin **scheduled wrapper around `SprintLock()`** that solves the biweekly-schedule problem.
|
|
99
|
+
The launch/lock ceremony runs every *other* Wednesday, but cron has no biweekly expression, so
|
|
100
|
+
its `CronJobs` row fires **every** Wednesday (`0 10 * * 3`, 10:00 AM Central) and this method
|
|
101
|
+
**self-gates in PHP** against the `Sprints` table: it queries for a sprint whose `dateEnd` =
|
|
102
|
+
**yesterday**. If one exists, today is a new-sprint start day → it calls `self::SprintLock()`.
|
|
103
|
+
Otherwise it returns a skip message (captured to `Core.WorkerJobs.output`) and does nothing.
|
|
104
|
+
`SprintLock()` itself is unchanged — this only decides *whether* to run it today. Action path
|
|
105
|
+
`Team/Sprint/SprintLockScheduled` → `_Worker_Team_Sprint::SprintLockScheduled`.
|
|
106
|
+
|
|
107
|
+
> **Deploy coupling.** The worker2 code deploy and the dbchanges2 migration that seeds the
|
|
108
|
+
> `Core.CronJobs` row must be **released together** — the cron row references the
|
|
109
|
+
> `Team/Sprint/SprintLockScheduled` action, which must exist in the deployed code or the
|
|
110
|
+
> scheduled job fails when it fires.
|
|
111
|
+
|
|
92
112
|
### `SprintDaily($sprint = null, …)`
|
|
93
113
|
In-sprint **leadership dashboard**, read-only with respect to metrics. Optionally re-captures
|
|
94
114
|
ClickUp data, then builds a multi-sheet workbook:
|
|
@@ -210,6 +230,13 @@ backfills `workTypeAtLock` the first time a task is seen).
|
|
|
210
230
|
|
|
211
231
|
## Change history
|
|
212
232
|
|
|
233
|
+
- 2026-07-21 — Added `SprintLockScheduled()` action + a `Core.CronJobs` seed
|
|
234
|
+
(`0 10 * * 3`, every Wednesday 10 AM Central, action `Team/Sprint/SprintLockScheduled`,
|
|
235
|
+
maxExecutionTime 600) to run **Sprint Lock automatically at the start of each biweekly
|
|
236
|
+
sprint**. Cron can't express "every other Wednesday", so the job runs every Wednesday and
|
|
237
|
+
self-gates in PHP: it calls the unchanged `SprintLock()` only when a `Sprints` row has
|
|
238
|
+
`dateEnd = yesterday` (i.e. today starts a new sprint), else returns a skip. Worker code and
|
|
239
|
+
the dbchanges2 cron-row migration must deploy together. (jcardinal)
|
|
213
240
|
- 2026-06-24 — Fixed a duplicate-key crash (`Tasks.sprintId_clickupIdentifier`) in the
|
|
214
241
|
ClickUp→DB sync: ClickUp pagination can return the same task on multiple pages, and the
|
|
215
242
|
loop was overloading one map (`$lookupTaskByClickUpId`) for both update-vs-insert and
|
package/package.json
CHANGED