toga-ai 1.0.956 → 1.0.958

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-24
9
+ updated: 2026-10-06
10
10
  owners: ["dfranks", "jcardinal", "mhammontree", "apeterson", "kyalamarthi", "bala"]
11
11
  files:
12
12
  - _underscore/Database.php
@@ -61,6 +61,8 @@ When `_underscore` serves a request for a client it opens **three distinct per-c
61
61
 
62
62
  - **`Core.Clients` has rows for clients whose DBs were never built** (prod 2026-09-24: 46 rows, 35 built). "DB exists" = a `Core.DatabaseHosts` row (`databaseId` + `environmentId` [+ `regionId`]) for the current `Environments.slug` — the same rows `registerClientDatabases()` resolves. Any fan-out over all clients must filter `EXISTS` on `DatabaseHosts` for the env, not `Core.Clients` alone; `isActive`-style flags are wrong (inactive clients still have DBs). Host-row count varies (e.g. `Archive_Nychh` has 1, others 4) — still built. Example: [Tools Multi-Client SQL](../../../../1.0/apps/tools/features/multi-client-sql.md).
63
63
  - **⚠ NEVER read `LAST_INSERT_ID()` (or any other per-connection session value) through `_Query`.** `_Query` defaults `_isReadHostEnabled = true` (`Query.php:8`) and routes SELECTs to `CONNECTION_READ` (`Query.php:273-276`). `LAST_INSERT_ID()` is scoped to the connection that did the INSERT, so a read-host SELECT returns **0** — silently wrong, and now an FK violation wherever the id is used as a foreign key. Use `mysqli_insert_id` on the write/transaction connection and reject `<= 0`. It is not biting today only because most client registrations leave `writeHost`/`readHost` null, and `Database.php:488` then points READ and WRITE at the **same** `mysqli` handle (so raw `mysqli_query` and `new _Query(...)` share one connection and one transaction). Configuring a real read host for any client would break every such call at once. Worked example: [Compass PEOPLE-file user lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
64
+ - **Locks and just-committed rows: force the writer and skip the cache.** A `_Query` SELECT goes to the read host and may be served from the query cache. For `GET_LOCK`/`RELEASE_LOCK` (session-scoped — must be on the writer) or reading a row another job just committed, call `$query->setisReadHostEnabled(false)` and `_Database::useQueryCache(false)`. Worked example: NYCDOE `Sftp/Import` per-checksum lock in [2.0 ASN SFTP Ingestion](../../../../clients/nycdoe/features/asn-sftp-ingestion-2-0.md).
65
+ - **`_Database::transactionCommit()` / `transactionRollback()` with NO database argument leave each alias's "started" flag `true`.** A later `transactionStart()` in the same PHP process then never begins a transaction — writes autocommit and a rollback undoes nothing. Prod worker jobs and api2 requests are safe (one job/request = fresh process state); it bites long-lived processes and test harnesses. Reset with `\_Database::$_transactionStarts = []` before `transactionStart()` (see `runAsWorkerJob` in [worker2 Codeception tests](../../worker2/features/codeception-integration-tests.md)).
64
66
  - **Raw `mysqli_query` bypasses `_Query`'s automatic query-cache reset.** After a hand-rolled write, call `_Database::resetQueryCache($dbName)` yourself or later model reads serve stale rows.
65
67
 
66
68
  - **NEVER build a client's database name as `'Client_' . $clientIdentifier`.** They diverge in production (`Compass_Usa` → `Client_Compass`). Resolve `Core.Clients.clientDatabaseId` → `Core.Databases.name`. This mirrors the log-database resolution already done in `_underscore`'s `Email.php`. A string-built name fails only for the clients whose names happen to diverge, so it passes local testing and breaks in production.
@@ -92,5 +94,6 @@ Methodology lesson — why the wrong conclusion was reached: the model-class com
92
94
  - [Re-pointing a DB alias mid-request](./database-alias-repointing.md)
93
95
 
94
96
  ## Change history
97
+ - 2026-10-06 — Writer+no-cache reads for locks/just-committed rows; no-arg commit/rollback leaves the transaction-started flag set (mhammontree)
95
98
  - 2026-09-23 — Added the read-host routing trap: `_Query` sends SELECTs to `CONNECTION_READ`, so `LAST_INSERT_ID()` read through `_Query` can return 0; noted that null `writeHost`/`readHost` currently collapses both to one handle, and that raw `mysqli_query` needs a manual `resetQueryCache()`. (mhammontree)
96
99
  - 2026-09-24 — Added gotcha: filter client fan-outs by `DatabaseHosts` for the environment; `Core.Clients` includes never-built clients. (jcardinal)
@@ -6,13 +6,14 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
9
+ updated: 2026-10-06
10
10
  owners: [mhammontree]
11
11
  files:
12
12
  - worker2/codeception.yml
13
13
  - worker2/tests/Integration.suite.yml
14
14
  - worker2/tests/Support/Helper/Integration.php
15
15
  - worker2/tests/Integration/ParseCest.php
16
+ - worker2/tests/Integration/ImportDecisionCest.php
16
17
  - worker2/tests/local.env.php.example
17
18
  - worker2/composer.json
18
19
  - worker2/.gitattributes
@@ -28,7 +29,7 @@ First 2.0 app-level automated test suite: Codeception 5 / PHPUnit 11 in worker2
28
29
 
29
30
  ## Summary
30
31
 
31
- worker2 is the **pilot repo** for a 2.0 test framework. One suite, `Integration` (not `unit`/`functional` — the code under test needs a real DB), boots the real `_underscore` framework and calls worker actions in-process against a **local** client DB. First coverage: `Sftp/Parse` (6 tests). **Tests are a manual/local step — never a deploy-time gate.** 1.0's pattern (`codecept.phar` + `.ebextensions/040_run_tests.config` running tests against production on every deploy) was deliberately NOT copied.
32
+ worker2 is the **pilot repo** for a 2.0 test framework. One suite, `Integration` (not `unit`/`functional` — the code under test needs a real DB), boots the real `_underscore` framework and calls worker actions in-process against a **local** client DB. Coverage: `Sftp/Parse` (`ParseCest`) and `Sftp/Import` `RunForFile` decisions (`ImportDecisionCest`) — 18 tests as of 2026-10-06. **Tests are a manual/local step — never a deploy-time gate.** 1.0's pattern (`codecept.phar` + `.ebextensions/040_run_tests.config` running tests against production on every deploy) was deliberately NOT copied.
32
33
 
33
34
  ## How it works
34
35
 
@@ -43,9 +44,11 @@ worker2 is the **pilot repo** for a 2.0 test framework. One suite, `Integration`
43
44
  ## Gotchas
44
45
 
45
46
  - **Calling a worker action directly bypasses `_Route`, so there is no open transaction** — without the explicit `_Database::transactionStart()` the commit is a silent no-op and reads miss writes. Do not work around the symptom (e.g. disabling read-host connections); start the transaction. See [CLI script bootstrap](../../_underscore/features/cli-script-bootstrap.md).
47
+ - **Testing code that commits, rolls back, then writes again (worker job semantics): use `runAsWorkerJob`** (`tests/Support/Helper/Integration.php`). `_Database::transactionCommit()` / `transactionRollback()` with no argument leave the per-alias "started" flag set, so a second `transactionStart()` in the same process does nothing (writes autocommit, rollback undoes nothing). The helper resets `\_Database::$_transactionStarts = []` before `transactionStart()` to mimic a fresh worker request. Framework detail: [Per-Client Database Connections](../../_underscore/features/per-client-database-connections.md).
46
48
  - **Do not `chdir` back after booting.** `Loader::autoloader` resolves project classes against the CWD lazily at first use, so restoring the old CWD breaks autoloading of `_Worker_*` classes mid-test.
47
49
  - **`_Database::$_registers` is public static** — read it directly; no Reflection.
48
50
  - **Prove a regression test fails without the fix** before trusting it (done for the Lenovo comma repair: serial mismatch once columns shift).
49
51
 
50
52
  ## Change history
53
+ - 2026-10-06 — ImportDecisionCest added; runAsWorkerJob helper for the stale transaction-flag trap (mhammontree)
51
54
  - 2026-09-25 — Created: Codeception/PHPUnit pilot, Integration suite for Sftp/Parse; cto-reviewed design, php-reviewer fixes to the harness (mhammontree)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-01
10
- owners: ["kyalamarthi", "ajean"]
9
+ updated: 2026-10-06
10
+ owners: ["kyalamarthi", "ajean", "mhammontree"]
11
11
  files:
12
12
  - worker2/.gitignore
13
13
  - worker2/Worker/Team/Github.php
@@ -15,6 +15,7 @@ files:
15
15
  - worker2/Config/production.ini
16
16
  - worker2/Config/netsuite_ec_key.pem
17
17
  - worker2/Worker/Team/Sprint.php
18
+ - worker2/.platform/php/conf.d/00-log-errors.ini
18
19
  related:
19
20
  - ../workflows/clickup-credential-rotation.md
20
21
  - ./automated-pr-merger-force-push-race.md
@@ -47,6 +48,13 @@ the publish on a credential literal.
47
48
  | TOGa Talos API key — **current** (`[talos] api_key`) | `worker2/Config/production.ini` `[talos]` group (~L230) | Shared by release notes and the transcript pipeline. Committed like every `Config/*.ini` (see Summary). |
48
49
  | PayPal webhook signature verification **off** in production | `worker2/Config/production.ini:182` | Impact already written up — a caller who knows the URL can create a paid SalesOrder. See [Rate PayPal subscription webhook](../../../../clients/rate/features/paypal-subscription-purchase-webhook.md). |
49
50
 
51
+ ### Secrets stored at runtime in `Core.WorkerJobs` (not committed, same exposure)
52
+
53
+ | Exposure | Location | Status |
54
+ |---|---|---|
55
+ | Stack traces include function arguments (first 15 chars of each string) | worker2 stores failed-job traces in `Core.WorkerJobs`; `.platform/php/conf.d/00-log-errors.ini` does **not** set `zend.exception_ignore_args = On` | **DevOps option, not done (Mark: devops decision).** Turning it on strips arguments from every trace. Known leak: phpseclib `login()` frames carry the password — NYCDOE `Sftp/Import` `connect()` works around it by not chaining the exception. |
56
+ | NYCDOE vendor FTP passwords | `Core.WorkerJobs.parameters` of old 1.0 `Utilities/Sftp/DeleteFile/DeleteSftpFile` jobs (1.0 sent the password in the job; source = `FTP_*` constants in `library/app/asnprocessor/*.php`) | **Open — separate ticket: rotate.** See [NYCDOE ServiceNow / ASN Integration](../../../../clients/nycdoe/features/servicenow-integration.md). |
57
+
50
58
  ### Fixed / no longer true
51
59
 
52
60
  - **ClickUp API token in `Sprint.php`** — the hardcoded `CLICKUP_API_ACCESS_TOKEN` constant is
@@ -72,9 +80,6 @@ the publish on a credential literal.
72
80
  - **Keep this table current.** Add an entry whenever a committed credential is found, and move it to
73
81
  *Fixed* when it is rotated and relocated, so this stays the one place the team looks.
74
82
 
75
- ## Change history
76
- - 2026-09-25 — renamed product reference from "TOGa IQ" to "TOGa Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
77
-
78
83
  ## Related
79
84
 
80
85
  - [Rotating the ClickUp API credential](../workflows/clickup-credential-rotation.md) — the worked rotation procedure; follow its shape for any credential here
@@ -84,5 +89,7 @@ the publish on a credential literal.
84
89
  - [Rate PayPal Subscription Purchase & Webhook Pipeline](../../../../clients/rate/features/paypal-subscription-purchase-webhook.md)
85
90
 
86
91
  ## Change history
92
+ - 2026-10-06 — WorkerJobs runtime exposures: trace arguments (`zend.exception_ignore_args` devops option), NYCDOE FTP passwords in old job params (mhammontree)
87
93
  - 2026-10-01 — TOGa Talos release-notes key moved to `[talos] api_key`; old key listed for revocation (ajean)
94
+ - 2026-09-25 — renamed product reference from "TOGa IQ" to "TOGa Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
88
95
  - 2026-09-21 — Register created from audit (kyalamarthi)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-01
9
+ updated: 2026-10-06
10
10
  owners: ["jcardinal", "ajean"]
11
11
  files:
12
12
  - worker2/Worker/Clickup/QaReview.php
@@ -110,6 +110,9 @@ Batch) QC Review → Stage or Client Review → To Deploy → **manual** deploy.
110
110
  **Needs Review** ([general automation](./clickup-general-automation.md) rule 7); `QaReview` /
