toga-ai 1.0.1071 → 1.0.1073

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.
@@ -16,6 +16,7 @@ files:
16
16
  - _underscore/Model/Client/UnitNote.php
17
17
  - _underscore/Model/Client/ServiceRequestNote.php
18
18
  - _underscore/Model/Client/PartRequestNote.php
19
+ - _underscore/Model/Client/UserNote.php
19
20
  related:
20
21
  - ../../toga-blox/features/add-notes-template.md
21
22
  - ./case-status-and-stage-gate.md
@@ -63,10 +64,11 @@ Before building anything that shows or filters by note author:
63
64
 
64
65
  - **`contactId` on a `*Notes` model is an unwritten FK — treat it as always NULL.** Seven models, zero writers.
65
66
  - **A notes list UI has no author to show.** Design for it, or the field silently renders blank for every row.
66
- - **Exception — `CaseNotes` (2026-10-15), `CustomerNotes` and `ContactNotes` (2026-10-17) have a real author:** `authorUserId` stamped in `::save()` from `_Model::getRequestUserId()`. Copy that pattern for the real fix. The `save()` body is now copied 3 times (Case, Customer, Contact) — move it to a shared trait/base before a 4th copy. See [Case status + stage gate](./case-status-and-stage-gate.md), [Customer related records](./customer-locations-files-notes.md), [Contact related records](./contact-notes-files-types.md).
67
+ - **Exception — server-stamped author:** `CaseNotes`, `CustomerNotes`, `ContactNotes`, `IssuerNotes`, `CertificationNotes`, `AttributeNotes`, `UserNotes` carry `authorUserId`, stamped in `::save()` from `_Model::getRequestUserId()` (write-once). Copy that pattern for the real fix. The `save()` body is now copied 7 times — **consolidation debt:** move it to a shared trait/base before adding another. See [Case status + stage gate](./case-status-and-stage-gate.md), [Customer related records](./customer-locations-files-notes.md), [Contact related records](./contact-notes-files-types.md).
67
68
  - **`dtStamp` is `FIELD_DATETIME_CREATED`** — stamped by the ORM on insert, the only provenance a note actually carries.
68
69
 
69
70
  ## Change history
71
+ - 2026-10-17 — UserNotes added; seven author-stamped note models now copy one save() body (TRUE-82872). (jcardinal)
70
72
  - 2026-10-17 — ContactNotes added as third server-stamped-author note model; save() body is a consolidation candidate (TRUE-82871). (jcardinal)
71
73
  - 2026-10-17 — CustomerNotes added as a second server-stamped-author note model (TRUE-82870). (jcardinal)
72
74
  - 2026-10-15 — Noted CaseNotes as the first note model with a server-stamped author. (jcardinal)
@@ -32,7 +32,8 @@ client `AclRecordScripts`. Data source: [record change audit log](record-change-
32
32
 
33
33
  - **Roots** = `ROOT_ACTIVITY_SURFACE_SLUGS` (route → activity Surface slug): `tickets` →
34
34
  `ticket-activity`, `units` → `asset-activity`, `cases` → `case-activity`, `service-requests` →
35
- `service-request-activity`, `customers` → `customer-activity`, `contacts` → `contact-activity`. Any other route → 400. **A new root = one map entry + its Surface
35
+ `service-request-activity`, `customers` → `customer-activity`, `contacts` → `contact-activity`,
36
+ `issuers`, `certifications`, `attributes`, `users` → `<singular>-activity`. Any other route → 400. **A new root = one map entry + its Surface
36
37
  (`dbchanges2` `Core/`).** Engine (`Source` reads Logs, `Reader` checks as the reader through api2)
37
38
  is generic; no new client ACL needed for a new root.
38
39
  - **Children:** `Logs.RecordParent`, level by level, max `Source::MAX_DEPTH` (3) levels and
@@ -45,7 +46,8 @@ client `AclRecordScripts`. Data source: [record change audit log](record-change-
45
46
  `isVisible = 0`). An UPDATE with no visible field is dropped. Secrets → `{redacted:true}`.
46
47
  4. FK values → `{uuid, label[, slug]}`, `{missing:true}`, or `{value:null}` — never a raw id.
47
48
  - **CREATE rows of `CREATE_HIDDEN_ROUTES`** (`ticket-notes`, `ticket-note-files`, `unit-notes`,
48
- `case-notes`, `service-request-notes`, `customer-notes`, `contact-notes`) are dropped — the host screen merges the notes feed.
49
+ `case-notes`, `service-request-notes`, `customer-notes`, `contact-notes`, `issuer-notes`,
50
+ `certification-notes`, `attribute-notes`, `user-notes`) are dropped — the host screen merges the notes feed.
49
51
  Note edits/deletes still show.
