toga-ai 1.0.1063 → 1.0.1065
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.
- package/knowledge/2.0/apps/_underscore/INDEX.md +2 -0
- package/knowledge/2.0/apps/_underscore/features/customer-locations-files-notes.md +71 -0
- package/knowledge/2.0/apps/_underscore/features/note-record-authorship.md +2 -1
- package/knowledge/2.0/apps/_underscore/features/record-activity-timeline.md +3 -2
- package/knowledge/2.0/apps/_underscore/features/record-hard-delete.md +60 -0
- package/knowledge/2.0/apps/_underscore/features/tableview-joins.md +15 -1
- package/knowledge/2.0/apps/dbchanges2/architecture.md +3 -0
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
- package/rules/common/communication.md +3 -0
- package/skills/finish/SKILL.md +24 -5
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
| [_Config group access — the two-argument "optional" form does NOT rescue a missing GROUP](features/config-group-access.md) | `_Config::<group>('prop', false)` does NOT rescue a MISSING GROUP (only a missing prop) — guard with `_Config::isGroupSet()`; open before reading per-environmen |
|
|
24
24
|
| [Contacts require a customerId — the Internal customer & every contact-creation path](features/contact-customer-required-internal-customer.md) | Every `Client_<Tenant>.Contacts` row must have a `customerId`; with no real customer, use the shared Internal customer (fixed uuid in every tenant). |
|
|
25
25
|
| [Core.Domains — the app host registry (and why you cannot derive a host)](features/core-domains-app-host-registry.md) | `Core.Domains` is the authoritative host↔client↔app↔env registry; a host is NOT derivable from env slug or api host — read the table. |
|
|
26
|
+
| [Customer related records — primary location, Locations_Customers, files, notes](features/customer-locations-files-notes.md) | Shared Client-DB data model around a Customer (locations, files, notes, contact count); open before linking anything to a customer or listing a customer's locat |
|
|
26
27
|
| [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the |
|
|
27
28
|
| [Effective persona resolution (_Model_Client_User::_personaIds) — the three sources and the sector hop](features/effective-persona-resolution.md) | A user's EFFECTIVE persona set = UNION of three sources (`Users_Personas` + own-location + 5-hop sector `Locations_Personas`), owned by `_personaIds()`; open be |
|
|
28
29
|
| [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. |
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
| [Persona nav-visibility gate + user nav pins (keyed by Core.SurfaceElements uuid)](features/persona-nav-visibility-gate.md) | Sidebar-nav persona gate in `_Model_Core_Surface` (`Client.PersonaNavVisibility`) and user nav pins (`Client.UserNavPreferences`), both keyed by Core `SurfaceEl |
|
|
50
51
|
| [Record activity timeline (GET /v2/record-logs/timeline, _Component_RecordActivity)](features/record-activity-timeline.md) | Generic per-record activity feed built from Logs_<Tenant>; open when adding a new timeline root, hiding noise fields, or debugging a missing activity row. |
|
|
51
52
|
| [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Field-level change audit trail per tenant; open to answer "who changed this field, to what, when" or to tell a direct SQL edit from an in-app change. |
|
|
53
|
+
| [Record hard delete (_Component_RecordDelete + PRE DELETE interceptor)](features/record-hard-delete.md) | Map-driven hard delete of everything hanging off one record (block / detach / delete children / orphan Files); open before making a record deletable or adding a |
|
|
52
54
|
| [Records meta endpoint (GET /v2/records/meta) — record names, label fields, translated field labels](features/record-meta-endpoint.md) | Per-route record meta (names, label/sublabel field, per-field display labels); open when a screen needs a record's or a field's visible name, or when adding a f |
|
|
53
55
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | Engine that mirrors a downstream ItemFulfillment up the SO→PO→SO chain onto every customer-facing SO; open when touching IF creation, bundle scaling, or upstrea |
|
|
54
56
|
| [Sales-order denial reason (`ApprovalDecisions.note`) — capture, the denial-details card, and the read-only workflow mode](features/sales-order-denial-reason.md) | Map of the SO approval denial reason (`ApprovalDecisions.note`) — where it is written, read, and displayed; open when building any denial-reason display, banner |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Customer related records — primary location, Locations_Customers, files, notes
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-17
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/Customer.php
|
|
13
|
+
- _underscore/Model/Client/Locations/Customer.php
|
|
14
|
+
- _underscore/Model/Client/Customers/File.php
|
|
15
|
+
- _underscore/Model/Client/CustomerNote.php
|
|
16
|
+
- dbchanges2/Core/2026-10-17b - CustomerLocationsFilesNotesActivity.sql
|
|
17
|
+
- dbchanges2/Client/2026-10-17b - CustomerLocationsFilesNotes.sql
|
|
18
|
+
related:
|
|
19
|
+
- ./record-hard-delete.md
|
|
20
|
+
- ./record-activity-timeline.md
|
|
21
|
+
- ./note-record-authorship.md
|
|
22
|
+
- ./tableview-joins.md
|
|
23
|
+
- ./model-save-parent-cascade-stored-field-deadlock.md
|
|
24
|
+
- ./contact-customer-required-internal-customer.md
|
|
25
|
+
- ./acl-permission-chain.md
|
|
26
|
+
---
|
|
27
|
+
Shared Client-DB data model around a Customer (locations, files, notes, contact count); open before linking anything to a customer or listing a customer's locations.
|
|
28
|
+
|
|
29
|
+
## Summary
|
|
30
|
+
|
|
31
|
+
Added 2026-10-17 (TRUE-82870, built for TOGa Desk, usable by any app). All tables live in every `Client_*` DB.
|
|
32
|
+
|
|
33
|
+
| Piece | Shape | Core ids |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `Customers.primaryLocationId` | FK → Locations, standard field | RecordFields 2799 |
|
|
36
|
+
| `Customers._contactCount` | non-stored `FIELD_SQL` COUNT of Contacts by `customerId` | RecordFields 2800 |
|
|
37
|
+
| `Locations_Customers` | many-to-many bridge, `UNIQUE (locationId, customerId)` | Records 382 `location-customers` |
|
|
38
|
+
| `Customers_Files` | copy of `Cases_Files`, `UNIQUE (customerId, fileId)` | Records 383 `customer-files` |
|
|
39
|
+
| `CustomerNotes` | copy of `CaseNotes` minus stage: write-once, internal | Records 384 `customer-notes` |
|
|
40
|
+
|
|
41
|
+
RecordFields 2801-2814 cover the three new records.
|
|
42
|
+
|
|
43
|
+
## How it works
|
|
44
|
+
|
|
45
|
+
- **Location of a customer:** use `primaryLocationId` (a real join). `_location` (city, state from the bill-to
|
|
46
|
+
Address) is kept for its existing readers — do not build new features on it.
|
|
47
|
+
- **Bridge, not `Locations.customerId`:** a location may belong to many customers (developer decision).
|
|
48
|
+
- **Inherent children:** `customers → location-customers` and `locations → location-customers` (isParentTouched 0),
|
|
49
|
+
`customers → customer-files` and `customers → customer-notes` (isParentTouched 1). Child keys:
|
|
50
|
+
`locationCustomers[]`, `customerFiles[]`, `customerNotes[]`.
|
|
51
|
+
- **CustomerNotes author:** `authorUserId` stamped in `save()` from `_Model::getRequestUserId()`, never from the
|
|
52
|
+
payload (nested creates skip the child's PRE POST interceptor, so it cannot live there). No client user
|
|
53
|
+
(API key, worker) → 400. Updates refused.
|
|
54
|
+
- **Client ACL:** full 4-piece chain, granted to every role with a global read on customers (Base locally).
|
|
55
|
+
382/383: CRUD. 384: create + read only, only `note` writable.
|
|
56
|
+
- **Delete:** see [record hard delete](./record-hard-delete.md). **Activity:** root `customers` →
|
|
57
|
+
surface `customer-activity` (see [record activity timeline](./record-activity-timeline.md)).
|
|
58
|
+
- **Listing a customer's locations:** separate parent-scoped TableView `desk-customer-locations` (Locations base,
|
|
59
|
+
OUTER joins → Locations_Customers → Customers, filtered by `Customers.uuid`). See gotchas.
|
|
60
|
+
|
|
61
|
+
## Gotchas
|
|
62
|
+
|
|
63
|
+
- **Never OUTER-join a many-to-many bridge into a shared TableView.** blox sends every join of a view on every
|
|
64
|
+
request (`buildJoinOptions`), so the general listing repeats each row once per bridge match. Make a
|
|
65
|
+
separate parent-scoped view instead (rule lives in [tableview-joins](./tableview-joins.md)).
|
|
66
|
+
- **`_contactCount` must stay non-stored** — a stored self-aggregate deadlocks the save cascade
|
|
67
|
+
([details](./model-save-parent-cascade-stored-field-deadlock.md)).
|
|
68
|
+
- **Assets link to a customer through Contacts** (`Units.contactId → Contacts.customerId`), not directly.
|
|
69
|
+
|
|
70
|
+
## Change history
|
|
71
|
+
- 2026-10-17 — Created: primaryLocationId, _contactCount, Locations_Customers, Customers_Files, CustomerNotes (TRUE-82870). (jcardinal)
|
|
@@ -63,10 +63,11 @@ Before building anything that shows or filters by note author:
|
|
|
63
63
|
|
|
64
64
|
- **`contactId` on a `*Notes` model is an unwritten FK — treat it as always NULL.** Seven models, zero writers.
|
|
65
65
|
- **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)
|
|
66
|
+
- **Exception — `CaseNotes` (2026-10-15) and `CustomerNotes` (2026-10-17) have a real author:** `authorUserId` stamped in `::save()` from `_Model::getRequestUserId()`. Copy that pattern for the real fix. See [Case status + stage gate](./case-status-and-stage-gate.md), [Customer related records](./customer-locations-files-notes.md).
|
|
67
67
|
- **`dtStamp` is `FIELD_DATETIME_CREATED`** — stamped by the ORM on insert, the only provenance a note actually carries.
|
|
68
68
|
|
|
69
69
|
## Change history
|
|
70
|
+
- 2026-10-17 — CustomerNotes added as a second server-stamped-author note model (TRUE-82870). (jcardinal)
|
|
70
71
|
- 2026-10-15 — Noted CaseNotes as the first note model with a server-stamped author. (jcardinal)
|
|
71
72
|
|
|
72
73
|
## Related
|
|
@@ -32,7 +32,7 @@ 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`. Any other route → 400. **A new root = one map entry + its Surface
|
|
35
|
+
`service-request-activity`, `customers` → `customer-activity`. Any other route → 400. **A new root = one map entry + its Surface
|
|
36
36
|
(`dbchanges2` `Core/`).** Engine (`Source` reads Logs, `Reader` checks as the reader through api2)
|
|
37
37
|
is generic; no new client ACL needed for a new root.
|
|
38
38
|
- **Children:** `Logs.RecordParent`, level by level, max `Source::MAX_DEPTH` (3) levels and
|
|
@@ -45,7 +45,7 @@ client `AclRecordScripts`. Data source: [record change audit log](record-change-
|
|
|
45
45
|
`isVisible = 0`). An UPDATE with no visible field is dropped. Secrets → `{redacted:true}`.
|
|
46
46
|
4. FK values → `{uuid, label[, slug]}`, `{missing:true}`, or `{value:null}` — never a raw id.
|
|
47
47
|
- **CREATE rows of `CREATE_HIDDEN_ROUTES`** (`ticket-notes`, `ticket-note-files`, `unit-notes`,
|
|
48
|
-
`case-notes`, `service-request-notes`) are dropped — the host screen merges the notes feed.
|
|
48
|
+
`case-notes`, `service-request-notes`, `customer-notes`) are dropped — the host screen merges the notes feed.
|
|
49
49
|
Note edits/deletes still show.
|
|
50
50
|
- **Paging:** cursor = `Logs.Record` id; `nextCursor` null at the end. Scan bounded
|
|
51
51
|
(`MAX_SCAN_BATCHES` × `SCAN_BATCH_SIZE`); a page cut by the bound still returns a `nextCursor`.
|
|
@@ -69,5 +69,6 @@ client `AclRecordScripts`. Data source: [record change audit log](record-change-
|
|
|
69
69
|
(schema + api2 change) — not built.
|
|
70
70
|
|
|
71
71
|
## Change history
|
|
72
|
+
- 2026-10-17 — Added root `customers` + `customer-notes` to `CREATE_HIDDEN_ROUTES` (TRUE-82870). (jcardinal)
|
|
72
73
|
- 2026-10-17 — One-FK rule for parent links; no request id for grouping (TRUE-82889). (jcardinal)
|
|
73
74
|
- 2026-10-15 — Added roots `units`, `cases`, `service-requests` + their note routes to `CREATE_HIDDEN_ROUTES` (TRUE-82836). (jcardinal)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Record hard delete (_Component_RecordDelete + PRE DELETE interceptor)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-17
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Component/RecordDelete/RecordDelete.php
|
|
13
|
+
- _underscore/Model/Client/Customer.php
|
|
14
|
+
- _underscore/Model/Client/Ticket.php
|
|
15
|
+
- _underscore/Model/Client/Case.php
|
|
16
|
+
- _underscore/Model/Client/ServiceRequest.php
|
|
17
|
+
related:
|
|
18
|
+
- ../../api2/features/api-payload-interceptors.md
|
|
19
|
+
- ./customer-locations-files-notes.md
|
|
20
|
+
- ./record-activity-timeline.md
|
|
21
|
+
---
|
|
22
|
+
Map-driven hard delete of everything hanging off one record (block / detach / delete children / orphan Files); open before making a record deletable or adding a table with a Files FK.
|
|
23
|
+
|
|
24
|
+
## Summary
|
|
25
|
+
|
|
26
|
+
`DELETE /v2/<route>/{uuid}` on an opted-in record runs a PRE DELETE interceptor that calls
|
|
27
|
+
`_Component_RecordDelete::purge(['model' => static::class, 'id' => $id])`. `purge()` cleans the related
|
|
28
|
+
rows from a map the model declares in `relatedRows()`; api2 then deletes the record row itself. Used by
|
|
29
|
+
Tickets, Cases, ServiceRequests, Customers. The class DocBlock is the full contract — read it before editing.
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
- **Opt in (3 steps):** `public static function relatedRows(): array` on the model; `preDelete(&$api, &$payload)`
|
|
34
|
+
that calls `purge()` for a route-uuid delete; a `Core.ApiPayloadInterceptors` row (recordId, `PRE`, `DELETE`).
|
|
35
|
+
Cleanup must be PRE — a POST DELETE interceptor never runs (see api-payload-interceptors).
|
|
36
|
+
- **Map keys** (all optional): `blockers` (any match → 400 `_Exception_Validation`, nothing deleted), `detach`
|
|
37
|
+
(`UPDATE … SET col = NULL`, column must be nullable), `files` (collect file ids first), `polymorphic` (run
|
|
38
|
+
before children), `children` (ordered set-based DELETEs; grandchildren first). Entries: `{table, column, via?, where?}`.
|
|
39
|
+
- **Run order:** lock record row → blockers → collect file ids → detach → polymorphic → children → orphan Files.
|
|
40
|
+
- **Files:** a `Files` row is deleted only if no table in `FILE_REFERENCE_TABLES` still points at it. Storage
|
|
41
|
+
objects (S3) are never touched.
|
|
42
|
+
- **Transaction:** first statement is a no-op UPDATE on the record row — it opens api2's request transaction and
|
|
43
|
+
takes the lock; any failure rolls the whole request back. All statements on the write connection, one DB.
|
|
44
|
+
- **Customer rule (Records 113, 2026-10-17):** block on Contacts / SalesOrders / PurchaseOrders; detach Tickets /
|
|
45
|
+
ServiceRequests / Cases `customerId`; delete Customers_Catalogs, Customers_Subscriptions, Locations_Customers,
|
|
46
|
+
Customers_Files, CustomerNotes rows; orphan Files from Customers_Files.
|
|
47
|
+
|
|
48
|
+
## Gotchas
|
|
49
|
+
|
|
50
|
+
- **New table with a `fileId` FK → add it to `FILE_REFERENCE_TABLES`.** Missing it makes the Files DELETE fail on
|
|
51
|
+
the FK and roll back every delete that touches a shared file (loud, not silent).
|
|
52
|
+
- **Client override of `relatedRows()` must merge `parent::relatedRows()` key by key;** a client `preDelete`
|
|
53
|
+
replaces the shared one and must call `parent::preDelete()`.
|
|
54
|
+
- **Use `$api->recordUuid ?? null`, never `isset($api->recordUuid)`** — `_Component` has `__get` but no `__isset`.
|
|
55
|
+
- **Load the record on the write connection** (read host off) — the read replica can lag this request's writes.
|
|
56
|
+
- **`detach` is for business records that outlive the parent** (e.g. Tickets.customerId); use `blockers` when
|
|
57
|
+
losing the link would lose meaning (orders, required FKs like `Contacts.customerId`).
|
|
58
|
+
|
|
59
|
+
## Change history
|
|
60
|
+
- 2026-10-17 — Created; Customers opted in, Customers_Files added to FILE_REFERENCE_TABLES (TRUE-82870). (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-17
|
|
10
10
|
owners: [bala, apeterson, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/TableView.php
|
|
@@ -31,6 +31,7 @@ related:
|
|
|
31
31
|
- ./acl-permission-chain.md
|
|
32
32
|
- ../../toga25-supply/features/transfer-orders-page.md
|
|
33
33
|
- ../../toga25-supply/features/record-modals-and-nested-tables.md
|
|
34
|
+
- ./customer-locations-files-notes.md
|
|
34
35
|
---
|
|
35
36
|
|
|
36
37
|
How `TableViewJoins` rows become a view's join SQL — aliasing, chained multi-hop joins, ACL gating, and the three ways a view breaks; open before adding/rebasing a join or debugging a view 500 / empty grid.
|
|
@@ -188,6 +189,15 @@ Cause: an **INNER** `TableViewJoins` on a foreign-key column that is **unpopulat
|
|
|
188
189
|
- **Fix / avoid:** for an **optional / possibly-empty** FK, use **LEFT OUTER** (rows survive with nulls in the joined columns) or omit the column entirely. Only use INNER when the FK is guaranteed populated for the tenant.
|
|
189
190
|
- **Check before you add:** verify FK population in the target tenant (`SELECT COUNT(*) ... WHERE <fk> IS NOT NULL`) before choosing INNER. NYCHH's `purchase-orders` ultimately **dropped** the Location (and Ship To) column rather than switch to OUTER.
|
|
190
191
|
|
|
192
|
+
### A many-to-many join in a SHARED view duplicates rows in every listing that uses it
|
|
193
|
+
|
|
194
|
+
blox sends **every** join of a view on **every** request (`buildJoinOptions`), filtered or not. So an OUTER join to a
|
|
195
|
+
many-to-many bridge added "only for the parent-scoped tab" also runs in the plain listing, and each row repeats once
|
|
196
|
+
per bridge match. Fix: a **separate parent-scoped TableView** that carries the bridge joins; leave the general view
|
|
197
|
+
alone. Example (2026-10-17): `desk-customer-locations` (Locations → Locations_Customers → Customers, read only with
|
|
198
|
+
`Customers.uuid = <customer>`) beside the unchanged `desk-locations` (`dbchanges2/Client/2026-10-17b`). A 1:0..1 join
|
|
199
|
+
(single FK each hop) is safe to add to a shared view.
|
|
200
|
+
|
|
191
201
|
### Context RecordFieldSettings are OPTIONAL for a joined column to render
|
|
192
202
|
|
|
193
203
|
`meta()` looks up `contextRecordFieldId`-scoped settings (`DefaultRecordFieldSettings` plus the Client / Persona / User / Language `RecordFieldSettings` tables) only to decide **which settings row supplies the column label**. When no context row exists for `(recordFieldId, contextRecordFieldId)`, it nulls `contextTable` / `contextField` and falls back to the field's own settings. A new joined column therefore does **not** require adding `RecordFieldSettings` rows just to appear — those rows are only needed to control the **header text** (see [page-meta-context-field-settings](./page-meta-context-field-settings.md), which is also where the "header renders as a camelCase slug" fallback is explained).
|
|
@@ -214,6 +224,10 @@ Core-level reference ids used (identical across all clients and environments): *
|
|
|
214
224
|
- **An unused join can already exist.** Confirm before inserting; a duplicate join of the same table is what breaks chained joins.
|
|
215
225
|
- **An INNER join to an EMPTY FK empties the whole grid with NO error.** Third failure mode: an inner join on an FK that is unpopulated for the tenant drops every row (meta looks fine, data returns 0). Use LEFT OUTER for an optional FK, or verify FK population before choosing INNER.
|
|
216
226
|
- **`PurchaseOrderItems` has no `itemId` — it reaches the catalog via `vendorItemId → VendorItems → Items`.** Converting a sales-order-item view (which joins Items directly) to purchase-order-items needs an extra **VendorItems** hop inserted before Items, with the Items join repointed to `VendorItems.itemId`. Don't just recolumn.
|
|
227
|
+
- **Never add a many-to-many (bridge) OUTER join to a shared view** — every listing on that view gets duplicate rows. Use a separate parent-scoped view.
|
|
228
|
+
|
|
229
|
+
## Change history
|
|
230
|
+
- 2026-10-17 — Many-to-many join in a shared view duplicates rows; use a parent-scoped view (TRUE-82870). (jcardinal)
|
|
217
231
|
|
|
218
232
|
## Related
|
|
219
233
|
|
|
@@ -156,6 +156,8 @@ VALUES (4188, '<pre-generated v4 uuid>', 219, 'serviceAddressId');
|
|
|
156
156
|
|
|
157
157
|
Because the `id`s must be reserved by a human, **this cannot be guessed or derived** — asking is mandatory. Record the assigned values in a comment at the top of the file so the next reader knows they were allocated, not invented.
|
|
158
158
|
|
|
159
|
+
**`/finish` always reports the central ids** — every `Records` / `RecordFields` id the ticket inserted (id → route / route.field) plus the next free ids, or "none used by this ticket". Every time, never skipped: the developer copies the next ids into Tools → Developers → Central IDs.
|
|
160
|
+
|
|
159
161
|
#### The next free `id` is NOT the same in every environment
|
|
160
162
|
|
|
161
163
|
The rule above keeps *existing* ids aligned, but each environment's `Core` has drifted independently, so **the next free value differs**. Verified 2026-08-17: the same new `ServiceRequests._serialNumbers` recordField took id **2483 in production** and **2766 in dev-sandbox**. Consequences:
|
|
@@ -398,5 +400,6 @@ The extra `SELECT … FROM ( … ) existing` layer both dodges error 1093 and pi
|
|
|
398
400
|
|
|
399
401
|
## Change history
|
|
400
402
|
|
|
403
|
+
- 2026-10-17 — /finish reports central ids used + next free (TRUE-82889). (jcardinal)
|
|
401
404
|
- 2026-10-17 — Second same-folder file also allowed for interceptor rows that must run after the code deploy (TRUE-82889). (jcardinal)
|
|
402
405
|
- 2026-10-13 — Added rule 15: no MySQL `CHECK` constraints (TRUE-82715). (jcardinal)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
21
21
|
|
|
22
22
|
## 2.0 framework
|
|
23
23
|
|
|
24
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
24
|
+
- **_underscore** (_Underscore) _(framework core)_ — 103 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
25
25
|
- **worker2** (Worker) — 90 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
26
26
|
- **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
27
27
|
- **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
package/package.json
CHANGED
|
@@ -69,6 +69,9 @@ rows for the developer to write by hand.
|
|
|
69
69
|
4. **Put verify SELECTs at the end of the file.** After the developer runs it, Claude runs
|
|
70
70
|
those SELECTs itself (local DB, or `toga-db` read-only) and reports the result in plain
|
|
71
71
|
words. Never ask the developer to read verify output.
|
|
72
|
+
5. **Central ids at `/finish`** (team rule, 2026-10-17, jcardinal). When a ticket inserts
|
|
73
|
+
`Core.Records` / `Core.RecordFields` rows, the `/finish` report always lists the ids used
|
|
74
|
+
and the next free ids.
|
|
72
75
|
|
|
73
76
|
## No running commentary — one compiled report at the end (team rule, 2026-10-15)
|
|
74
77
|
|
package/skills/finish/SKILL.md
CHANGED
|
@@ -460,6 +460,8 @@ CU-<internalId>
|
|
|
460
460
|
|
|
461
461
|
## Migration steps
|
|
462
462
|
- <dbchanges2 file + database + order, config changes, queue registration — or "None">
|
|
463
|
+
- Central ids: <Records / RecordFields ids used, then "next free: Records <n>, RecordFields <n>"
|
|
464
|
+
— or "none used"> (dbchanges2 PRs only; same data as the Step 17 block)
|
|
463
465
|
|
|
464
466
|
## Review summary
|
|
465
467
|
**Risk: <LEVEL>**
|
|
@@ -578,10 +580,27 @@ developer the list name and leave the status alone.
|
|
|
578
580
|
Short list:
|
|
579
581
|
|
|
580
582
|
1. KB: publish status (Step 5).
|
|
581
|
-
2.
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
583
|
+
2. **Central ids — REQUIRED, always present, never skipped** (team rule, 2026-10-17). Its own
|
|
584
|
+
block with a clear heading, right after the KB line: the developer must copy the next ids
|
|
585
|
+
into Tools → Developers → Central IDs.
|
|
586
|
+
- Source: grep the ticket branch's dbchanges2 `Core/` SQL files for `INSERT INTO Records` /
|
|
587
|
+
`INSERT INTO RecordFields` ids. Confirm on the local DB:
|
|
588
|
+
`SELECT id, … FROM Core.Records WHERE id IN (…)`, the same for `Core.RecordFields`, and
|
|
589
|
+
`SELECT MAX(id)` on both.
|
|
590
|
+
- List every id: `Records <id> → <route>`, `RecordFields <id> → <route>.<field>`.
|
|
591
|
+
- Then: `Next free: Records <n>, RecordFields <n>` — n = the higher of (this ticket's top
|
|
592
|
+
id, local `MAX(id)`) + 1. Central IDs stays the source of truth.
|
|
593
|
+
- None inserted → write `Central ids: none used by this ticket.`
|
|
594
|
+
```
|
|
595
|
+
**Central ids**
|
|
596
|
+
- Records 412 → desk/tickets
|
|
597
|
+
- RecordFields 9031 → desk/tickets.priority
|
|
598
|
+
Next free: Records 413, RecordFields 9032
|
|
599
|
+
```
|
|
600
|
+
3. Ticket fields changed: before → after with the reason, one line each (or "none").
|
|
601
|
+
4. PRs: one url per repo, and which were created vs. updated.
|
|
602
|
+
5. PM comment: posted (yes/no), with the posted text.
|
|
603
|
+
6. Status: `development finished` (or why not).
|
|
604
|
+
7. Anything manual the developer must still do — as a numbered list.
|
|
586
605
|
|
|
587
606
|
After that, if the thread may continue another day, suggest `/session-save <slug>`.
|