toga-ai 1.0.663 → 1.0.665

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: architecture
8
8
  status: active
9
- updated: 2026-08-13
9
+ updated: 2026-08-26
10
10
  owners: ["jcardinal", "rgirish", "mhammontree"]
11
11
  files:
12
12
  - _underscore/_underscore.php
@@ -369,6 +369,7 @@ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentNa
369
369
  pin is `Addresses.isValidated` and `Entitlements.serviceAddressId` both-or-neither. Relevant code:
370
370
  `_underscore/Model/Rate/Entitlement.php`, `api2/Component/Api/V2/V2.php::getFullModelData`.)
371
371
  - **`_Database::register()` auto-starts a lazy transaction (since Apr 2 2026, commit `fa7835ed`).** Any code that calls `register()` and then writes to that DB must call `_Database::transactionCommit()` before the request ends — otherwise MySQL silently rolls back all writes when the connection closes. Lazy transactions only materialise on the first write, so read-only callers are unaffected. See `_underscore/Database.php:48`. First discovered when Rate SAML user provisioning silently discarded all new user INSERTs (Jun 2026).
372
+ - **`_Model::load()` with a collapsed WHERE clause silently full-table-scans → OOM (guarded 2026-08-26, `Model.php`).** `_Model::search()` builds `SELECT <pk> FROM <table> [WHERE …] ORDER BY <pk>` and emits the WHERE **only if at least one search term survives**. `buildSqlFieldValue()` **drops** a term (returns false) when a set field's value is `null` and its type is a plain `FIELD_CHAR` — i.e. not one of the special-cased types (`AUTOINCREMENT`, `DATETIME_CREATED`, `DATETIME_UPDATED`, `SQL`, `STORAGE`, `CHAR_UUID`). If the only set search field collapses this way, the WHERE vanishes and `search()` selects the **whole table**, constructing one model per row → memory exhaustion on a large table. Because `load()` calls `search()` first and only then checks `count() == 1`, it **OOMs building the result array before it ever counts** — the empty-WHERE case never reaches the "not exactly one" branch. **`load()` means "find exactly one by criteria"; an empty WHERE is always a bug, never a full-table scan.** **Fix (commit `0b944a51`, `_production`):** `load()` now guards at the top — if no set search field yields a WHERE term (via `buildSqlFieldValue`), it `error_log`s and returns `false` (or throws when `throwExceptionIfNotFound`) instead of scanning. `search()` is unchanged, so intentional "list all rows" callers of `search()` are unaffected. **Triage:** a PHP OOM stack trace of `Model.php search() → __construct` that is *shallow* (a single search→construct, not deeply repeated) is a runaway result set from a collapsed WHERE, **not** recursion — confirm by checking whether the single set search field is a `FIELD_CHAR` whose value is null. (Root cause of a recurring prod OOM: `Logs.Issue` ref `2L` (#38), first seen 2026-08-03, 136 occurrences, trace `Model.php search()→__construct`.)
372
373
  - **PHP "Unclosed '{'" parse errors report a MISLEADING line number.** When a `.php` file loaded by the autoloader (`Loader.php`) has a dropped/unbalanced brace, PHP reports `Unclosed '{' on line N` where N is the **outermost `class X {` line** and fails at EOF — NOT at the true location of the missing `}`. Worse, because the file loads lazily via the SPL autoloader, the runtime trace points at the **caller** that triggered the autoload (e.g. a `new _Email()` call site), not the broken file. **Triage rule:** for an "Unclosed '{'" error, the real culprit is a missing `}` somewhere between the reported line and EOF of the file that failed to load — run `php -l <file>` (it reports the EOF line) and scan the whole file. **Merge-conflict resolutions are a common source of a single dropped brace** — review the entire merge, not just the one file the error appears to name. (First hit: production 500 EO-1, Jul 2026 — a `}` dropped from `_Email::send()` during merge `685e4a14` surfaced as a trace pointing at the `new _Email()` caller.)
373
374
 
374
375
  - **A controller exception used to be silently swallowed by `Route.php`, then masked as a view error
@@ -395,6 +396,7 @@ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentNa
395
396
  stack trace) to API consumers. Not yet done — tracked as a follow-up.
396
397
 
397
398
  ## Change history
399
+ - 2026-08-26 — Documented and guarded the `_Model::load()` collapsed-WHERE full-table-scan OOM: `search()` omits the WHERE when the only set field is a null `FIELD_CHAR` (dropped by `buildSqlFieldValue`), so `load()` scans the whole table and OOMs building models before its `count()==1` check; `load()` now returns false / throws instead of scanning (commit `0b944a51`). Added the shallow `search()→__construct` OOM-trace triage rule. Root cause of prod OOM `Logs.Issue` ref `2L`, 136 occurrences since 2026-08-03. (jcardinal)
398
400
  - 2026-07-27 — Added the gotcha that **in-request reads cannot see the transaction's own uncommitted nested writes** — a `postPost` interceptor must resolve just-created records from the `getFullModelData`-hydrated `$payload`, not a same-request `_Query` read-back (even on the write host). Found on the Rate WH service-address pin (TRUE-79533). (mhammontree)
399
401
  - 2026-06-11 — Documented lazy transaction gotcha in `_Database::register()` (rgirish)