111
111
  `FeatureReview` ignore Needs Review, so there is no loop.
112
112
  - **Deny** sends the task to **Rework** and **reverts** the task's changes from the QA/QC branch.
113
+ - **BE `Conflict`** (BE Review option orderindex 4) = Denied: Rework + clear Environment + revert
114
+ (`REVIEW_CONFLICT`, reason `be-review-conflict`). Entry reset already clears it; `replayEarlyReview`
115
+ does not replay it.
113
116
  - **Unit-test gate is a STUB today.** `runUnitTests()` currently returns `true`. Real gating (Cypress /
114
117
  Postman / Codeception) is pending — do not treat the gate as enforcing anything yet.
115
118
 
@@ -172,9 +175,17 @@ Batch) QC Review → Stage or Client Review → To Deploy → **manual** deploy.
172
175
  so `FeatureReview` owns them; `StageBatch` ships a Feature as one feature branch and finalizes its
173
176
  subtasks (and skips subtasks in the batch scan).
174
177
  - The feature's own **BE Review is a MANUAL field for now** (jcardinal + CTO decision).
178
+ - `rollUpBe`: feature's own BE **Conflict** → `reworkFeature` (`feature-be-conflict`); any subtask BE
179
+ **Denied or Conflict** → those subtasks + feature to Rework.
175
180
 
