toga-ai 1.0.474 → 1.0.476

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.
@@ -95,6 +95,20 @@ so they displayed **double-encoded** (e.g. "Grand & Toy"). `vocabulary/post.
95
95
  `html_entity_decode()`s values on save so the DB stores **raw** text; `vocabulary/get.php`
96
96
  already escapes once at display. Do not re-add encoding on save.
97
97
 
98
+ **Per-model scoping is STRICT — each model owns its own copy (fixed 2026-07-29).**
99
+ `vocabulary/get.php` reads **strictly** `WHERE c_trueAiModelId = <selectedModel>` with **no
100
+ NULL fallback** — `c_trueAiModelId` is NOT "NULL = global". The per-model scoping (tools #5)
101
+ shipped with **no data backfill**: `Team/2026-07-27a` added `c_trueAiModelId` to
102
+ `TranscriptPromptTerms`/`TranscriptReplacements`/`TranscriptPromptTemplate` as `DEFAULT NULL`
103
+ and nothing populated it, so every existing row was NULL and selecting **any** model showed an
104
+ **empty page** (confirmed prod: 122 terms / 110 replacements / 1 template, all NULL). Fixed by
105
+ `dbchanges2 Team/2026-07-29a` (PR dbchanges2 #444): the existing NULL base rows are assigned to
106
+ **dev-core**, then that base is **replicated into every other active AiModel** (each an
107
+ independent, editable copy with its own fresh UUID). New models added later are **NOT
108
+ auto-seeded** — a known limitation, since a Team migration can't enumerate `Client_True`
109
+ models dynamically (see the separate-clusters gotcha in the
110
+ [worker2 pipeline doc](../../../2.0/apps/worker2/features/talos-transcript-ingestion.md#gotchas--known-issues)).
111
+
98
112
  ### `/talos/knowledge-bases` — KB list + inline rename
99
113
 
100
114
  The list/admin page (`get.php`) for Talos knowledge bases. Unlike KB Documents (whose KB
@@ -383,6 +397,16 @@ Refactor from a single hard-coded `development-team` KB to per-AI-model, data-dr
383
397
 
384
398
  ## Change history
385
399
 
400
+ - 2026-07-29 — **Fixed the empty Vocabulary page: the per-model scoping shipped with no data
401
+ backfill.** `vocabulary/get.php` reads strictly `WHERE c_trueAiModelId = <model>` (no NULL
402
+ fallback), but `Team/2026-07-27a` added the column `DEFAULT NULL` and nothing populated it —
403
+ so every model showed empty (prod: 122 terms / 110 replacements / 1 template, all NULL). New
404
+ migration `dbchanges2 Team/2026-07-29a - TranscriptVocabularyBackfillAllModels.sql` (PR
405
+ dbchanges2 #444) assigns the base NULL rows to **dev-core** then replicates that base into
406
+ **every active AiModel** as an independent editable copy (fresh UUID each). Decision: vocabulary
407
+ is **strict per-model, each model owns its own copy** (not NULL=global, not shared);
408
+ later-added models are **not auto-seeded** (Team migrations can't enumerate `Client_True`
409
+ models). (ajean)
386
410
  - 2026-07-29 — **Refactored KB Documents + Vocabulary to per-AI-model, data-driven scoping**
387
411
  (from the single hard-coded `development-team` KB). KB Documents is now an
388
412
  `AiModel → VectorIndex → documents` cascade; every document action is tenant-guarded
@@ -6,8 +6,8 @@ project: Database Changes
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-07-21
10
- owners: [jcardinal, mhammontree, bala]
9
+ updated: 2026-07-29
10
+ owners: [jcardinal, mhammontree, bala, ajean]
11
11
  files:
12
12
  - Core/
13
13
  - Client/
@@ -293,6 +293,44 @@ tables with a `UNIQUE(parentId, childId)` (e.g. `ContactAddresses`) can't be re-
293
293
  null-then-delete those. Then delete the non-keeper parents via the materialized double-nested
294
294
  subquery. Used in `Client_Prudential/2026-07-07 - Contact Dedup Merge.sql`.
295
295
 
296
+ ## Self-referencing INSERT...SELECT guard — wrap the existing-set subquery in a derived table
297
+
298
+ The same MySQL restriction (and a second, subtler hazard) applies when an
299
+ `INSERT ... SELECT` into a table must be made **idempotent / guarded per-key against that
300
+ SAME table** — e.g. "insert a base row for every model that doesn't already have one."
301
+
302
+ Reading the target table directly in the guard subquery fails two ways:
303
+
304
+ 1. **Error 1093** — "You can't specify target table 'X' for update in FROM clause," exactly as
305
+ with the self-referencing DELETE above.
306
+ 2. **A guard that flips mid-statement.** Even where MySQL allows it, a correlated
307
+ `NOT IN (SELECT … FROM <target>)` guard can be re-evaluated **as rows are inserted**, so the
308
+ set it checks against grows during the statement and the guard's answer changes partway
309
+ through — producing partial or duplicated inserts.
310
+
311
+ Wrap the existing-set subquery in a **derived table** (with `DISTINCT` or `LIMIT`) so MySQL
312
+ **materializes it once** against the pre-INSERT state:
313
+
314
+ ```sql
315
+ -- CORRECT — the derived table snapshots the "already present" set before any insert
316
+ INSERT INTO TranscriptPromptTerms (uuid, category, term, isActive, c_trueAiModelId)
317
+ SELECT UUID(), t.category, t.term, t.isActive, m.mid
318
+ FROM <base rows> t
319
+ CROSS JOIN <active model ids> m
320
+ WHERE m.mid NOT IN (
321
+ SELECT mid FROM (
322
+ SELECT DISTINCT c_trueAiModelId AS mid
323
+ FROM TranscriptPromptTerms
324
+ WHERE c_trueAiModelId IN (<active model ids>)
325
+ ) existing
326
+ );
327
+ ```
328
+
329
+ The extra `SELECT … FROM ( … ) existing` layer both dodges error 1093 and pins the guard to a
330
+ one-time snapshot, so a per-key idempotent backfill stays correct. Used in
331
+ `Team/2026-07-29a - TranscriptVocabularyBackfillAllModels.sql` to replicate the dev-core base
332
+ vocabulary into every other active AI model exactly once.
333
+
296
334
  ## Relationship to the rest of 2.0
297
335
 
298
336
  `dbchanges2` is registered as a **2.0 core repo** (`role: core` in `registry.json`) — it is
@@ -303,6 +341,11 @@ defined in `2.0/apps/_underscore/architecture.md`, and its change files create/a
303
341
  tables that `_Model_*` classes map to.
304
342
 
305
343
  ## Change history
344
+ - 2026-07-29 — Added *Self-referencing INSERT...SELECT guard — wrap the existing-set subquery in
345
+ a derived table*: an `INSERT...SELECT` guarded per-key against its own target must wrap the
346
+ existing-set subquery in a derived table (DISTINCT/LIMIT) so MySQL materializes it once —
347
+ avoids error 1093 and stops the guard flipping mid-statement as rows are inserted. First used
348
+ in `Team/2026-07-29a - TranscriptVocabularyBackfillAllModels.sql` (PR #444). (ajean)
306
349
  - 2026-07-21 — Added rule #6 to *Adding a new change*: never seed a `uuid` column with MySQL
307
350
  `UUID()` (time/MAC-based v1, violates the 2.0 v4-UUID standard); pre-generate a v4 UUID
308
351
  literal and hardcode it in `VALUES`. `Core/2026-05-08 … CronJobs_WorkerCleanup_insert` is a
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
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 |
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, worker2/composer.json |
6
6
  | [Deploy-Time Auto-Registration to the Shared ALB Target Group (non-production)](features/alb-target-group-auto-registration.md) | TOGA does **not** pay for EB-managed load-balancer registration, so an EB instance is normally **not** added to its environment's ALB target group — a fresh or | worker2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, worker2/ebs/register_instance_to_shared_application_load_balancer.php, worker2/.platform/hooks/prebuild/_shared/040-write-instance-id.sh, worker2/.platform/hooks/prebuild/_shared/041-write-region.sh, worker2/.platform/hooks/postdeploy/015_install_composer.sh, api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, api2/ebs/register_instance_to_shared_application_load_balancer.php |
7
7
  | [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 |
8
8
  | [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 |
@@ -29,9 +29,10 @@
29
29
  | [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
30
30
  | [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 |
31
31
  | [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 |
32
- | [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | > **DB-DRIVEN AI-MODEL ROUTING (2026-07-29).** Which knowledge base a transcript is cleaned > into is now decided by the **meeting organizer's "home" AI model** | worker2/Worker/Team/Transcripts.php, worker2/bin/sync-knowledge-bases.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Client_True/2026-07-27a - TranscriptAiModelRoutingColumns.sql, dbchanges2/Client_True/2026-07-27b - TranscriptAiModelRoutingData.sql, dbchanges2/Team/2026-07-27a - TranscriptVocabularyAiModelScope.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, dbchanges2/Team/2026-07-28a - TranscriptProcessingRetryAttempts.sql, dbchanges2/Team/2026-07-28b - TranscriptPromptTemplateConverseModel.sql, dbchanges2/Core/2026-07-28a - TeamsTranscriptRetryCron.sql |
32
+ | [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | > **DB-DRIVEN AI-MODEL ROUTING (2026-07-29).** Which knowledge base a transcript is cleaned > into is now decided by the **meeting organizer's "home" AI model** | worker2/Worker/Team/Transcripts.php, worker2/bin/sync-knowledge-bases.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Client_True/2026-07-27a - TranscriptAiModelRoutingColumns.sql, dbchanges2/Client_True/2026-07-27b - TranscriptAiModelRoutingData.sql, dbchanges2/Team/2026-07-27a - TranscriptVocabularyAiModelScope.sql, dbchanges2/Team/2026-07-29a - TranscriptVocabularyBackfillAllModels.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, dbchanges2/Team/2026-07-28a - TranscriptProcessingRetryAttempts.sql, dbchanges2/Team/2026-07-28b - TranscriptPromptTemplateConverseModel.sql, dbchanges2/Core/2026-07-28a - TeamsTranscriptRetryCron.sql |
33
33
  | [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) |
34
34
  | [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 |
35
35
  | [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 |
36
36
  | [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 |
37
+ | [PHP Runtime Upgrade on Elastic Beanstalk (worker2 8.3 → 8.5 + PhpSpreadsheet 1.x → 3.x)](workflows/php-runtime-upgrade-dependency-audit.md) | The procedure used to move worker2 from **PHP 8.3 to PHP 8.5** on Elastic Beanstalk, and the dependency work that had to land first. | worker2/composer.json, worker2/composer.lock, worker2/Worker/Team/Sprint.php, worker2/Worker/Client/TowFoundation/ProcessReceipts.php, worker2/Worker/Forecast/Import.php |
37
38
  | [Ticket → ClickUp Pseudocode Planning (Talos-grounded)](workflows/ticket-to-pseudocode-planning.md) | A repeatable procedure for turning a ClickUp ticket into a reviewed, formatted implementation plan posted back to the ticket's `📝 Pseudocode` custom field. | test/@dave/clickup_md2delta.js |
@@ -6,17 +6,19 @@ project: Worker
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-07-28
9
+ updated: 2026-07-29
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Controller/Index.php
13
13
  - worker2/Worker/
14
14
  - worker2/LambdaFunctions/
15
15
  - _underscore/Worker.php
16
+ - worker2/composer.json
16
17
  related:
17
18
  - ./features/creating-worker-actions.md
18
19
  - ./features/alb-target-group-auto-registration.md
19
20
  - ../_underscore/features/async-query-execution.md
21
+ - ./workflows/php-runtime-upgrade-dependency-audit.md
20
22
  ---
21
23
 
22
24
  ## Summary
@@ -32,6 +34,18 @@ processes background jobs. It's a `_underscore` 2.0 app (`index.php` is just
32
34
  **MySQL is the source of truth; SQS is delivery only.** All job state lives in
33
35
  `Core.WorkerJobs`. Every worker invocation reads from that table and writes its result back.
34
36
 
37
+ **PHP runtime baseline: 8.5 on both environments.** `_production` and `_sandbox-dev` both run
38
+ *PHP 8.5 on 64bit Amazon Linux 2023 / platform 4.13.4* (production was moved off 8.3 on
39
+ 2026-07-29). `vendor/` is gitignored, so a dependency that is incompatible with the environment's
40
+ PHP **only ever fails at EB deploy time**, never locally — and if two environments run different
41
+ PHP, identical code deploys to one and fails on the other. worker2's root `require` therefore
42
+ carries an explicit upper bound, `"php": ">=8.2 <8.6"`, so the next platform jump fails pointing
43
+ at *our* requirement instead of at a confusing transitive library error; `composer update` is run
44
+ on the **lowest** supported runtime so one lock is valid everywhere. Never silence such a failure
45
+ with a `config.platform.php` pin or a downgrade to a version that merely *permits* the new PHP —
46
+ that trades a loud deploy failure for silent runtime breakage. Full procedure:
47
+ [PHP Runtime Upgrade on Elastic Beanstalk](./workflows/php-runtime-upgrade-dependency-audit.md).
48
+
35
49
  **Production vs. non-production inbound differ.** The SQS/Lambda job pipeline above describes
36
50
  **production**. **Non-production worker2 environments do not incorporate SQS at all** — they exist
37
51
  for **manual invocation over HTTP** (hence `index.php` dispatch and `.platform/httpd/conf.d/`, and
@@ -44,6 +58,17 @@ passes through SQS** — a queue-based reproduction of a prod issue will not wor
44
58
  listener rules and security groups must restrict non-prod to internal/VPN sources, and the
45
59
  HTTP-triggered job endpoints must enforce auth.
46
60
 
61
+ **Critical rules:** MySQL is the source of truth and SQS is delivery only — all job state lives in
62
+ `Core.WorkerJobs`, so never treat a queue message as the record of a job. Every worker/webhook
63
+ endpoint must return **HTTP 200 on every path, including failure** (failures are recorded in
64
+ `WorkerJobs`): there is no DLQ and the visibility timeout is 3600s, so a single 500 becomes a
65
+ poison-message storm. Workers are `abstract class _Worker_<Name>` with `public static` entry
66
+ methods and are invoked **only** through `_Worker::runTask(action, parameters)` — never call a
67
+ worker method directly. This repo is PHP and Python only: never add a `.sql` file here, as all
68
+ schema changes and cron seeds belong to `dbchanges2`. Both environments run **PHP 8.5**, and the
69
+ repo's own root `require` (`">=8.2 <8.6"`) is the deliberate binding upper bound — never pin
70
+ `config.platform.php` or downgrade a package merely to make a deploy install on a newer runtime.
71
+
47
72
  ## AWS infrastructure
48
73
 
49
74
  | Component | Notes |
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-28
9
+ updated: 2026-07-29
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Platform/Cache.php
@@ -18,6 +18,7 @@ related:
18
18
  - ./creating-worker-actions.md
19
19
  - ../../_underscore/features/per-client-database-connections.md
20
20
  - ../architecture.md
21
+ - ../workflows/php-runtime-upgrade-dependency-audit.md
21
22
  ---
22
23
 
23
24
  ## Summary
@@ -123,6 +124,9 @@ environment.
123
124
  re-reads its own just-emptied tables from the query cache and mis-decides what is left.
124
125
 
125
126
  ## Change history
127
+ - 2026-07-29 — Verified `Platform/Cache/Truncate` working on **PHP 8.5** after worker2's EB
128
+ runtime upgrade from 8.3 (both environments now 8.5, AL2023 platform 4.13.4). No code change.
129
+ See [PHP Runtime Upgrade on Elastic Beanstalk](../workflows/php-runtime-upgrade-dependency-audit.md). (jcardinal)
126
130
  - 2026-07-28 — Added second action `_Worker_Platform_Cache::Truncate()` on the same class: manual /
127
131
  administrative, deliberately **not** scheduled. Removes the entire cross-client cache
128
132
  (`DELETE FROM Tables` + cascade, defensive `DELETE FROM Tables_Clients` for orphaned cursors, then
@@ -16,6 +16,7 @@ files:
16
16
  - dbchanges2/Client_True/2026-07-27a - TranscriptAiModelRoutingColumns.sql
17
17
  - dbchanges2/Client_True/2026-07-27b - TranscriptAiModelRoutingData.sql
18
18
  - dbchanges2/Team/2026-07-27a - TranscriptVocabularyAiModelScope.sql
19
+ - dbchanges2/Team/2026-07-29a - TranscriptVocabularyBackfillAllModels.sql
19
20
  - dbchanges2/Team/2026-06-30a
20
21
  - dbchanges2/Team/2026-06-30b
21
22
  - dbchanges2/Team/2026-06-30c
@@ -297,9 +298,17 @@ spacing (Bedrock allows only one in-flight ingestion per KB, so parallel syncs r
297
298
  index selection in `Process`).
298
299
  - **`Team.TranscriptPromptTerms` / `TranscriptReplacements` / `TranscriptPromptTemplate`** gained
299
300
  **`c_trueAiModelId`** (soft ref → `Client_True.AiModels.id`, dbchanges2 `Team/2026-07-27a`) so
300
- the transcript-cleanup vocabulary is scoped per AI model.
301
+ the transcript-cleanup vocabulary is scoped per AI model. **`c_trueAiModelId` is STRICT
302
+ per-model, NOT "NULL = global"** — each model owns its own independent copy. The `2026-07-27a`
303
+ add shipped `DEFAULT NULL` with no backfill, so `dbchanges2 Team/2026-07-29a` (PR #444) seeds
304
+ it: the base NULL rows → **dev-core**, then that base is replicated into every active AiModel
305
+ (fresh UUID per copy). Later-added models are **not** auto-seeded (a Team migration can't
306
+ enumerate `Client_True` models — see the separate-clusters gotcha).
301
307
  - The `2026-07-27b` data backfill maps existing dev emails → dev-core model, `ecastellucci` →
302
308
  operations model, and each VectorIndex's S3 target.
309
+ - **Active `Client_True.AiModels` (confirmed prod 2026-07-29):** id 1 `hr`, 2 `dev-core`
310
+ ("Talos DevCore"), 3 `one`, 4 `sales`, 6 `legal`, 7 `contact-center`, 8 `operations`,
311
+ 9 `executive`, 10 `sales-demo`; id 5 `tech-support` is **INACTIVE**.
303
312
  - **These `c_`-prefixed columns needed ZERO `_underscore`/model-layer changes** — see the
304
313
  framework note under Gotchas.
305
314
 
@@ -453,9 +462,37 @@ part of the ingestion loop** (raw reads removed). Credential values live only in
453
462
  - **Cross-ACCOUNT S3.** `togaiq` (us-east-1) approved/archive writes use the `[talos]` key;
454
463
  `CopyObject` across accounts is impossible, so archive is get(in-memory)+put. (The old
455
464
  toga-private cross-account read is no longer in the loop.)
465
+ - **The `Team` DB and `Client_True` live on SEPARATE clusters** (Team on the core cluster,
466
+ `Client_True` on the client cluster). Therefore a **Team migration cannot resolve a
467
+ `Client_True.AiModels.id` via a cross-DB subquery** — the two are not queryable in one
468
+ statement. This is why `c_trueAiModelId` is a **soft cross-DB ref with no FK** (nothing to
469
+ validate in-DB) and why any Team-side migration that needs a model id must **hardcode the id
470
+ literal** and document the resolving query + date in the file header (as
471
+ `Team/2026-07-29a` does). It is also why later-added models can't be auto-seeded with
472
+ vocabulary from a Team migration.
473
+ - **worker2 reads the cleanup vocabulary GLOBALLY — it is NOT yet scoped by model.**
474
+ `_Worker_Team_Transcripts::loadActiveTemplate()` / `loadPromptTerms()` /
475
+ `loadActiveReplacements()` filter only `WHERE isActive = 1`, with **no `c_trueAiModelId`
476
+ scope**. So today `c_trueAiModelId` is consumed **only by the tools `/talos/vocabulary` admin
477
+ UI**, not by transcript cleaning — the worker cleans every transcript with whatever the
478
+ **global** set returns. **FOLLOW-UP (not done):** scope these three loaders by the transcript's
479
+ `aiModelId`; once models have divergent vocabulary the global read will clean every transcript
480
+ with the merged/global set instead of the routed model's copy.
456
481
 
457
482
  ## Change history
458
483
 
484
+ - 2026-07-29 — **Backfilled the per-model transcript vocabulary and recorded three durable
485
+ facts.** `c_trueAiModelId` (added `DEFAULT NULL` by `Team/2026-07-27a`) was never populated, so
486
+ the tools `/talos/vocabulary` UI (which reads it strictly, no NULL fallback) showed empty for
487
+ every model. `dbchanges2 Team/2026-07-29a` (PR #444) assigns the base NULL rows to **dev-core**
488
+ and replicates that base into every active AiModel (independent copy, fresh UUID). Decision:
489
+ vocabulary is **strict per-model** (not NULL=global, not shared); later-added models are not
490
+ auto-seeded. Recorded: (1) **Team and `Client_True` are on separate clusters** → Team migrations
491
+ can't cross-DB-resolve a `Client_True.AiModels.id` and must hardcode the id literal; (2)
492
+ **worker2 reads vocabulary globally** (`isActive=1` only, no `c_trueAiModelId` scope) — the
493
+ column is consumed only by the tools UI today, with a follow-up to scope the worker's three
494
+ vocabulary loaders by the transcript's `aiModelId`; (3) the full active `Client_True.AiModels`
495
+ set (id 5 `tech-support` inactive). (ajean)
459
496
  - 2026-07-29 — **DB-driven per-user AI-model routing.** `Export` now polls
460
497
  `Client_True.Users WHERE c_transcriptAiModelId IS NOT NULL AND isActive=1` (the `[teams]`
461
498
  organizer id/email lists were removed) and threads `aiModelId` into each `Process` job;
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-24
9
+ updated: 2026-07-29
10
10
  owners: ["jcardinal", "kyalamarthi"]
11
11
  files:
12
12
  - worker2/Worker/Team/Sprint.php
@@ -17,6 +17,7 @@ related:
17
17
  - ./creating-worker-actions.md
18
18
  - ./clickup-work-type-automation.md
19
19
  - ./clickup-project-routing.md
20
+ - ../workflows/php-runtime-upgrade-dependency-audit.md
20
21
  ---
21
22
 
22
23
  ## Summary
@@ -222,6 +223,13 @@ backfills `workTypeAtLock` the first time a task is seen).
222
223
 
223
224
  ## Gotchas
224
225
 
226
+ - **PhpSpreadsheet coordinates are ARRAYS now (3.x).** All `*ByColumnAndRow` methods were
227
+ removed in PhpSpreadsheet 2.0; `Sprint.php` had **828** of them (the bulk of the repo's 856)
228
+ and they are all now the array form — `->setCellValue([$col, $row], $v)`,
229
+ `->getStyle([$c1, $r1, $c2, $r2])`, `->mergeCells([$c1, $r1, $c2, $r2])`. Do not reintroduce
230
+ the old signatures when copying older report code into this file; the full mapping and the
231
+ safe transform method are in
232
+ [PHP Runtime Upgrade on Elastic Beanstalk](../workflows/php-runtime-upgrade-dependency-audit.md#step-5--migrate-the-call-sites-bycolumnandrow-removed-in-20).
225
233
  - **Single source file, huge methods.** `SprintEnd` alone is ~2,700 lines; `SprintLock` and
226
234
  `SprintDaily` are each ~1,000. Almost all report layout/formatting is inline. Treat the
227
235
  method boundaries (and the constants block at the top) as the map.
@@ -363,6 +371,12 @@ middleware defaults every tile/chart endpoint to it when no `?sprint=` is suppli
363
371
 
364
372
  ## Change history
365
373
 
374
+ - 2026-07-29 — Migrated all **828** `*ByColumnAndRow` call sites in `Sprint.php` to
375
+ PhpSpreadsheet's array-coordinate API as part of the phpspreadsheet 1.30.2 → 3.10.7 upgrade
376
+ required by the worker2 PHP 8.5 runtime move. No report behavior change intended; the
377
+ array-range forms of `getStyle`/`mergeCells` were verified empirically against 3.10.7
378
+ (including an Xlsx save/load round-trip). See
379
+ [PHP Runtime Upgrade on Elastic Beanstalk](../workflows/php-runtime-upgrade-dependency-audit.md). (jcardinal)
366
380
  - 2026-07-24 — Productionized the dashboard: the metric definitions above are now served by six
367
381
  api2 Record Scripts on `_Model_Team_Sprint` (see
368
382
  [Sprint Dashboard API](../../api2/features/sprint-dashboard-api.md)), retiring the Express
@@ -0,0 +1,196 @@
1
+ ---
2
+ title: PHP Runtime Upgrade on Elastic Beanstalk (worker2 8.3 → 8.5 + PhpSpreadsheet 1.x → 3.x)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-07-29
10
+ owners: [jcardinal]
11
+ files:
12
+ - worker2/composer.json
13
+ - worker2/composer.lock
14
+ - worker2/Worker/Team/Sprint.php
15
+ - worker2/Worker/Client/TowFoundation/ProcessReceipts.php
16
+ - worker2/Worker/Forecast/Import.php
17
+ related:
18
+ - ../architecture.md
19
+ - ../features/team-sprint-management.md
20
+ - ../features/platform-cache-cleanup.md
21
+ - ../../../1.0/apps/tools/workflows/deploy-to-elastic-beanstalk-al2023.md
22
+ - ../../../clients/tow-foundation/features/receipt-processing.md
23
+ ---
24
+
25
+ ## Summary
26
+
27
+ The procedure used to move worker2 from **PHP 8.3 to PHP 8.5** on Elastic Beanstalk, and the
28
+ dependency work that had to land first. It is written as a reusable checklist because every
29
+ TOGA repo will face the same sequence as EB platform branches advance.
30
+
31
+ Trigger: an EB `app-deploy` to `_sandbox-dev` failed at **"Install composer dependencies"** with
32
+
33
+ ```
34
+ Your lock file does not contain a compatible set of packages...
35
+ phpoffice/phpspreadsheet is locked to version 1.30.2 ... requires php >=7.4.0 <8.5.0
36
+ -> your php version (8.5.8) does not satisfy that requirement.
37
+ ```
38
+
39
+ Outcome: worker2 now runs **phpspreadsheet 3.10.7** on a single lock valid on both 8.3 and 8.5,
40
+ and **both** EB environments (`_production` and `_sandbox-dev`) run PHP 8.5 on Amazon Linux 2023
41
+ platform 4.13.4.
42
+
43
+ ## Step 1 — Diagnose the environment, not the branch
44
+
45
+ `_production` and `_sandbox-dev` were **byte-identical** in `composer.json`, `composer.lock`
46
+ (both phpspreadsheet 1.30.2), and all 21 `.platform/` + `.ebextensions/` files. The only
47
+ difference was the EB environment's **PHP runtime**:
48
+
49
+ | Environment | Platform |
50
+ |---|---|
51
+ | `_production` | PHP **8.3** running on 64bit Amazon Linux 2023/4.13.4 |
52
+ | `_sandbox-dev` | PHP **8.5** running on 64bit Amazon Linux 2023/4.13.4 |
53
+
54
+ Same platform branch, different PHP. The developer deleted and re-branched `_sandbox-dev` from
55
+ `_production` and the failure **reproduced identically** — re-branching cannot fix an
56
+ environment/runtime mismatch. When a deploy fails on one environment and not another with
57
+ identical code, diff the **environment**.
58
+
59
+ `vendor/` is gitignored in worker2, so this class of failure only ever surfaces at EB deploy
60
+ time, never locally.
61
+
62
+ ## Step 2 — Audit the WHOLE lock for upper PHP bounds
63
+
64
+ Composer reports only **"Problem 1"**. Before sizing a runtime upgrade, walk every locked
65
+ package's `require.php` for an upper bound. Of worker2's **36** locked packages, exactly **one**
66
+ carried a bound below 8.5 — `phpoffice/phpspreadsheet`. Everything else
67
+ (`aws/aws-sdk-php`, `sentry/sentry`, guzzle, `phpseclib`, `phpmailer`, `symfony/*`,
68
+ `markbaker/*`) was open-ended or 8.5-inclusive. Do not fix the first reported package and
69
+ assume you are done.
70
+
71
+ ## Step 3 — Read the library's version-line policy, not just its constraint
72
+
73
+ PhpSpreadsheet lines, all released 2026-07-12 (verified on Packagist + GitHub Releases):
74
+
75
+ | Line | `require.php` | PHP 8.5? |
76
+ |---|---|---|
77
+ | 1.30.6 | `>=7.4.0 <8.5.0` | **deliberately excluded** (1.30.1+ tightened from 1.30.0) |
78
+ | 2.4.7 | `>=8.1.0 <8.6.0` | yes, but capped at 8.6 |
79
+ | 3.10.7 | `^8.1` | yes, no upper cap |
80
+ | 5.9.0 | `^8.2` | current line |
81
+
82
+ **Trap:** 1.30.0's `^7.4 || ^8.0` technically *permits* 8.5, and maintainers narrowed 1.30.1+
83
+ to `<8.5.0` **on purpose**. So downgrading to 1.30.0 — or pinning `config.platform.php` — makes
84
+ `composer install` succeed while the code runs on an explicitly unsupported runtime. That
85
+ converts a loud deploy failure into silent runtime breakage. There is a composer-only way to
86
+ make 1.x *install* on both 8.3 and 8.5; there is **no** composer-only way to make it *safe*.
87
+
88
+ ## Step 4 — Pick the target: `^3.10`, not 2.4 and not 5.x
89
+
90
+ - **`^3.10`** (resolved 3.10.7) — supports 8.3 and 8.5 with **no 8.6 cap**, and its breaking
91
+ surface is limited to the 2.0 removals (Step 5).
92
+ - **2.4.7** rejected: its `<8.6.0` cap would force a repeat migration at PHP 8.6.
93
+ - **5.x** rejected: adds 4.0 behavioral changes (DataValidation stored per-worksheet not
94
+ per-cell; CSV reader stops auto-detecting Mac line endings; HTML writer emits `TRUE`/`FALSE`
95
+ instead of `1`/empty string; Xlsx writer `forceFullCalc` default flips) and 5.0 changes
96
+ (external images need `setAllowExternalImages(true)`; `DefaultValueBinder` binds integers
97
+ >15 digits as strings) — real risk to report output for **zero gain** on the 8.5 question.
98
+
99
+ Also add an explicit **upper** bound to the repo's own root require:
100
+
101
+ ```json
102
+ "php": ">=8.2 <8.6"
103
+ ```
104
+
105
+ 3.10.7 itself allows up to `<9.0`, so the repo bound is the deliberate binding constraint —
106
+ when a future EB platform lands PHP 8.6, composer fails pointing at **worker2's own**
107
+ requirement rather than at a confusing transitive library error.
108
+
109
+ **Resolve the lock on the lowest runtime you must support.** The lock was produced locally on
110
+ PHP 8.2.12 (below both 8.3 and 8.5), which yields the most **conservative** package set — valid
111
+ on 8.3 and 8.5 alike, so no `config.platform.php` pin was needed.
112
+
113
+ `ezyang/htmlpurifier` is **dropped** as a transitive dependency going 1.x → 3.x. Safe here
114
+ (zero direct usage in worker2) — check it in any other repo doing this upgrade.
115
+
116
+ ## Step 5 — Migrate the call sites (`*ByColumnAndRow` removed in 2.0)
117
+
118
+ PhpSpreadsheet 2.0 removed **all** deprecated `*ByColumnAndRow` methods ("All deprecated things
119
+ have been removed"). **856** call sites migrated across 3 files: `Worker/Team/Sprint.php` (828),
120
+ `Worker/Client/TowFoundation/ProcessReceipts.php` (20), `Worker/Forecast/Import.php` (8).
121
+
122
+ The mapping — coordinates become an **array**:
123
+
124
+ ```php
125
+ ->setCellValueByColumnAndRow($c, $r, $v) => ->setCellValue([$c, $r], $v)
126
+ ->setCellValueExplicitByColumnAndRow($c, $r, $v, $t) => ->setCellValueExplicit([$c, $r], $v, $t)
127
+ ->getCellByColumnAndRow($c, $r) => ->getCell([$c, $r])
128
+ ->getStyleByColumnAndRow($c1, $r1, $c2, $r2) => ->getStyle([$c1, $r1, $c2, $r2])
129
+ ->mergeCellsByColumnAndRow($c1, $r1, $c2, $r2) => ->mergeCells([$c1, $r1, $c2, $r2])
130
+ ```
131
+
132
+ Per-method counts: `setCellValue` 447, `getStyle` 341, `mergeCells` 53, `getCell` 14,
133
+ `setCellValueExplicit` 1.
134
+
135
+ **Method gotcha:** a naive regex breaks on nested calls and on commas inside string literals.
136
+ The migration used a paren/quote/comment-aware transform that splits only **top-level** commas
137
+ and **validates each call's arity** against an expected set, skipping and reporting anomalies
138
+ rather than silently rewriting. Result: 856/856 converted, 0 skipped, 0 anomalies, `php -l`
139
+ clean on all three files, zero residual `ByColumnAndRow` in the repo.
140
+
141
+ **Verified empirically against installed 3.10.7** (not assumed from docs): 4-element
142
+ `getStyle([1,1,3,1])` really does style the full range (A1 and C1 both bold), the 2-element
143
+ `getStyle([2,2])` targets a single cell, `mergeCells([1,5,3,5])` yields `A5:C5`, and both styles
144
+ and merges survive an Xlsx save/load round-trip.
145
+
146
+ ## Step 6 — Bump the runtime as its own isolated change
147
+
148
+ The safe sequence, and the reason it is safe:
149
+
150
+ 1. Make the code run on **both** runtimes first — one lock valid on 8.3 and 8.5. `_production`
151
+ therefore stayed deployable to the old 8.3 environment throughout the transition.
152
+ 2. Prove it on `_sandbox-dev` (already 8.5).
153
+ 3. **Then** bump `_production`'s EB platform 8.3 → 8.5 as an isolated, independently
154
+ revertible change.
155
+
156
+ Never a combined code+runtime jump — if it breaks you cannot tell which half did it. The
157
+ alternative of downgrading `_sandbox-dev` to 8.3 was explicitly declined: it hides the problem
158
+ that production will hit next.
159
+
160
+ Post-upgrade verification: `Platform/Cache/Truncate` confirmed working on PHP 8.5 (see
161
+ [Platform Cache Cleanup](../features/platform-cache-cleanup.md)).
162
+
163
+ ## Open risk — pre-existing composer advisories (own ticket)
164
+
165
+ **Unrelated to this work and not introduced by it.** `composer audit` reports **16
166
+ medium-severity** advisories across 4 packages, principally:
167
+
168
+ | Package | Locked | Fixed in | Advisories |
169
+ |---|---|---|---|
170
+ | `guzzlehttp/guzzle` | 7.10.0 | >= 7.15.1 | CVE-2026-59883, CVE-2026-55767, CVE-2026-55568 |
171
+ | `guzzlehttp/psr7` | 2.9.0 | >= 2.12.3 | CVE-2026-59882, CVE-2026-55766, CVE-2026-49214 |
172
+
173
+ Plus cookie-scope / `Referer` / `Proxy-Authorization` leakage advisories. Deliberately left out
174
+ of the 8.5 branch to keep it a single-concern change — **needs its own ticket.**
175
+
176
+ ## Key rules
177
+
178
+ - **Identical code failing on one environment only = diff the environment.** Re-branching never
179
+ fixes a runtime mismatch.
180
+ - **Audit every locked package's PHP bound**, not just the one composer names.
181
+ - **Never pin `config.platform.php` or downgrade to a version that merely *permits* the new
182
+ PHP** to silence a deploy error — that trades a loud failure for a silent one.
183
+ - **Run `composer update` on the lowest runtime you must support** so one lock covers all
184
+ environments.
185
+ - **Carry an explicit upper PHP bound in the repo's own root `require`** so the next runtime
186
+ jump fails pointing at us.
187
+ - **Code first (dual-runtime), runtime second (isolated).**
188
+
189
+ ## Change history
190
+ - 2026-07-29 — Initial capture. worker2 upgraded phpspreadsheet 1.30.2 → 3.10.7 (`^3.10`) and
191
+ root require `>=8.2` → `>=8.2 <8.6`; 856 `*ByColumnAndRow` call sites migrated to the array
192
+ coordinate form across `Team/Sprint.php`, `Client/TowFoundation/ProcessReceipts.php`, and
193
+ `Forecast/Import.php`; `ezyang/htmlpurifier` dropped as a transitive dep. Production EB
194
+ environment then bumped PHP 8.3 → 8.5 (AL2023 platform 4.13.4) as an isolated change — both
195
+ environments now 8.5. 16 pre-existing medium composer advisories (guzzle 7.10.0, psr7 2.9.0)
196
+ flagged for a separate ticket. (jcardinal)
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
19
19
  ## 2.0 framework
20
20
 
21
21
  - **_underscore** (_Underscore) _(framework core)_ — 40 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
- - **worker2** (Worker) — 34 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
+ - **worker2** (Worker) — 35 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 19 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
24
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
25
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
@@ -5,8 +5,8 @@ project: Worker
5
5
  client: tow-foundation
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-07-17
9
- owners: ["rgirish"]
8
+ updated: 2026-07-29
9
+ owners: ["rgirish", "jcardinal"]
10
10
  files:
11
11
  - worker2/Worker/Client/TowFoundation.php
12
12
  - worker2/Worker/Client/TowFoundation/ProcessReceipts.php
@@ -14,6 +14,7 @@ files:
14
14
  - worker2/Worker/Client/TowFoundation/TowFoundationCategories.php
15
15
  related:
16
16
  - clients/tow-foundation/profile.md
17
+ - ../../../2.0/apps/worker2/workflows/php-runtime-upgrade-dependency-audit.md
17
18
  ---
18
19
 
19
20
  ## Summary
@@ -353,6 +354,13 @@ Fatal errors send only to `NOTIFY_EMAIL_DEV` (no CC/BCC).
353
354
 
354
355
  ## Gotchas / known issues
355
356
 
357
+ - **PhpSpreadsheet coordinates are ARRAYS now (3.x).** worker2 runs phpspreadsheet 3.10.7 on
358
+ PHP 8.5; every `*ByColumnAndRow` method was removed in PhpSpreadsheet 2.0. The 20 call sites
359
+ in `ProcessReceipts.php` use the array form (`->setCellValue([$col, $row], $v)`,
360
+ `->getStyle([$c1, $r1, $c2, $r2])`). Do not paste older QB-Excel layout code using the old
361
+ signatures. See
362
+ [PHP Runtime Upgrade on Elastic Beanstalk](../../../2.0/apps/worker2/workflows/php-runtime-upgrade-dependency-audit.md).
363
+
356
364
  - **`$year` variable shadowing** — `Run()` uses `$year` as both a filter parameter and a
357
365
  loop variable (line ~159: `$year = $receipt['year']`). After the loop `$year` holds the
358
366
  last receipt's year, not the original filter value. Harmless now but fragile if the loop
@@ -465,6 +473,10 @@ Fatal errors send only to `NOTIFY_EMAIL_DEV` (no CC/BCC).
465
473
 
466
474
  ## Change history
467
475
 
476
+ - 2026-07-29 — Migrated the **20** `*ByColumnAndRow` call sites in `ProcessReceipts.php` to
477
+ PhpSpreadsheet's array-coordinate API (phpspreadsheet 1.30.2 → 3.10.7, required by worker2's
478
+ PHP 8.3 → 8.5 runtime move). No intended change to the QB Excel output. (jcardinal)
479
+
468
480
  - 2026-07-17 — **Memo fidelity overhaul + in-sheet Review column + cycle-aware MoveBack + offline export.** All in `ProcessReceipts.php`. (1) FIXED: the AI was paraphrasing/inventing the payment memo — rewrote the `extractReceiptData()` prompt + `payment_memo` schema to transcribe a human-added note **verbatim** and return **`null`** (never infer) when none exists; removed the old "infer a memo" instruction (client-reported: `"Lucy Ball of Lone Pine Foundation"`→`"LB LPFoundation"`, and a fabricated Asana memo). (2) FIXED: statement `Notes` override silently failed when AI OCR'd the wrong **year** — `matchStatementCandidate()` now falls back to a month+day (year-ignoring) date match so the authoritative Notes memo still wins despite year drift. (3) BUILT: in-sheet `Review` column (`buildExcelRow`+`generateExcel`) flags `"VERIFY AMOUNT (no matching charge on statement)"` (e.g. Ololo Safari KES `$172,872` OCR'd vs USD `$1,340.09`) and `"ADD MEMO (no note found on receipt)"` — the client reviews the Excel, not the email, so unverified/blank rows must surface in the sheet. (4) BUILT: prompt now scans the **entire page** (margins/corners/header/beside address) for visually-distinct human notes (missed a highlighted Optimum "Telephone & Internet" box, a red Garelick "Board Meeting … Remaining Deposit", an orange Lucid "IT Software License"). (5) BUILT: strip a leading 1–2-caps-plus-colon initials tag (`"RF:"`) from the memo — enforced in the prompt **and** as a deterministic `stripInitialsPrefix()` backstop that leaves 3+-letter prefixes (`"Postage:"`) alone. (6) BUILT: cycle-aware `MoveBack($scopeToCycle=true)` + `cycleDateWindow()`/`archivedFileDate()` — restore now moves only files whose archived-filename date falls in the cycle window (Amex = day-after-prior-3rd → this-3rd), instead of dumping the whole Archive into one cycle. (7) BUILT: read-only `DownloadCycleFiles(person, billingCycle, destDir, year, match, includeArchive)` — downloads a person's cycle receipts + statement Excel(s) to a local dir (constrained inside `__DIR__`), walking Archive via `collectPersonArchiveFiles()`, falling back to all statements when none matches. (8) DISCOVERED: local `Run()` always ends in a non-fatal `Unknown database 'logs_towfoundation'` at `_Email->send()` (Excel is generated + uploaded before that step, so work succeeds); Nadia Alia's statements are raw Amex "Transaction Details" exports with no `Notes` column (blank `ADD MEMO` memos are correct for her; no statement covers `06-04..07-03`); Talos is currently pointed at beta (`api.beta.togaiq.com`). Code changes remain **uncommitted** on the `_production` working tree (`fix/towfoundation-memo-verbatim` branch proposed, not yet created). (rgirish)
469
481
  - 2026-07-13 — **Read-only Preview action + Ligia map-key fix + July cycle findings.** (1) BUILT: `Preview(?year, ?person, ?billingCycle): string` — a true dry-run that reuses `walkReceiptsFolder()` and the same filters as `Run()` but only tallies (downloads/extracts/moves nothing, sends no email), returning pretty JSON with per-person receipt counts, cycles, fileTypes, up-to-3 sampleFiles, and hasStatement, plus a softer `personsWithNoReceipts` list. Read `persons[]` as the authoritative empty signal; `personsWithNoReceipts` is derived from statement-only folders and also surfaces stray top-level folders (e.g. "Processed") as cosmetic noise. (2) FIXED: `CLASS_MAP`/`PAYMENT_ACCOUNT_MAP` keys for Ligia Marroquin Soto were `"Ligia Marroquin"` (no "Soto") — never hit against the folder-derived `"Ligia Marroquin Soto"`, so her Class + Payment Account silently blanked (both maps fall back to `''` with no warning). Renamed keys to `"Ligia Marroquin Soto"` (Class ⇒ `Administration:Operations`, Payment Account ⇒ `AMEX Open Credit Card:Ligia Marroquin-Soto Amex CC`); broadened the person-folder-normalization gotcha — map keys MUST equal `folder − " CC receipts"`. (3) DISCOVERED: July Amex `"07-03-2026"` has 72 receipts across 9 people (Nadia Alia 43 …); Diane Sierpina, Brent Peterkin, Susan Ransden empty for July; Ryan Farrell + Magdalena Minta have July receipts but no statement → AI-inferred memos; **Michael Zuber Zander** has a receipts folder but is in neither map (and zero receipts) → left unmapped pending client-provided QB Class + Payment Account. Also recorded that SharePoint creds live in the `[sharepoint_towfoundation]` Config ini section, read via `_Config::sharepoint_towfoundation()`. (rgirish)
470
482
  - 2026-07-13 — **Per-person QB Excel location fix + two production findings.** (1) FIXED: `uploadExcelToSharePoint()` now writes each person's generated QB Excel to that person's own `Credit Card Receipts/{Person} CC receipts/{Year}/3. QB Excel/` subfolder (matching the docblock and client expectation) instead of a single shared root `3. QB Excel/`; signature is now `(accessToken, driveId, personFolderName, year, excelName, tmpFile)`, fed by a `$personFolders` map populated first-write-wins in Pass 1 with an `error_log` fallback to `{personName}/{currentYear}`. (2) DISCOVERED (production-critical gotcha): the statement-Notes lookup is EXACT string equality between the statement filename (`cycleKey`) and the receipt's billing-cycle FOLDER name — no date/fuzzy fallback — so a card whose folder has no identically-named statement silently skips the authoritative Notes memo and falls back to AI inference with zero signal (confirmed for Ryan Farrell's Mastercard folder vs. an Amex-named statement); also noted `computeRefNo()` hard-codes cycle-end day `03`, giving Mastercard rows a `…03…` Ref No. (pre-existing, unchanged). (3) DECIDED: monthly production procedure is two explicit per-card `Run` invocations (Amex + Ryan Farrell's Mastercard), NOT a "MM-YYYY month sweep" — the sweep prototype was deliberately reverted because `PAYMENT_ACCOUNT_MAP` is keyed per-person not per-card and would misattribute the QB Payment Account for anyone holding two cards in a swept month; recorded production-safety facts (no dry-run — `limit` still archives + emails; file moves reversible via `MoveBack` but email is not recallable; recipients are compile-time `NOTIFY_*` constants). (rgirish)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.474",
3
+ "version": "1.0.476",
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",