400
402
  - 2026-06-25 — Added the Surface platform UI presentation/configuration layer (DB-driven UI config replacing `Page::meta()`, CTO-reviewed AGREE-WITH-ADJUSTMENTS) (jcardinal)
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-08-25
9
+ updated: 2026-08-26
10
10
  owners: [jcardinal, bala, mhammontree, dfranks]
11
11
  files:
12
12
  - api2/Controller/Index.php
@@ -131,6 +131,8 @@ One ~2,000-line `execute()` then `processRoutePairs()`:
131
131
  > was at fault. The **sibling CloudWatch JSONL file-append logger** (`Controller/Index.php` ~L433) is
132
132
  > **intentionally left bare-ms** — a JSONL append has no UNIQUE key and cannot collide.
133
133
 
134
+ > **A present-but-null client `transactionId` triggered the `_Model::load()` full-table scan on `Logs.Api` → memory exhaustion on `/v2/auth/public` (FIXED 2026-08-26).** The inbound request-logger's duplicate-`transactionId` check does `$existingLog = new $logsApiModelName(); $existingLog->transactionId = $transactionId; if ($existingLog->load()) {…}`. The `Logs Api` models declare `transactionId = FIELD_CHAR` (confirmed in both `_Model_Client_Logs_Api` and `_Model_Core_Logs_Api`), so when `$transactionId` is **null**, `load()` drops the only search term, the WHERE collapses, and it **full-table-scans the enormous prod `Logs.Api` table → OOM** (the `_underscore` `Model.php` collapsed-WHERE mechanism — see [_underscore architecture — Gotchas](../_underscore/architecture.md)). **Trigger:** a caller posting a *present-but-null* transactionId (e.g. `{"transactionId": null}`). V2 only auto-generates a uuid when the key is **absent** (`if (!\_Page::isSet('transactionId'))`, ~L543); a *present* null falls through to `$transactionId = \_Page::get('transactionId')` = null. **Fix (commit `36ef31f`, `_production`):** (a) after reading `transactionId` in the auth block (~L553), coerce null/empty → `_String::generateUuid()`; (b) guard the final request-logger dup-check (~L2277) to skip `load()` when `transactionId` is null/empty. **Triage:** don't assume an OOM/incident was caused by the latest deploy — this predated the suspected afternoon deploy by ~3 weeks; check `Logs.Issue.dtCreated` (first-occurrence) and `Logs.Api` volume/response-code trends first. (Recall Known issue #6: api2 clones `_underscore` from the moving `_<ENVIRONMENT>` branch, so framework behavior can shift under a stable api2 commit.)
135
+
134
136
  ## CRUD engine — `processRoutePairs()`
135
137
 
136
138
  **Metadata-driven** — routes/models/fields/permissions come from Core/Client DB tables, not
@@ -325,6 +327,7 @@ they are the known sharp edges. Do not re-discover these from scratch.
325
327
  single-caller branches in `V2.php` as unverified until exercised directly.
326
328
 
327
329
  ## Change history
330
+ - 2026-08-26 — Documented and fixed a `/v2/auth/public` OOM: a present-but-null client `transactionId` (`{"transactionId": null}`) flowed into the request-logger's `_Model::load()` dup-check; because `Logs.Api.transactionId` is `FIELD_CHAR`, a null value collapses the WHERE and full-table-scans prod `Logs.Api` → memory exhaustion (the `_underscore` collapsed-WHERE mechanism). V2 only auto-uuids an *absent* transactionId (~L543), so a present null fell through (~L553 read → null). Fix (commit `36ef31f`): coerce null/empty transactionId → uuid in the auth block, and skip `load()` in the final logger dup-check (~L2277) when null/empty. Added the "don't blame the latest deploy — check first-occurrence" triage note. (jcardinal)
328
331
  - 2026-08-25 — **Resolved the auto-generated `Api.transactionId` 1062 collision** (was the 2026-07-28
329
332
  gotcha): the two DB-insert log sites now uuid the log-row `transactionId` (`_String::generateUuid()`)
330
333
  — `V2.php` `internalApiRequest()` logger (~L2370) and `Controller/Index.php` error-recovery logger
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.663",
3
+ "version": "1.0.665",
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",
@@ -28,6 +28,23 @@ pushes that skill describes. It does not authorize publishing anything else in t
28
28
 
29
29
  Force-pushing is never automatic in any repo, including the knowledge repo.
30
30
 
31
+ ## Claude never creates git worktrees
32
+
33
+ Claude **must never** run `git worktree add` (or otherwise create a git worktree) in any
34
+ repo, for any developer, under any circumstances — **including** to isolate a hotfix from the
35
+ branch the developer currently has checked out. There is no approved use of worktrees.
36
+
37
+ Worktrees strand the developer: their editor and terminal end up pinned inside the worktree
38
+ directory (`C:\WWW\_hotfix\...`) with no obvious way back to their normal checkout, and
39
+ cleaning it up needs worktree-specific commands (`git worktree remove`) most developers do
40
+ not expect.
41
+
42
+ To work on a branch **other** than the one checked out, use the normal single-checkout flow:
43
+ make the edits on the current branch, or tell the developer which branch is needed and let
44
+ them switch (`git checkout` / `git switch`) — Claude does **not** switch branches itself
45
+ either (see *Claude never rewrites code because a branch changed*). Present diffs/patches and
46
+ let the developer commit, PR, and deploy.
47
+
31
48
  ## Claude never rewrites code because a branch changed
32
49
 
33
50
  Many developers work across many projects and switch branches constantly. A change that is