toga-ai 1.0.195 → 1.0.196

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.
@@ -7,6 +7,7 @@
7
7
  | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
8
8
  | [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
9
9
  | [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | _underscore/Component/Forecast/SaleImport/SaleImport.php, _underscore/Component/Forecast/Db/Db.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php |
10
+ | [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. | _underscore/Model/Core/Model.php |
10
11
  | [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
11
12
  | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
12
13
  | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql |
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: _Model magic-field access (__get without __isset)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-25
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - _underscore/Model/Core/Model.php
13
+ related:
14
+ - ../../api2/features/language-translation-layer.md
15
+ ---
16
+
17
+ ## Summary
18
+
19
+ `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`.
20
+ Because PHP does not route `isset()` / null-coalescing through `__get()`, any `isset($model->field)` or
21
+ `$model->field ?? null` on a magic (DB) field is **always** `false` / `null` — even when the column
22
+ exists and has a value. And a bare `$model->field` read for a column that is **not** on the model
23
+ **throws**. This bites any code that probes for an optional/conditional field on a generic model.
24
+
25
+ ## How it works
26
+
27
+ - `_Model::__get($name)` resolves a configured DB field and returns its value; for an unconfigured
28
+ field it throws (not a silent null).
29
+ - There is **no** `_Model::__isset()`, so PHP's `isset()`/`empty()`/`??` short-circuit to "not set"
30
+ for every magic field before `__get` is ever consulted. This is a PHP language rule, not a bug in
31
+ the value.
32
+
33
+ **Safe pattern for a possibly-absent field on a generic model:**
34
+
35
+ ```php
36
+ if (array_key_exists('uuid', $model->getFieldConfig())) {
37
+ $uuid = $model->uuid; // safe: __get won't throw, field is known to exist
38
+ }
39
+ ```
40
+
41
+ Check `array_key_exists('field', $model->getFieldConfig())` first, then read via `__get`. Do **not**
42
+ gate on `isset($model->field)` or `$model->field ?? $default` — both silently misbehave.
43
+
44
+ ## Gotchas / known issues
45
+
46
+ - `isset($model->magicField)` / `$model->magicField ?? null` are **always** false/null for DB-backed
47
+ magic fields. Use `getFieldConfig()` + `array_key_exists` to test presence.
48
+ - A bare `__get` on an unconfigured field **throws** — never read a maybe-absent field without the
49
+ `getFieldConfig()` guard.
50
+ - This surfaced as a missing `uuid` in the API translation-layer fallback warning — see
51
+ [Language Translation Layer](../../api2/features/language-translation-layer.md).
52
+
53
+ ## Change history
54
+
55
+ - 2026-06-25 — Documented the `__get`-without-`__isset` gap and the `getFieldConfig()` +
56
+ `array_key_exists` safe-read pattern, discovered while debugging a missing `uuid` in the API2
57
+ translation fallback warning. (jcardinal)
@@ -3,7 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
6
- | [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, expanding later) in multiple languages without forking the schema or breaking English consume | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql |
6
+ | [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, expanding later) in multiple languages without forking the schema or breaking English consume | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql |
7
7
  | [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
8
8
  | [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
9
9
  | [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | |
@@ -6,10 +6,11 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-23
9
+ updated: 2026-06-25
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
+ - api2/Component/Api/V2/Response/Response.php
13
14
  - _underscore/Model/Core/Setting.php
14
15
  - _underscore/Model/Core/RecordField.php
15
16
  - _underscore/Model/Core/DefaultGlobalSetting.php
@@ -20,6 +21,7 @@ files:
20
21
  - dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql
21
22
  related:
22
23
  - ./acl-permission-chain.md
24
+ - ../../_underscore/features/model-magic-field-access.md
23
25
  ---
24
26
 
25
27
  ## Summary
@@ -55,6 +57,19 @@ persona** (in the user's persona order) that sets it wins. The resolved code is
55
57
  response as `audience.language`. API-credential auth gets no language. Token refresh copies the claim,
56
58
  so a language change requires re-authentication.
57
59
 
60
+ **Invalid-language hard failure (auth time).** The resolved code is validated against the client's
61
+ `Languages` table. An **unset/empty** setting defaults to `en` (no error). But a **non-empty** code
62
+ that does **not** exist in `Languages` now **fails authentication** with HTTP 400
63
+ `EV-16 DEFINED_MESSAGE_ERROR_INVALID_LANGUAGE_SETTING` (validation group, defined in `Response.php`),
64
+ with the offending `language` code in the message `identifiers` — instead of silently falling back to
65
+ `en`. A misconfigured language must surface loudly, not silently degrade.
66
+
67
+ **Auth-mint status finalization (subtle).** The auth success path sets `status = 201` and attaches the
68
+ tokens. EV-16 is signalled by a local flag `$invalidLanguageSetting` set at the resolution site; the
69
+ finalization branches `if (empty($invalidLanguageSetting))` → `201` + tokens, else
70
+ `addDefinedMessage(EV-16)` + `data = null`. See Gotchas — do **not** convert this to an
71
+ `is_null($this->response->status)` guard.
72
+
58
73
  **Which fields are translatable (metadata-driven).** `Core.RecordFields.translationRecordFieldId`
59
74
  (new column, positioned after `recordId`) points a source field's RecordField at the sidecar field's
60
75
  RecordField. `buildLookups`/RecordFields-load resolves this into
@@ -65,7 +80,10 @@ serialization site (top-level full-model, custom-fields, FK child, both inherent
65
80
  `getTranslatedFieldValue($record, $field, $sourceModel, $defaultValue)` is called. When a non-base
66
81
  language is active and the field is translatable, it loads the sidecar row for that source row +
67
82
  `languageId` (cached per row so multiple fields = one load) and returns the sidecar value if non-null;
68
- otherwise it returns the English default and queues a deduped `W*` warning.
83
+ otherwise it returns the English default and queues a fallback warning. The warning now carries the
84
+ missing record's `uuid` in its `identifiers` and dedupes per-field-**and**-per-record
85
+ (key `recordFieldId:uuid`) rather than once-per-field — so every record lacking a translation for a
86
+ field is reported individually.
69
87
 
70
88
  **Write path.** On create and update, `extractTranslationWrites()` pulls translatable fields out of
71
89
  the write set for a non-base language (so the English source is never overwritten), and after the
@@ -96,6 +114,26 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
96
114
  per-field fallback warning signals the English fallback.
97
115
  - The base language is `en`; when the resolved language is `en` (or API auth) all translation logic is
98
116
  skipped and responses are byte-identical to pre-feature.
117
+ - **"Why isn't my language setting applying?"** A `ClientGlobalSettings` row with `isOverridable = 0`
118
+ **locks** the value against all downstream persona/user layers — a `UserGlobalSettings` (or persona)
119
+ override never wins while the client-global layer is locked. This is by design (the 8-layer cascade
120
+ with `isOverridable` locking). To let user/persona overrides apply, the locking layer must be
121
+ `isOverridable = 1` (or its value cleared). The cascade itself is correct — this is a data-config
122
+ gotcha, not a code bug.
123
+ - **Namespace gotcha in `V2.php` (`namespace api;`).** The api-namespaced classes
124
+ (`_Component_Api_V2_Response`, `_Component_Api_V2_Response_Message`) must be referenced **unqualified**
125
+ (resolves to `api\…`) or with the full `\api\_Component_Api_V2_Response_Message`. A bare leading
126
+ backslash (`\_Component_Api_V2_Response_Message`) forces the global namespace and throws
127
+ `EO-1 "Could not find required file"` via the autoloader. The translation fallback-warning code hit this.
128
+ - **Do NOT guard the auth-mint finalization with `is_null($this->response->status)`.** The auth response
129
+ auto-adds client `name` + user `firstName`/`lastName` and runs `processRoutePairs()`, which sets
130
+ `$this->response->status` to `200` **before** finalization. An `is_null` guard there suppresses the
131
+ `201` + tokens and yields a broken `200`/`data:null` login. Branch on the explicit
132
+ `$invalidLanguageSetting` flag instead.
133
+ - Reading a possibly-absent magic field off a generic `_Model` needs `array_key_exists(...)` +
134
+ `__get` — `isset()`/`?? null` always read false/null. See
135
+ [_Model magic-field access](../../_underscore/features/model-magic-field-access.md); this was the
136
+ cause of the uuid not appearing in the fallback warning.
99
137
  - Migrations not yet executed at time of writing; needs live verification.
100
138
 
101
139
  ## Change history
@@ -103,6 +141,14 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
103
141
  - 2026-06-23 — Initial build: audience.language + JWT embedding, ItemTranslations sidecar + metadata +
104
142
  full ACL chain, and translation-aware read/write at the API layer. Also fixed a latent autoload bug
105
143
  by renaming `DefaultFlobalSetting.php` → `DefaultGlobalSetting.php`. (jcardinal)
144
+ - 2026-06-25 — Added defined error `EV-16 INVALID_LANGUAGE_SETTING` (HTTP 400, in `Response.php`): a
145
+ non-empty language code absent from `Languages` now fails auth instead of silently falling back to
146
+ `en` (empty still defaults to `en`). Documented the auth-mint `201` finalization (branch on the
147
+ `$invalidLanguageSetting` flag, never an `is_null(status)` guard). Fixed the fallback-warning
148
+ namespace bug (no bare leading backslash on api-namespaced classes). Fallback warning now includes the
149
+ record `uuid` and dedupes per-record (`recordFieldId:uuid`). Documented the `isOverridable = 0`
150
+ client-global lock as a config gotcha. Discovered the `_Model` `__isset` gap behind the missing-uuid
151
+ bug (see linked _underscore doc). Live-tested on the local Compass DB. (jcardinal)
106
152
 
107
153
  ## Related docs
108
154
 
@@ -15,7 +15,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
15
15
 
16
16
  ## 2.0 framework
17
17
 
18
- - **_underscore** (_Underscore) _(framework core)_ — 13 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
18
+ - **_underscore** (_Underscore) _(framework core)_ — 14 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
19
19
  - **worker2** (Worker) — 14 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
20
20
  - **api2** (API) — 6 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
21
21
  - **dbchanges2** (Database Changes) _(framework core)_ — 2 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.195",
3
+ "version": "1.0.196",
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",