toga-ai 1.0.195 → 1.0.197

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)
@@ -3,6 +3,6 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Forwarder Architecture](architecture.md) | Forwarder is a tiny **standalone PHP application** that powers TOGA's short, branded redirect domains. | forward/forward.ini, forward/index.php, forward/.htaccess, forward/.platform/httpd/conf.d/rewritemap.conf, forward/.ebextensions/rewritemap.config, forward/design/index.php, forward/composer.json |
6
- | [Design Demo Admin Tool](features/design-demo-admin.md) | A standalone, dependency-free PHP tool served at `https://demo.togatech.com/design` that lets the **design team** publish self-contained "Claude Design" HTML ex | forward/design/index.php, forward/design/.config.example.php, forward/.gitignore, forward/.htaccess, forward/.ebextensions/php-uploads.config |
6
+ | [Design Demo Admin Tool](features/design-demo-admin.md) | A standalone, dependency-free PHP tool served at `https://demo.togatech.com/design` that lets the **design team** publish self-contained "Claude Design" HTML ex | forward/design/index.php, forward/tabs.json, forward/design/.config.example.php, forward/.gitignore, forward/.htaccess, forward/.ebextensions/php-uploads.config |
7
7
  | [Encrypted-Link Handler](features/encrypted-link-handler.md) | A `index.php` feature in [Forwarder](../architecture.md) for redirect domains whose URL path carries an **encrypted token** that must be decoded before redirect | forward/index.php |
8
8
  | [Static Demo Hosting](features/static-demo-hosting.md) | A lightweight way to host self-contained static HTML pages (demos, exported designs, download landing pages) on the Forwarder app under a clean URL — e.g. | forward/.htaccess, forward/togadesk/index.html |
@@ -6,10 +6,11 @@ project: Forwarder
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-24
9
+ updated: 2026-06-25
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - forward/design/index.php
13
+ - forward/tabs.json
13
14
  - forward/design/.config.example.php
14
15
  - forward/.gitignore
15
16
  - forward/.htaccess
@@ -94,9 +95,34 @@ auto-deploy ("live in a few minutes").
94
95
  brief "Copied ✓" confirmation.
95
96
  - **Copy Claude Design prompt** — one-click copy of the standard self-contained-HTML export
96
97
  prompt (the same prompt documented in [static-demo-hosting](static-demo-hosting.md)).
98
+ - **Create a tab** (`apiCreateTab`, action `create-tab`) — appends a named tab to
99
+ `tabs.json`. Case-insensitive dedupe; name length-capped 40. Commits `tabs.json` only.
100
+ - **Move a project to a tab** (`apiSetProjectTab`, action `set-project-tab`) — sets/clears a
101
+ project's `tab` field; empty string = Unsorted (the field is `unset`). Commits that
102
+ project's `project.json` **only** — tab membership does not affect the redirect stub, so
103
+ **no stub rebuild**. `apiCreateProject` also accepts an optional `tab`.
97
104
 
98
105
  The full action set is: `list`, `create-project`, `upload`, `adopt`, `set-latest`,
99
- `toggle-version`, `edit-version`, `toggle-project` (plus `bootstrap`).
106
+ `toggle-version`, `edit-version`, `toggle-project`, `create-tab`, `set-project-tab` (plus
107
+ `bootstrap`). `list` also returns the tab list (`{ projects, tabs }`).
108
+
109
+ ### Tab-based organization
110
+
111
+ Demo projects can be grouped into named **tabs** (e.g. separate Production / Pipeline /
112
+ Sandbox).
113
+
114
+ - **`tabs.json`** at the repo root holds the ordered tab list — `{"tabs": ["Production",
115
+ "Pipeline", "Sandbox"]}`. `loadTabs()` returns those three as **defaults when `tabs.json`
116
+ does not exist yet** (so tabs appear before any are created); the file is only written on
117
+ the first tab mutation (`create-tab`).
118
+ - Each `project.json` gains an optional **`tab`** field (string). Missing/absent =
119
+ **"Unsorted"**.
120
+ - **UI:** a filter chip bar (All / each tab / Unsorted / + New tab) with per-tab project
121
+ counts; clicking a chip filters the list. Each project card has a **Tab `<select>`** to
122
+ move the project between tabs (on the select's `change`). The New Project modal has a Tab
123
+ dropdown defaulting to the currently-viewed tab.
124
+ - The **displayed** tab list is the **union** of `tabs.json` order plus any tab names
125
+ referenced by projects — so removing a tab from `tabs.json` never orphans a project.
100
126
 
101
127
  ### Naming & validation
102
128
 
@@ -117,7 +143,10 @@ anyone who reaches it can publish or unlist. Do not treat the tool as protected.
117
143
 
118
144
  ## Data model
119
145
 
120
- `project.json` per managed demo (see Versioning model above). No database.
146
+ `project.json` per managed demo (see Versioning model above), plus an optional `tab` string
147
+ field per project (absent = Unsorted). A single repo-root **`tabs.json`** —
148
+ `{"tabs": [...]}` — holds the ordered tab list (defaults to `["Production", "Pipeline",
149
+ "Sandbox"]` until first written). No database.
121
150
 
122
151
  ## Client variations
123
152
 
@@ -158,6 +187,14 @@ None — internal/shared design-team tool.
158
187
  `isValidVersionId` + length caps). Added a **Copy URL** button on each project card
159
188
  (`navigator.clipboard` → `https://demo.togatech.com/<project>`). Both additive; no new
160
189
  security surface. (jcardinal)
190
+ - 2026-06-25 — Added **tab-based organization**: new repo-root `tabs.json` (ordered tab
191
+ list, defaults Production/Pipeline/Sandbox until first written via `create-tab`), optional
192
+ `tab` field on `project.json` (absent = Unsorted), and actions **`create-tab`** /
193
+ **`set-project-tab`** (`apiCreateProject` accepts an optional `tab`). UI filter chip bar
194
+ with per-tab counts, per-card Tab select, and a Tab dropdown on the New Project modal. The
195
+ displayed tab list is the union of `tabs.json` and tabs referenced by projects, so dropping
196
+ a tab from `tabs.json` never orphans a project. `set-project-tab` commits `project.json`
197
+ only (no stub rebuild). (jcardinal)
161
198
 
162
199
  ## Related docs
163
200
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.195",
3
+ "version": "1.0.197",
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",