176
181
  ## Gotchas
177
182
 
183
+ - **A task already sitting in BE `Conflict` before the 2026-10-06 fix must be moved to Rework by hand** —
184
+ code reacts only to a field change.
185
+ - **ClickUP for Dev MCP is allowlist-scoped.** Its read scope must include Dev Sprints `90116585845`;
186
+ otherwise `get_task` on a custom id fails with "outside the authorized scope" or a misleading
187
+ "Invalid ClickUp API token". Workaround: prod `Team.Tasks` `customClickupIdentifier` →
188
+ `clickupIdentifier`, then `get_task` with the internal id.
178
189
  - **Entry is on ENTERING the status, not leaving it.** The legacy trigger fired on leaving; the QA/QC
179
190
  entry point had to move to the entering event or the whole chain never starts.
180
191
  - **The unit-test gate is a stub (`runUnitTests()` returns true).** It does not block anything yet.
@@ -220,5 +231,6 @@ Batch) QC Review → Stage or Client Review → To Deploy → **manual** deploy.
220
231
  - [ClickUp custom-field conventions](./clickup-custom-field-conventions.md)
221
232
 
222
233
  ## Change history
234
+ - 2026-10-06 — BE Review `Conflict` now routes to Rework + revert like Denied (QaReview + FeatureReview) (jcardinal)
223
235
  - 2026-10-01 — DevFinished CatchUp (cron 86); QC Batch turned on (id 75); review fields reset to Needs Review on entry; Stage deploys confirmed manual (ajean; worker2 PR #178, dbchanges2 PR #504)
224
236
  - 2026-09-25 — Scope narrowed to Dev Sprints `90116585845` (jcardinal)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: nycdoe
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-29
9
+ updated: 2026-10-06
10
10
  owners: [mhammontree]
11
11
  files:
12
12
  - worker2/Worker/Sftp/Import.php
@@ -17,6 +17,8 @@ files:
17
17
  - dbchanges2/Client_Nycdoe/2026-09-22b - Add ASN parsing fields to ServiceRequests.sql
18
18
  - dbchanges2/Client_Nycdoe/2026-09-22c - Add customerPurchaseOrder field to Cases.sql
19
19
  - worker2/tests/Integration/ParseCest.php
20
+ - worker2/tests/Integration/ImportDecisionCest.php
21
+ - dbchanges2/Client_Nycdoe/2026-09-14a - Add SFTP import status fields to Files.sql
20
22
  - dbchanges2/Core/2026-09-14a - Add SFTP Import cron - NYCDOE Lenovo.sql
21
23
  related:
22
24
  - servicenow-integration.md
@@ -29,7 +31,7 @@ related:
29
31
 
30
32
  ## Summary
31
33
 
32
- The 2.0 replacement for 1.0's `legacy_import_asn.php` + `asnprocessor/*` (see [ServiceNow / ASN Integration](servicenow-integration.md) for the 1.0 pipeline, still live). **ASN-first:** nothing is created in 2.0 except from an ingested ASN file. Phase 1 (fetch) and Phase 2 (parse) are built and verified locally against real sample files for all 4 manufacturers; Phase 3 (NetSuite SO, freeze/growth lineage) is not built. Not yet deployed/scheduled in prod.
34
+ The 2.0 replacement for 1.0's `legacy_import_asn.php` + `asnprocessor/*` (see [ServiceNow / ASN Integration](servicenow-integration.md) for the 1.0 pipeline, still live). **ASN-first:** nothing is created in 2.0 except from an ingested ASN file. Phase 1 (fetch) and Phase 2 (parse) are built and verified locally against real sample files for all 4 manufacturers; Gated delete (1.0 hands each file to 2.0) is built, not deployed. Phase 3 (NetSuite SO, freeze/growth lineage) is not built. Not scheduled in prod.
33
35
 
34
36
  ## How it works
35
37
 
@@ -47,23 +49,35 @@ The 2.0 replacement for 1.0's `legacy_import_asn.php` + `asnprocessor/*` (see [S
47
49
  | Acer | CDW 47-col, 3 names differ from Lexmark | `ACER_HEADER` | `YYYY/MM/DD` |
48
50
  | Apple | own | `APPLE_HEADER` | `order_date` `M/D/YY` or `M/D/YYYY` |
49
51
 
50
- ### Activation / cutover (NOT built — design under review)
51
- - **`Sftp/Import/Run` is read-only on the SFTP** (`nlist`/`stat`/`get` only). 1.0 still deletes each file within minutes and runs `*/5` vs 2.0 `*/15`, so a naive 2.0 shadow run sees a random subset. See 1.0 Stage 1b in [ServiceNow / ASN Integration](servicenow-integration.md).
52
- - **DECIDED (Mark, 2026-09-25/29): 1.0 must not delete a file until 2.0 has imported and parsed it.** Replaces the 2026-08-28 "copy to temp subfolder" bridge idea.
53
- - **The 4 `dbchanges2/Core/2026-09-14a - Add SFTP Import cron - NYCDOE <Mfr>` rows ship `isActive=0`** (ordered rollout); they stay off through shadow mode.
54
- - **PROPOSED design (2026-09-29, for review with Jeff; needs a fresh `cto` pass — the 2026-09-25 CTO agreement was for an earlier polling variant):**
55
- 1. 1.0 replaces its `DeleteSftpFile` runTask with `Sftp/Import/RunForFile(clientId, username, filePath)` — no password in SQS.
56
- 2. `RunForFile` loads **all** `Sftp/Import/Run` CronJobs rows for clientId+username (the CDW username returns both Lexmark and Acer rows — never trust a manufacturer from 1.0), matches `fnmatch` patterns (no match → delete directly, old behavior), fetches, saves `Files`, queues Parse with `deleteSourceOnSuccess=true`.
57
- 3. Parse on success queues new `Sftp/Import/DeleteSource(clientId, filesUuid)`, which re-checks `c_status` (`succeeded`/`released`) **and** the remote file's sha256 vs `Files.checksum` before deleting.
58
- 4. Checksum already in `Files`: `succeeded` → delete; stale `pending` → re-queue Parse; `failed` → keep the file.
59
- 5. 1.0 skips files already in `Logs.FileLog` (job+fileName+fileTimestamp) but still re-queues `RunForFile`.
60
- 6. Kill switch `[nycdoe] asn_delete_mode` = `gated` | `immediate`; missing = `immediate`.
61
- 7. Cutover: turn the CronJobs rows on with `deleteSourceOnSuccess`, remove the 1.0 processors.
62
- 8. Add `released` to `Files.c_status` for manual release of failed files.
52
+ ### Gated delete (BUILT 2026-10-06, branch `TRUE-80519`, NOT deployed)
53
+ **Rule (Mark 2026-09-25, Jeff approved flow 2026-10-06): 1.0 never deletes a DOE ASN file; 2.0 deletes it only after it is imported and parsed.** Cutover = flip cron jobs 1.0 → 2.0; Phase 3 (NetSuite) later. 1.0 side: Stage 1b gated mode in [ServiceNow / ASN Integration](servicenow-integration.md).
63
54
 
64
- Schema (`dbchanges2/Client_Nycdoe/`): `2026-09-14a` Files `c_status` (ENUM `pending`/`processing`/`succeeded`/`failed`, not shipped)/`c_manufacturer`/`c_dtProcessed`; `2026-09-22a` ServiceRequestStages seed (New/Scheduled/On Hold/Delivered/Cancelled); `2026-09-22b` ServiceRequests fields; `2026-09-22c` Cases `c_customerPurchaseOrder`; `2026-09-25a` Cases `longDescription`.
55
+ 1. **1.0 queues `Sftp/Import/RunForFile(clientId, username, filePath)`** in place of its delete — no password in SQS.
56
+ 2. **`RunForFile`** loads **all** `Sftp/Import/Run` CronJobs rows for clientId+username (used only as config; rows stay `isActive=0` in shadow mode; the CDW account returns Lexmark + Acer rows — manufacturer is never taken from 1.0).
57
+ - Path must be exactly `remoteDirectory` + a plain file name (`isAllowedFilePath`), else refused.
58
+ - Name matches CronJobs param `retiredFileNamePatterns` (Lenovo row: `DOE-OPEN-LENOVO-*`) → deleted on sight.
59
+ - Name matches no pattern → **KEPT** on the SFTP (Mark: may be a real file sent under a wrong name).
60
+ - mtime < 120 s → wait (next trigger).
61
+ - Per-checksum MySQL `GET_LOCK` on the client **writer**, query cache off. Checksum lookup reads the writer and filters `c_status IS NOT NULL` (`Files` also holds non-SFTP files).
62
+ - Checksum already in `Files`: `succeeded`/`released` → queue `DeleteSource` with the triggering `filePath`; `pending` > 30 min → re-queue Parse; `processing` > 30 min → mark `failed` + throw; `failed` → keep.
63
+ - New file → fetch, save `Files`, queue Parse with `deleteSourceOnSuccess=true`.
64
+ 3. **Parse** claims its `Files` row (`pending`→`processing`, committed; a second job returns `already_claimed`), commits, then queues `DeleteSource`. On error: roll back, then save status **outside** the job transaction — `pending` only for transient DB errnos (1213, 1205, 2006, 2013), else `failed`.
65
+ 4. **`Sftp/Import/DeleteSource(clientId, filesUuid, filePath=null)`** deletes only if `c_status` is `succeeded`/`released` **and** the remote sha256 still equals `Files.checksum`.
66
+ 5. **`Run()`** gained `deleteSourceOnSuccess` and `retiredFileNamePatterns` params and skips files modified < 5 min ago. Validates `fileModelClass` (`is_a _Model_Client_File`) and `adapter` (a method on `_Worker_Sftp_Parse`).
67
+ 6. **Kill switch** is 1.0's `[nycdoe] asn_delete_mode` (missing = old immediate delete).
65
68
 
66
- Tests: `worker2/tests/Integration/ParseCest.php` (all 4 mfrs + header rejection + comma repair) — see [worker2 Codeception integration tests](../../../2.0/apps/worker2/features/codeception-integration-tests.md).
69
+ **Manual release of a failed file:** set `Files.c_status='released'`, then queue `Sftp/Import/DeleteSource` for that `filesUuid`.
70
+
71
+ **Before cutover (open):**
72
+ - `Run()` has no retry for a lost Parse or lost `DeleteSource` job (only `RunForFile` re-drives). A row stuck `processing` turns `failed` only on the next 1.0 trigger after 30 min.
73
+ - **Manual ASN files** are uploaded to the Lenovo folder (e.g. `WR260197522-RPL Manual ASN.csv`, 2026-09-14). 2.0 KEEPS them (no pattern) and imports nothing — decide how 2.0 handles manual files.
74
+ - Separate tickets (not done): SSH host-key pinning in `Sftp/Import` `connect()`; rotate the FTP passwords old `DeleteSftpFile` jobs likely stored in `Core.WorkerJobs.parameters` (see [worker2 secrets register](../../../2.0/apps/worker2/features/committed-secrets-and-config-debt.md)).
75
+
76
+ Schema (`dbchanges2/Client_Nycdoe/`): `2026-09-14a` Files `c_status` (ENUM `pending`/`processing`/`succeeded`/`failed`/`released`) + non-unique index on `Files.checksum`/`c_manufacturer`/`c_dtProcessed`; `2026-09-22a` ServiceRequestStages seed (New/Scheduled/On Hold/Delivered/Cancelled); `2026-09-22b` ServiceRequests fields; `2026-09-22c` Cases `c_customerPurchaseOrder`; `2026-09-25a` Cases `longDescription`.
77
+
78
+ Core `2026-09-14a` Lenovo row carries `retiredFileNamePatterns`. Neither `2026-09-14a` file had run in prod (verified 2026-10-06), so both were edited in place.
79
+
80
+ Tests: `ParseCest.php` (all 4 mfrs, header rejection, comma repair, claim, kept `failed` status) + `ImportDecisionCest.php` (RunForFile decisions, path check, retired names) — see [worker2 Codeception integration tests](../../../2.0/apps/worker2/features/codeception-integration-tests.md).
67
81
 
68
82
  ## Gotchas
69
83
 
@@ -71,9 +85,14 @@ Tests: `worker2/tests/Integration/ParseCest.php` (all 4 mfrs + header rejection
71
85
  - **1.0 `asnprocessor/lenovo.php` is not a source of truth for today's Lenovo layout.** Its 47-col mapping is stale; the real file is 30 cols (confirmed on 3 live samples). Verify every header and date format against a **real current sample file**, never the 1.0 code — wrong Lenovo header, Lexmark date, and Apple 2-digit year (`'25'` → year `0025`) were all caught this way.
72
86
  - **Acer and Lexmark share column positions, not header names** (Acer `First_Name_Shipping`/`Last_Name_Shipping`/`SerialNo` vs Lexmark `Contact_Name_Shipping`/`Email_Shipping`/`Svc_Tag_ID`). Keep separate header constants. Lenovo stays its own path (not a CDW supplier).
73
87
  - **Lenovo unescaped comma in `Product Long Description` (col 18) is repaired, not rejected** — `repairLenovoRowColumnCount()` merges the extra fragments back (ported from 1.0; the only field Lenovo breaks this way). A true header mismatch is still fatal. No real occurrence seen in 2.0 yet — re-confirm when Lenovo next sends one.
74
- - **`DOE-OPEN-LENOVO-*` files are a retired process — never parse; delete on sight** (Skyler/Eric). Lenovo's `fileNamePattern` `DOE-Lenovo-ASN-*.csv` already excludes them. SFTP cleanup of old ones is deferred.
88
+ - **`DOE-OPEN-LENOVO-*` files are a retired process — never parse; delete on sight** (Skyler/Eric). Enforced by the Lenovo row's `retiredFileNamePatterns`.
89
+ - **Every CronJobs `params` JSON key must be a `Run()` parameter** — params are spread as named args, so an unknown key is a fatal error. Add the parameter before the key.
90
+ - **worker2 runs each job in one transaction** (`Route.php` `transactionStart`; `Controller/Index.php` rolls back on throw). A status save followed by `throw` is rolled back — the Phase 2 bug left rejected files `pending` forever. Save failure status after the rollback, outside the job transaction.
91
+ - **`connect()` must not chain the phpseclib exception** — phpseclib `login()` frames carry the first 15 chars of the password, and worker2 stores job traces in `Core.WorkerJobs`. It rethrows a fresh exception with no `previous`.
92
+ - **Locks and just-committed rows must read the writer** — see the read-host gotcha in [Per-Client Database Connections](../../../2.0/apps/_underscore/features/per-client-database-connections.md).
75
93
  - **Schema drift: `_Model_Client_Case` declares `longDescription` but `Client_Nycdoe.Cases` lacked the column** → any direct-model Case load/save 1054s. Fixed for DOE (`2026-09-25a`). Other tenants' `Cases` may have the same gap — unchecked (ask Rohan).
76
94
 
77
95
  ## Change history
96
+ - 2026-10-06 — Gated delete built (RunForFile/DeleteSource, Parse claim + status-outside-transaction fix, retired names, `released` status); open cutover items (mhammontree)
78
97
  - 2026-09-29 — Activation plan: gated-delete decision, proposed RunForFile/DeleteSource design, CronJobs rows set isActive=0 (mhammontree)
79
98
  - 2026-09-25 — Phase 2 parsing built for all 4 mfrs; header validation, comma repair, real-sample header/date corrections; integration tests added (mhammontree)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: nycdoe
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-29
9
+ updated: 2026-10-06
10
10
  owners: [mhammontree, sking]
11
11
  files:
12
12
  - worker/crons/sync/nycdoe/import_asn.php
@@ -28,6 +28,7 @@ files:
28
28
  - worker/schedules/cron.worker.notification.json
29
29
  - library/app/api/nycdoe.php
30
30
  - library/app/nycdoe.php
31
+ - library/app/asnprocessor.php
31
32
  - library/app/asnprocessor/manufacturer.php
32
33
  - library/app/asnprocessor/apple.php
33
34
  - library/app/asnprocessor/lenovo.php
@@ -90,7 +91,7 @@ The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents +
90
91
  | EDI invoices out (NetSuite saved search 2108) | `send_edi_open_invoices.php` | every 2 h at :13 |
91
92
  | Installation schedule reports | `notifications/nycdoe/send_installation_schedules_doe.php` (3 business days ahead, weekends skipped, **holidays NOT skipped**), daily + current-week variants, weekly part-orders | 5–8 AM daily/Mondays |
92
93
 
93
- **Unscheduled / dormant:** `download_tickets*.php` (superseded by `import_inc.php` / `import_asn.php`), `DOA_*` variants, `doe_install_fix.php`, `send_installation_schedules.php` (base variant). Note `sync/syncro/download_tickets.php` in the schedule is the **Syncro** integration, not NYCDOE. A second `crons/infrastructure/import_asn.php` also exists — confirm which variant before editing.
94
+ **Unscheduled / dormant:** `download_tickets*.php` (superseded by `import_inc.php` / `import_asn.php`), `DOA_*` variants, `doe_install_fix.php`, `send_installation_schedules.php` (base variant). Note `sync/syncro/download_tickets.php` in the schedule is the **Syncro** integration, not NYCDOE. A second `crons/infrastructure/import_asn.php` also calls the asnprocessors but is **not scheduled** — only `legacy_import_asn.php` runs.
94
95
 
95
96
  ### The ASN pipeline
96
97
  ```
@@ -111,8 +112,8 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
111
112
 
112
113
  - **Stage 1a (API):** vendor detected by string match on `u_asset_mfg` (apple=1, acer=2, lenovo=3, lexmark=4; unknown → Lexmark). Serial 'S'-prefix stripped for Lexmark/Lenovo. Ship-to resolved via `App_Api_NYCDOEV2::resolveShippingAddress()`; on failure a tracking `NYCDOETicket` is recorded but **ASN creation is skipped**. `dedupeKey = vendorId|PO|DOE-part|serialNumber`. Dedup is SELECT-before-INSERT with per-row try/catch (unique index is the race backstop).
113
114
  - **Stage 1b (SFTP):** the only source of **non-serialized** items (in practice the Lexmark feed). Serial-less lines of the same PO+part are summed per file into one queue row with `dedupeKey = vendorId|PO|DOE-part|<file token>`. Dedup is INSERT + catch "Duplicate entry" (race-safe).
114
- - **Per file, in order** (`App_ASNProcessor_{Lenovo,Apple,Lexmark}::processFiles()`): read over plain FTP (`App_FTP`: `ftp_connect`/`ftp_rawlist`/`ftp_get`) → save legacy `Logs.FileLog` (jobs `ASN_LENOVO` / `ASN_APPLE` / `ASN_LEXMARK`) → parse → cache to disk → email → queue `App_Worker::runTask('Utilities/Sftp/DeleteFile/DeleteSftpFile')` (`lenovo.php:176-184`, `apple.php:105-113`, `lexmark.php:102-110`). worker2 `Worker/Utilities/Sftp/DeleteFile.php` then deletes the file over SFTP (phpseclib3). ⚠ The SFTP password travels **inside the SQS message** (source: hardcoded constants in `asnprocessor/*.php`) — see the credentials gotcha.
115
- - **Delete is gated only on the disk-cache write** (`$filePutContentsResults !== false`), **not on a good parse** — a bad file is still deleted within minutes, and `Logs.FileLog` is the only copy left.
115
+ - **Per file, in order** (`App_ASNProcessor_{Lenovo,Apple,Lexmark}::processFiles()`): read over plain FTP (`App_FTP`: `ftp_connect`/`ftp_rawlist`/`ftp_get`) → parse → cache to disk → email → save legacy `Logs.FileLog` (jobs `ASN_LENOVO` / `ASN_APPLE` / `ASN_LEXMARK`; the row means "done") → `App_ASNProcessor::releaseSourceFile()`. A file is never released before it is logged.
116
+ - **`asn_delete_mode` (`[nycdoe]` in `worker/config.worker.ini`; built 2026-10-06 on `TRUE-80519`, NOT deployed):** missing/`immediate` = original behavior — queue `App_Worker::runTask('Utilities/Sftp/DeleteFile/DeleteSftpFile')`, worker2 `Worker/Utilities/Sftp/DeleteFile.php` deletes over SFTP (phpseclib3), delete gated only on the disk-cache write, not a good parse. ⚠ That path sends the SFTP password **inside the SQS message** (source: `FTP_*` constants in `asnprocessor/*.php`) — see the credentials gotcha. `gated` = queue 2.0 `Sftp/Import/RunForFile` (clientId/username/filePath, no password); 2.0 deletes only after its own import+parse — see [2.0 ASN SFTP Ingestion](asn-sftp-ingestion-2-0.md) (Gated delete). Gated mode also skips files already in `Logs.FileLog` (`isFileAlreadyLogged()`: job + fileName + fileTimestamp via `App_Date::convertToSQLDate`, same as `App_Model`) but still re-releases them. **Activate:** set the ini value + worker deploy. Default config verified to behave exactly as before (offline under PHP 7.2; SQL against legacy prod).
116
117
  - **No Acer processor runs.** `asnprocessor/acer.php` is dead code (not called from `legacy_import_asn.php`, never appends to `$output`, never deletes). Acer files do arrive — via CDW on the shared CDW/Lexmark SFTP account — and Lexmark `processFiles()` has **no file-name filter**, so 1.0 processes every Acer file **as Lexmark** (Lexmark vendorId). Proof: 458 `ASN_LEXMARK` FileLog rows with `acer` in `fileName` (2025-08-18 → 2026-09-25), 0 `ASN_ACER`. Any 1.0-vs-2.0 count must compare 1.0 Lexmark against 2.0 Lexmark + Acer.
117
118
  - **Vendors upload and forget** — they never re-send or notice if a file sits on the SFTP (Mark, 2026-09-25). A deleted-but-unparsed file is gone unless recovered from `Logs.FileLog`.
118
119
  - **Stage 2:** queue → ASN/Items/Units/TrackingNumbers in a transaction. Serialized: one Unit per serial. Non-serialized: the file's quantity is a **cumulative** total — `unsent item qtyOrder = cumulative − SUM(committed qtyOrder)`; post-freeze growth becomes a **fresh item row**, a decrease fires a discrepancy email and changes nothing. Exactly one Unit per serial-less item row regardless of quantity. Failed rows retry 5× then alert `devteam@goagilant.com`.
@@ -268,6 +269,7 @@ All **live** DOE business logic is 1.0. The only other 2.0 code is the in-progre
268
269
  - Local deep-dive (full WR260236464 case study with record IDs): `doe_service_now_integration.md` in Mark's WWW workspace root; also the `service-now-integration` skill (`asn-import-flow.md`)
269
270
 
270
271
  ## Change history
272
+ - 2026-10-06 — Stage 1b gated delete mode (`asn_delete_mode`, `releaseSourceFile`, FileLog saved before release) built, not deployed (mhammontree)
271
273
  - 2026-09-29 — Stage 1b file-delete path (delete not gated on parse; SFTP password rides in SQS), Acer-as-Lexmark, FileLog index facts (mhammontree)
272
274
  - 2026-09-29 — Assignment-group scoping corrected (inverted `isValidDepartmentName`, three competing group lists) with detail in hold-status-sync; `Logs.API` 30-day retention shipped. (mhammontree)
273
275
  - 2026-09-25 — TRUE-82332: cross-link to the close-clobber fix; test-repo copy of the ServiceNow credential noted (mhammontree)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.956",
3
+ "version": "1.0.958",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",