50
52
  - **Paging:** cursor = `Logs.Record` id; `nextCursor` null at the end. Scan bounded
51
53
  (`MAX_SCAN_BATCHES` × `SCAN_BATCH_SIZE`); a page cut by the bound still returns a `nextCursor`.
@@ -68,7 +70,11 @@ client `AclRecordScripts`. Data source: [record change audit log](record-change-
68
70
  best-effort (same user + short time window). Exact grouping needs a new request-id column
69
71
  (schema + api2 change) — not built.
70
72
 
73
+ - **`users` root is safe for secrets:** `password` / `ldapDn` are redacted on write (api2
74
+ `LOG_REDACTED_FIELD_NAMES`) and on read (`isRedactedLogField`).
75
+
71
76
  ## Change history
77
+ - 2026-10-17 — Added root `users` + `user-notes`; listed `issuers` / `certifications` / `attributes` roots already in code (TRUE-82872). (jcardinal)
72
78
  - 2026-10-17 — Added root `contacts` + `contact-notes` to `CREATE_HIDDEN_ROUTES` (TRUE-82871). (jcardinal)
73
79
  - 2026-10-17 — Added root `customers` + `customer-notes` to `CREATE_HIDDEN_ROUTES` (TRUE-82870). (jcardinal)
74
80
  - 2026-10-17 — One-FK rule for parent links; no request id for grouping (TRUE-82889). (jcardinal)
@@ -6,12 +6,13 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-13
10
- owners: [bala, ajean]
9
+ updated: 2026-10-18
10
+ owners: [bala, ajean, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/Logs/Record.php
13
13
  - _underscore/Model/Client/Logs/RecordField.php
14
14
  - _underscore/Model/Client/Logs/CustomRecordField.php
15
+ - api2/Component/Api/V2/V2.php
15
16
  related:
16
17
  - ../../api2/features/request-logging.md
17
18
  - ../../../1.0/apps/tools/features/compass-user-persona-admin.md
@@ -62,6 +63,8 @@ Order by `Record.timestamp` for a fixed `(recordId, primaryKeyId)` for the full
62
63
  - **A worker doing raw bulk updates is invisible here.** If a job needs auditable effects it must write its own `Record` **note** row explicitly — nothing is automatic. Real example: [Compass PEOPLE-file user lifecycle](../../../../clients/compass-usa/features/people-file-user-lifecycle.md), whose deactivation batch is a raw `UPDATE Users SET isActive = 0 WHERE id IN (...)`.
63
64
  - **⚠⚠ `_Model_Client_Logs_Record::addNote()` IS A SILENT NO-OP OUTSIDE api2.** `addNote()` only **stages** rows into the class's static `$_notes` array. The **only** thing that flushes it is api2's `Component/Api/V2/V2.php`. Nothing in `_underscore` and nothing in `worker2` flushes it. So **any worker or cron that calls `addNote()` loses the note entirely** — no error, no warning, no row. Most dangerous mode on this page: the calling code reads as if it audited itself.
64
65
  - A worker needing a note must either drain `_Model_Client_Logs_Record::$_notes` itself and INSERT into `Logs_<Tenant>.Record`, or write the `Record` rows directly. Note **`Logs_<Tenant>.Record.clientId` is `NOT NULL`**, and a worker generally has no `$api` to read the client id from — resolve from `Core` (`Clients` JOIN `Databases` on `name`). Known ids: `Client_Compass` = **2** (`Logs_Compass`), `Client_CompassCanada` = **43** (`Logs_CompassCanada`). Applies to **every** client, not just Compass.
66
+ - **⚠ In api2, `addNote()` must come AFTER any `internalRequest` in the same hook.** The internal request's record-log pass clears every note queued before it. Also, a write done through an internal request on **another** model is logged on that model's row only — not on the parent's — so add a note on the parent if its timeline / `_updated` must show it. Reference: [write intent on a PUT](../../api2/features/api-payload-interceptors.md#audited-action-as-a-table-write-the-action-table-pattern).
67
+ - **⚠ `_created` on a record = the EARLIEST `Record` log row** (`V2.php` `loadLogRecordBatch`, `MIN(id)`), not the row's insert. A row created outside the API (SQL seed, migration) has no create log, so `_created` shows **whoever first wrote it through the API**. Use the table's stored `dtCreated` for "when created".
65
68
  - **`Record.userId` is a TENANT `Users.id` — but it is NULLABLE, so a staff-actor row IS possible with no schema change** (verified 2026-09-04, correcting the earlier claim that internal-tool writes had no audit home). You cannot express "a `Client_True` staff member did this" in `userId`, so put the staff identity in **`note`**:
66
69
  - `recordId` = the `Core.Records` id of the model (**`Users` = 2**), `primaryKeyId` = the target row's id, `clientId` = the tenant's `Core.Clients` id, `userId` = **NULL**, `note` = who did it and what changed.
67
70
  - `_Model_Compass_ApprovalDecision` (`_underscore/Model/Compass/ApprovalDecision.php:1344-1366`) already hand-writes exactly this shape — an established pattern.
@@ -75,6 +78,7 @@ Order by `Record.timestamp` for a fixed `(recordId, primaryKeyId)` for the full
75
78
  - **Do not paste logged values into tickets or the KB.** `oldValue`/`newValue` and note text routinely contain user emails and other personal data. Reference rows by location; quote the mechanism, not the people.
76
79
 
77
80
  ## Change history
81
+ - 2026-10-18 — `_created` = earliest log row (seeded rows show first API writer); `addNote()` after `internalRequest`; internal writes log on their own model only (TRUE-82876). (jcardinal)
78
82
  - 2026-10-13 — Added PurchaseOrders / PurchaseOrderItems ids and this log as the surviving source once the Api log ages out. (bala)
79
83
 
80
84
  ## Related
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-14
10
- owners: [ajean, rgirish]
9
+ updated: 2026-10-17
10
+ owners: [ajean, rgirish, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/User.php
13
13
  - api2/Component/Api/V2/V2.php
@@ -78,6 +78,13 @@ So whoever controls an address that appears on a `Users` row can request, receiv
78
78
  - **⚠ The reset lookup has no `isActive` filter and no duplicate handling.** On a tenant that allows duplicate emails, "the user with this address" is whichever row MySQL happens to return first. Do not assume the reset went to the account you were looking at.
79
79
  - **⚠ Do not assume the database enforces one email per user.** The 2.0 blank-client template declares `UNIQUE KEY email (email)`, but **live tenants have diverged** — both Compass tenants have no unique key and no index on `email` (prod-verified 2026-09-04). Any code that writes an email must check for a cross-row collision itself. Evidence and the full index list: [PEOPLE-file User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
80
80
  - **⚠ Treat an email write as a security change, not a data change.** Minimum rails for any UI or job that sets `Users.email`: confirm the value twice, validate the domain against `Core.ClientEmailDomains`, refuse when another row in the tenant already holds the address, and write an audit row **before** the update. That is the shape the [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) shipped.
81
+ - **⚠ AD sync overwrites `email`, `title`, `phoneNumber` on every run.**
82
+ `_Model_Client_User::syncFromActiveDirectoryUser()` (~L314) writes them from AD; an app edit to
83
+ those fields on an AD-synced user is silently reverted (Client_True: 2,788 of 2,791 users have
84
+ `ldapDn` set, 2026-10-17). Lock those fields in the UI when calc field `_isDirectorySynced` = 1
85
+ (`ldapDn` non-empty) — use it instead of exposing `ldapDn`.
86
+ - **Client role `Base` cannot write `Users.email`** (RecordField 115 `isWritable` 0 in Client_True /
87
+ Client_Elite, 2026-10-17). An email edit needs a stronger role — keep it that way.
81
88
  - **An email is also a worker matching key.** worker2's `ensureCrossRegionUsersActive()` matches Compass users on **email**, so changing an address can silently remove a user from a hardcoded protected set — a second reason email edits need an explicit block list.
82
89
  - **⚠ `_Http::validateJsonWebToken()` checks the signature only, never `exp`.** Any code that trusts
83
90
  a JWT must check `exp` itself. Before 2026-10-13, `updatePassword()` did not, so a reset link
@@ -92,6 +99,7 @@ So whoever controls an address that appears on a `Users` row can request, receiv
92
99
  - **Never paste real addresses into tickets or the KB.** Reference the mechanism and the column, not the person.
93
100
 
94
101
  ## Change history
102
+ - 2026-10-17 — AD sync overwrites email/title/phoneNumber (`_isDirectorySynced` lock); Base role cannot write email (TRUE-82872). (jcardinal)
95
103
  - 2026-10-14 — Reset token now 30 min and `updatePassword()` rejects expired tokens; documented the two scripted APIs, Public ACL, `Core.Domains` link host, and the BCC / query-string exposure (rgirish)
96
104
  - 2026-09-16 — Created: email as login identity and tenant-routing key (ajean)
97
105
 
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-17
9
+ updated: 2026-10-18
10
10
  owners: ["mhammontree", "dfranks", "bala", "snaredla", "jcardinal", "rgirish"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -28,6 +28,8 @@ files:
28
28
  - _underscore/Model/Client/UserGlobalSetting.php
29
29
  - dbchanges2/Core/2026-10-09a - UserGlobalSettingsInterceptors.sql
30
30
  - _underscore/Model/Client/CaseRecordRemoval.php
31
+ - _underscore/Model/Client/Workflow.php
32
+ - _underscore/Model/Client/WorkflowVersion.php
31
33
  related:
32
34
  - ./cross-client-data-retrieval.md
33
35
  - ../../_underscore/features/record-activity-timeline.md
@@ -289,6 +291,20 @@ Reference: `_Model_Client_CaseRecordRemoval` (TRUE-82889, CTO-reviewed).
289
291
  `postPut` **before** the stage gate, and the gate counts only rows that exist after the write —
290
292
  never trust a payload flag that says "I will create it".
291
293
 
294
+ **Write intent that writes ANOTHER record under the caller's ACL** (TRUE-82876). Reference:
295
+ `PUT /workflows/{uuid} {"status":"ACTIVE","publishDraft":true}` (`_Model_Client_Workflow`):
296
+ 1. PRE PUT strips the intent key, locks, runs the checks (e.g. overlap) first.
297
+ 2. The second record's write goes through
298
+ `_Model_Client_Ticket::runInsideRequestContext($api, fn() => _Model_Client_Ticket::internalRequest(...))`
299
+ — here `PUT /workflow-versions/{draft} {isPublished:1}` — so api2 checks the caller's ACL and that
300
+ route's `prePut`. Never write the other model directly.
301
+ 3. Any refusal throws → `MODEL_SAVE_ERROR` → 400 → `transactionRollback` of `DB_CLIENT`
302
+ (`V2.php` ~6844-6906, ~2304-2310): the whole PUT is all-or-nothing.
303
+ 4. Audit: the internal write is **not** logged on the parent's row. Call
304
+ `_Model_Client_Logs_Record::addNote(...)` **after** the internal request — its record-log pass
305
+ clears every note queued before it (see
306
+ [record change audit log](../../_underscore/features/record-change-audit-log.md#gotchas)).
307
+
292
308
  ### Own-row guards in a PRE interceptor
293
309
 
294
310
  A PRE hook that limits a user to their own rows must close four bypasses — each found by review on the `user-global-settings` guard (TRUE-82613):
@@ -358,4 +374,5 @@ Refuse API-key callers when the rule is "the signed-in user" (no user on the tok
358
374
  - 2026-10-09 — Own-row guards in PRE hooks (alias keys + parent-route bypasses, PRE before field ACL); user-global-settings reference; known open holes. (jcardinal)
359
375
  - 2026-10-15 — PRE → POST state hand-off via `$api->passThrough`; detect a real field change, not a resent one (TRUE-82727). (bala)
360
376
  - 2026-10-16 — Command keys: strip in `prePut`, park on `passThrough`, act in `postPut`; empty body → `{}` (TRUE-82849). (jcardinal)
377
+ - 2026-10-18 — Write intent that writes another record via internal request (caller ACL, rollback, `addNote` after the call) (TRUE-82876). (jcardinal)
361
378
  - 2026-10-17 — "Action table" pattern for audited actions; write intents before a stage gate; new interceptor rows ship in a file run after the code deploy (TRUE-82889). (jcardinal)
@@ -45,7 +45,7 @@ Because client DBs live on **different clusters**, a cross-DB JOIN/UNION is impo
45
45
 
46
46
  Entry point is `_Component_Api_CrossClient` (`api2/Component/Api/CrossClient/CrossClient.php`), delegated from a guard at the **top** of the `ACTION__LIST` case in `_Component_Api_V2::processRoutePairs()`: when `isCrossClientRequest($httpOptions)` is true (a non-empty `client` option), it calls `handleListing()`, merges result rows into `$outData` and meta into `$this->response->meta`, and breaks. The guard is **inert** on ordinary single-client GETs.
47
47
 
48
- 1. **resolveScope** — the caller's entitled clients resolve via the `Users_Clients` bridge (`Users.homeClientUserId` maps one home user to per-client user records across tenant DBs); each target yields a minted per-client identity. This is **serial** — one DB context switch + query per permitted client, on every request including cache hits.
48
+ 1. **resolveScope** — the caller's entitled clients resolve via the `Users_Clients` bridge (Records 199 `user-clients`); `mintIdentity()` then picks each target's linked user. Identity model: a **home** row has `homeClientUserId` NULL; a **linked** row in another client DB has `clientId` = home Core client id + `homeClientUserId` = home `Users.id` (`UNIQUE(clientId, homeClientUserId)`). **"Which clients can this user reach" = `Users_Clients`**, not the linked rows — they drift (local data has a linked row with no `Users_Clients` row). This is **serial** — one DB context switch + query per permitted client, on every request including cache hits.
49
49
  2. **queryHash** — SHA-256 of the normalized query plus `QUERY_HASH_VERSION`; the cache key. Bump `QUERY_HASH_VERSION` whenever how cached rows are built or ordered changes — it orphans every older result table (v2 = dotted-field sort keys).
50
50
  3. **Build lock** — a named `GET_LOCK` over a **fixed-width digest** of client/user/hash.
51
51
  4. **Fan out** — a two-phase concurrent HTTP handshake per client (see [encrypted-user-uuid-auth-handoff](./encrypted-user-uuid-auth-handoff.md)): Phase A mints an access token, Phase B issues the authenticated GET.
@@ -145,5 +145,6 @@ Page 1 of 25 still fetches exactly 25; page 254 fetches 500 per pass — ~13 rou
145
145
  - [_Model magic-field access](../../_underscore/features/model-magic-field-access.md)
146
146
 
147
147
  ## Change history
148
+ - 2026-10-17 — Home vs linked user rows; `Users_Clients` is the reach list, linked rows drift (TRUE-82872). (jcardinal)
148
149
  - 2026-10-17 — Fetch scope is `client=` OR own token, never both; key multi-client rows by client + record uuid (TRUE-82903). (jcardinal)
149
150
  - 2026-10-17 — Internal-request writes must add their own `cacheInvalidations` (TRUE-82889). (jcardinal)
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-17
9
+ updated: 2026-10-18
10
10
  owners: [tcox, bala, apeterson, jcardinal, rgirish]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -69,6 +69,28 @@ controller.
69
69
  the join is genuinely 1:1, or the duplicates agree. Worked example:
70
70
  [category tile sort order](../../toga2-commerce/features/category-tile-sort-order.md).
71
71
 
72
+ ### Grouped / aggregate queries (`group=` + an `_`-prefixed aggregate field)
73
+
74
+ Each output row = one bucket: the group key(s) + the aggregate values.
75
+
76
+ - **Stored ROOT column key** (`group=status`): selected as `Table.field AS field` in aggregate mode
77
+ only and copied into the row (`V2.php` ~L5236 map, ~L5468 select, ~L5739 copy). Before TRUE-82876
78
+ it went into `GROUP BY` but never into the SELECT, so buckets had counts and **no key** (UI showed
79
+ "NONE"). Flat (non-aggregate) queries are unchanged.
80
+ - **The key is ACL-gated:** it is returned only if `resolveColumnReference([... 'isValueReturned'
81
+ => true])` passes, so a group column the caller cannot READ is never returned, in any
82
+ `identifier_acl_mode`.
83
+ - **Computed `FIELD_SQL` and joined-path group fields** already return their key.
84
+ - **Cross-client** `mergeGroupRows()` merges buckets by the key value — it needs the key present
85
+ (see [cross-client retrieval](cross-client-data-retrieval.md)).
86
+
87
+ **Known gaps (open, not fixed — 2026-10-18):**
88
+ - A **joined `Table.field`** group key is still not selected.
89
+ - `FOUND_ROWS` / the total count is wrong for grouped queries — do not page a grouped result by it.
90
+ - Grouped queries still add an `ORDER BY uuid` tie-breaker, which a strict `ONLY_FULL_GROUP_BY`
91
+ server rejects. Prod `sql_mode` is unverified; local MySQL accepts it (see
92
+ [local vs prod MySQL config](../../dbchanges2/workflows/local-vs-prod-mysql-config-parity.md)).
93
+
72
94
  ### ⚠ `depth` and `calcDepth` are IGNORED inside the base64 `_` options blob
73
95
 
74
96
  `fields`, `where`, `sort`, `join` work inside the `_` blob. **`depth` and `calcDepth` do not** —
@@ -368,7 +390,10 @@ where=(field:op:value,LOGIC,field:op:value,(nested,OR,nested))
368
390
  - **A dotted FK path (`case.uuid`) in `where` fails `EV-8` on a list GET.** Filter by a parent FK
369
391
  through a join alias: `ojoin=Cases:Cases.id=Tickets.caseId&where=(Cases.uuid:eq:<uuid>)` (or a
370
392
  `TableViewJoins` row that gives the alias). A path through an **inherent child**
371
- (`ticketUsers.user.uuid`) does work.
393
+ (`ticketUsers.user.uuid`) does work — but only when the **first** segment is an inherent child
394
+ of the route (`buildChildPathWhereSql`). A path that starts with a forward FK hop fails
395
+ (`service-requests` → `ticket.ticketUsers.user.uuid`). No-api2-change fix: a TableView that
396
+ outer-joins the FK table and the bridge, then `where` on the join alias.
372
397
 
373
398
  ### ✅ You CAN `where` on a calculated (`FIELD_SQL`) field — but ONLY with the BARE name
374
399
 
@@ -625,6 +650,8 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
625
650
  [V2 request logging](request-logging.md).
626
651
 
627
652
  ## Change history
653
+ - 2026-10-18 — Grouped queries: stored root group key now selected + ACL-gated; known gaps (joined key, total count, `ORDER BY uuid` under ONLY_FULL_GROUP_BY) (TRUE-82876). (jcardinal)
654
+ - 2026-10-17 — Child-path `where` must start at an inherent child; FK-first path → TableView join alias (TRUE-82872). (jcardinal)
628
655
  - 2026-10-17 — Multi-sort comma list + `uuid` tie-breaker for stable paging; sort on calc works; refreshed where-path line refs. (jcardinal)
629
656
  - 2026-10-17 — POST create = 201, PUT/DELETE = 200 (TRUE-82889). (jcardinal)
630
657
  - 2026-10-15 — Security: `where` on an unreadable field still filters under default `identifier_acl_mode=log` (hash guessing); dotted FK path in `where` = `EV-8`, use a join alias (TRUE-82836). Folded older history. (jcardinal)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1071",
3
+ "version": "1.0.1073",
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",