toga-ai 1.0.1073 → 1.0.1075
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/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +7 -1
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/acl-capabilities.md +84 -0
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +11 -3
- package/knowledge/2.0/apps/_underscore/features/record-hard-delete.md +4 -1
- package/knowledge/2.0/apps/_underscore/features/record-meta-endpoint.md +14 -6
- package/knowledge/2.0/apps/dbchanges2/architecture.md +15 -1
- package/knowledge/2.0/standards/framework-rules.md +5 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/nychh/profile.md +3 -1
- package/knowledge/clients/true/profile.md +2 -1
- package/package.json +1 -1
- package/rules/common/communication.md +11 -0
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -1868,6 +1868,11 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
|
|
|
1868
1868
|
`c_netsuiteInternalLocationId` = <id>, then rerun." It replaced 7 `POST /locations` spots: receipt pre-check over
|
|
1869
1869
|
ALL lines before any write, receipt + adjustment unit paths, TO origin + destination. A person verifies and creates
|
|
1870
1870
|
the Location (client needs a Warehouse `LocationTypes` row, shared uuid `78c24f51-b33b-438b-e3f2-b6232d9ecb75`).
|
|
1871
|
+
- **Unknown warehouse holding the client's OWN orders → add the Location row; do not turn on
|
|
1872
|
+
`IS_ENABLED_SKIP_TRANSFER_ORDERS_FROM_UNKNOWN_WAREHOUSE`** (skip is for foreign locations, e.g. Elite's 5 "Main";
|
|
1873
|
+
it drops real orders). A cursor rewind re-reads old orders, so old parent locations surface. NYCHH 2026-10-18
|
|
1874
|
+
(TRUE-82859): Issue 1041, NS location 1 "New York Warehouse" (2021 TO #91091) → `Client_Nychh/2026-10-18a -
|
|
1875
|
+
NewYorkWarehouseLocation.sql`; self-healed, no mode reset. See [NYCHH profile](../../../../clients/nychh/profile.md).
|
|
1871
1876
|
- **Unit location comes from the receipt/adjustment LINE, not `custitemnumber_location`.** That field is a bin/bay
|
|
1872
1877
|
custom list whose ids overlap real location ids, so reading it as a location put units in the wrong warehouse.
|
|
1873
1878
|
|
|
@@ -2163,6 +2168,7 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
|
|
|
2163
2168
|
[NetSuite Sync Alert Monitor](../../library/features/netsuite-sync-alert-monitor.md).
|
|
2164
2169
|
|
|
2165
2170
|
## Change history
|
|
2171
|
+
- 2026-10-18 — NYCHH SALES_ORDERS froze on unknown warehouse NS location 1 (Issue 1041) after the 10-14 cursor rewind; fixed by adding the Location row, not the skip flag. Data only. TRUE-82859. (jcardinal)
|
|
2166
2172
|
- 2026-10-16 — NYCHH SALES_ORDERS froze on a TO line removed in NetSuite (FK 1451, Issue 445); TO-line delete now clears PO/SO links first. Library only, prod-verified. TRUE-82859. (jcardinal)
|
|
2167
2173
|
- 2026-10-09 — Customer PO duplicate lines: merge step (all TO clients) + SO-QTY opt-in (hh); [doc](../../library/features/customer-po-line-merge-and-quantity.md). Library + worker deploy together. TRUE-82606. (jcardinal)
|
|
2168
2174
|
- 2026-10-09 — **Item-fulfillment tracking numbers now carry a carrier** (TRUE-82527). The fan-out passes the fulfillment’s shipping-method carrier into ; the number decides, the method is only a fallback, and no carrier row is ever created. (bala)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|-----|---------|
|
|
5
5
|
| [Proposed — git-sourced base+overlay JSON authoring for the Surface layer](architecture/surface-authoring-proposal.md) | Draft proposal (NOT implemented) to move Surface authoring off hand-written SQL onto git-sourced base+overlay JSON; open only for that design rationale. |
|
|
6
6
|
| [_underscore Framework Architecture](architecture.md) | The 2.0 `_underscore` PHP backend framework — DB abstraction, ORM models, routing, ACL, Surface, multi-tenant clusters; open for how any 2.0 app backend works. |
|
|
7
|
+
| [ACL Capabilities (roles = collections of capabilities; dual-read transition; role delete)](features/acl-capabilities.md) | Client-DB ACL layer between roles and permissions (Capabilities, Roles_Capabilities, capabilityId on ACL rows); open before writing any client ACL grant, readin |
|
|
7
8
|
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not |
|
|
8
9
|
| [Address Uniqueness Normalization (unit identifier + 5-digit ZIP comparison)](features/address-uniqueness-normalization.md) | Two reusable normalization primitives for any "one X per physical address" rule; open when comparing addresses for uniqueness. |
|
|
9
10
|
| [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | US address validation via a carrier waterfall (USPS → FedEx → UPS) exposed as a scripted endpoint; open when validating/normalizing addresses. |
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ACL Capabilities (roles = collections of capabilities; dual-read transition; role delete)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-18
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/Capability.php
|
|
13
|
+
- _underscore/Model/Client/Roles/Capability.php
|
|
14
|
+
- _underscore/Model/Client/Role.php
|
|
15
|
+
- _underscore/Model/Client/AclRecordPermission.php
|
|
16
|
+
- _underscore/Model/Client/AclFieldPermission.php
|
|
17
|
+
- _underscore/Model/Client/AclCustomFieldPermission.php
|
|
18
|
+
- api2/Component/Api/V2/V2.php
|
|
19
|
+
- dbchanges2/Client/2026-10-18b - TRUE-82880 Capabilities.sql
|
|
20
|
+
- dbchanges2/Core/2026-10-18e - TRUE-82880 CapabilitiesCore.sql
|
|
21
|
+
related:
|
|
22
|
+
- acl-permission-chain.md
|
|
23
|
+
- record-hard-delete.md
|
|
24
|
+
- ../../api2/features/api-key-authentication-and-scoping.md
|
|
25
|
+
---
|
|
26
|
+
Client-DB ACL layer between roles and permissions (Capabilities, Roles_Capabilities, capabilityId on ACL rows); open before writing any client ACL grant, reading ACL rows in SQL, deleting a role, or dropping `roleId`.
|
|
27
|
+
|
|
28
|
+
## Summary
|
|
29
|
+
|
|
30
|
+
A **Capability** (`Client_*.Capabilities`, Core.Records **409**) is a per-client named set of
|
|
31
|
+
`AclRecordPermissions` (+ their logic groups), `AclFieldPermissions` and `AclCustomFieldPermissions`
|
|
32
|
+
rows, linked by a new `capabilityId` column. A **Role** is a set of capabilities
|
|
33
|
+
(`Roles_Capabilities`, Records **410**). Access = **union** of all capabilities of all the user's
|
|
34
|
+
roles. No per-role or per-user overrides. Authoring UI = Hub (later). Built TRUE-82880.
|
|
35
|
+
|
|
36
|
+
**Not capability-keyed (stay role-keyed):** `AclActionPermissions`, `AclRecordScripts`,
|
|
37
|
+
`AclCustomRecordScripts`, `Users_Roles`, `Apis_Roles`. **Core DB ACL stays role-based** — capabilities
|
|
38
|
+
exist only for `aclDatabase = 'CLIENT'` records.
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
- **JWT unchanged** — it still carries role ids. Per request, `V2::buildLookups()` expands
|
|
43
|
+
role → capability ids (`buildClientRoleIdsByCapabilityId`).
|
|
44
|
+
- **Dual-read (transition):** client ACL rows load with
|
|
45
|
+
`capabilityId IN (<caps>) OR (capabilityId IS NULL AND roleId IN (<roles>))`
|
|
46
|
+
(`buildClientAclDualReadCondition`). So legacy role-only rows still work.
|
|
47
|
+
- **Lookup buckets unchanged:** `[db][roleId][recordId]`, keyed by permission id, so a row reached
|
|
48
|
+
through two roles is not counted twice. `resolveClientAclRowRoleIds` maps a capability row back to
|
|
49
|
+
every role holding that capability.
|
|
50
|
+
- **Field permissions merge writable-wins:** same field from two capabilities → writable if any says writable.
|
|
51
|
+
- **`AclFieldPermissions.appId`:** NULL = all apps; otherwise must equal the request's app.
|
|
52
|
+
- **Migration** (`Client/2026-10-18b`, dynamic, re-runnable): one capability per role, named after
|
|
53
|
+
the role, linked only to that role; existing ACL rows get its `capabilityId`. `roleId` stays filled
|
|
54
|
+
and becomes nullable.
|
|
55
|
+
- **`roleId` drop is later:** its own forward `dbchanges2` file, after a release. Do the gotchas
|
|
56
|
+
below first.
|
|
57
|
+
|
|
58
|
+
### Role delete (`_Model_Client_Role::preDelete`, PRE DELETE interceptor on record 274)
|
|
59
|
+
|
|
60
|
+
- Gated only by `allowDelete` on `roles` ACL. Hard delete. **No special system-role logic.**
|
|
61
|
+
- **Blocked (400)** while any `Users_Roles` or `Apis_Roles` row holds the role.
|
|
62
|
+
- Uses [record hard delete](record-hard-delete.md) `relatedRows()`: deletes `Roles_Capabilities`
|
|
63
|
+
links and role-keyed rows (`AclActionPermissions`, `AclRecordScripts`, `AclCustomRecordScripts`,
|
|
64
|
+
`Announcements_Roles`, `ApprovalTemplateStageAllowedRoles`, `SurfaceOverrides`).
|
|
65
|
+
- Then `removeAclRows()`: legacy rows (`capabilityId IS NULL`) are deleted (record permissions with
|
|
66
|
+
their logic groups first); capability rows are kept with `roleId = NULL`. **Capabilities are never
|
|
67
|
+
deleted with a role.**
|
|
68
|
+
|
|
69
|
+
## Gotchas
|
|
70
|
+
|
|
71
|
+
- **⚠ Before the `roleId` drop: worker2 `Worker/Sync/TrackingNumbers.php` (~L460) still joins
|
|
72
|
+
`AclRecordPermissions` / `AclFieldPermissions` on `roleId`.** Move it to the capability path first.
|
|
73
|
+
Grep every repo for raw-SQL reads of `roleId` on these three tables too.
|
|
74
|
+
- **⚠ New unique keys with NULL `appId` / `indirectRecordId` do not block duplicates** (MySQL treats
|
|
75
|
+
NULLs as distinct). Today the old role-based keys still protect; after the `roleId` drop they do
|
|
76
|
+
not — guard inserts with `NOT EXISTS`.
|
|
77
|
+
- **Writers (Hub, migrations) must write `roleId = NULL` on new capability rows.** A filled `roleId`
|
|
78
|
+
makes the row look legacy-shaped and keeps it alive on the old key.
|
|
79
|
+
- **⚠ Open (security, pre-existing):** the client `Base` role (every user holds it) has full CRUD on
|
|
80
|
+
`user-roles`, `roles`, `acl-field-permissions`, `acl-custom-field-permissions` — a user can grant
|
|
81
|
+
themselves more access. Developer decision: fix when Hub is built.
|
|
82
|
+
|
|
83
|
+
## Change history
|
|
84
|
+
- 2026-10-18 — Created: Capabilities layer, dual-read, writable-wins field merge, role delete rules (TRUE-82880). (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: ["jcardinal", "mhammontree", "tcox", "bala", "apeterson", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -28,6 +28,11 @@ inserting only an `AclRecordPermissions` row is the single most common mistake
|
|
|
28
28
|
silently does nothing without the rest of the chain. A complete grant requires **all four** tables,
|
|
29
29
|
plus `AclFieldPermissions` for the fields to be readable/writable.
|
|
30
30
|
|
|
31
|
+
**Client-DB ACL rows now belong to a Capability** (`capabilityId`), and roles are sets of
|
|
32
|
+
capabilities; legacy `roleId`-only rows still work during the transition (dual-read). Read
|
|
33
|
+
[ACL Capabilities](acl-capabilities.md) before writing or querying client ACL rows. Core DB ACL is
|
|
34
|
+
still role-based.
|
|
35
|
+
|
|
31
36
|
## The complete chain (all four are required)
|
|
32
37
|
|
|
33
38
|
```
|
|
@@ -412,7 +417,8 @@ first (see
|
|
|
412
417
|
- **Two separate role spaces with different ids and names:** Core roles (`Core.Roles`: Public, Integrations, Super User, Base) and per-tenant client roles (`Client_<X>.Roles`). Ids are not comparable across spaces.
|
|
413
418
|
- `AclRecordPermissions.roleId` / `AclFieldPermissions.roleId` are read in the record's `aclDatabase` role space: CORE record → Core role id; CLIENT record → client role id. `roleId=1` is Core Public on a Core record but a client role on a client record — always filter by the record's `aclDatabase`.
|
|
414
419
|
- **Link:** `Client.Roles.coreRoleId`. At login the user's client roles map to Core roles through their distinct non-null `coreRoleId`s, set as `id.core.roles`. CORE records filter on the user's Core roles; CLIENT records on their client roles.
|
|
415
|
-
- **No inheritance:** no parent role / hierarchy; roles are used verbatim by the resolver.
|
|
420
|
+
- **No inheritance:** no parent role / hierarchy; roles are used verbatim by the resolver. Client roles
|
|
421
|
+
reach client ACL rows through their capabilities (union) — see [ACL Capabilities](acl-capabilities.md).
|
|
416
422
|
- **Public = read-only baseline every authenticated user inherits.** Public is MERGED (additively) into the effective core roles at token build, `processRoutePairs`, and `buildLookups`; without the merge the mapped `coreRoleId`s overwrite it and a role like Core Base cannot read Core metadata (e.g. `Core.Clients.uuid`). Keep Public read-only: never grant it write on Core metadata.
|
|
417
423
|
- Related: no backend ACL cache in 2.0 — a stale 403 is the UI cache or ACL logic, never a backend cache.
|
|
418
424
|
|
|
@@ -925,11 +931,13 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
|
|
|
925
931
|
NULL plus `sqlExpression='1'` means no row scoping whatsoever (see the section above).
|
|
926
932
|
- [ ] **No business-critical column is `isWritable = 1` for `Base`** unless customers are genuinely
|
|
927
933
|
meant to write it via plain CRUD.
|
|
928
|
-
- [ ] For `aclDatabase = 'CLIENT'` records: rows go in client DB(s); resolve `roleId` by subselect
|
|
934
|
+
- [ ] For `aclDatabase = 'CLIENT'` records: rows go in client DB(s); resolve `roleId` by subselect;
|
|
935
|
+
set `capabilityId` (see [ACL Capabilities](acl-capabilities.md)).
|
|
929
936
|
- [ ] The field is declared on the **generated** `_underscore/Model/Client/<Name>.php` (else `EO-1`),
|
|
930
937
|
and every repo is on the **same branch** so the generated model matches the DB.
|
|
931
938
|
|
|
932
939
|
## Change history
|
|
940
|
+
- 2026-10-18 — Client ACL rows now carry `capabilityId`; linked the new Capabilities doc (TRUE-82880). (jcardinal)
|
|
933
941
|
- 2026-09-21 — Named the mechanism behind the half-built chain: `_Model_Core_AclRecordPermission::buildSqlExpression`
|
|
934
942
|
returns **`'(0)'`** with no logic group, so `allowRead = 1` alone denies everything (second symptom: 403 listing
|
|
935
943
|
`aclRecordPermissionIds` with `[null, null, null]`). Added that a `CLIENT` record's grant must exist in every
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/RecordDelete/RecordDelete.php
|
|
@@ -53,6 +53,8 @@ Tickets, Cases, ServiceRequests, Customers, Contacts. The class DocBlock is the
|
|
|
53
53
|
ContactNotes, ContactAttempts, ContactMessagingOptIns, Campaigns_Contacts, Campaigns_MeetWithContacts,
|
|
54
54
|
Contacts_Tags, Contacts_Vendors, Contacts_Locations, Locations_Contacts, ContactAddresses / EmailAddresses /
|
|
55
55
|
PhoneNumbers. `preDelete` clears the contact's `primary*` pointers first (they and the children point at each other).
|
|
56
|
+
- **Role rule (Records 274):** blocked by Users_Roles / Apis_Roles; ACL rows handled after `purge()` — full rule in
|
|
57
|
+
[ACL Capabilities § Role delete](acl-capabilities.md).
|
|
56
58
|
|
|
57
59
|
## Gotchas
|
|
58
60
|
|
|
@@ -71,5 +73,6 @@ Tickets, Cases, ServiceRequests, Customers, Contacts. The class DocBlock is the
|
|
|
71
73
|
losing the link would lose meaning (orders, required FKs like `Contacts.customerId`).
|
|
72
74
|
|
|
73
75
|
## Change history
|
|
76
|
+
- 2026-10-18 — Roles opted in (TRUE-82880). (jcardinal)
|
|
74
77
|
- 2026-10-17 — Contacts opted in (block / detach / cascade map, schema-drift probes); Contacts_Files added to FILE_REFERENCE_TABLES (TRUE-82871). (jcardinal)
|
|
75
78
|
- 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-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Core/Record.php
|
|
@@ -22,15 +22,22 @@ Per-route record meta (names, label/sublabel field, per-field display labels); o
|
|
|
22
22
|
## Summary
|
|
23
23
|
|
|
24
24
|
`GET /v2/records/meta?route=<route>` → `_Model_Core_Record::meta()`. Returns
|
|
25
|
-
`{route, name, pluralName, labelField, sublabelField, identifierField, fieldLabels}`.
|
|
25
|
+
`{route, name, pluralName, icon, iconWeight, labelField, sublabelField, identifierField, fieldLabels}`.
|
|
26
|
+
Batch: `?routes=a,b,c` (max `META_BATCH_MAX_ROUTES` = 400; callers chunk) →
|
|
27
|
+
`{records: {<route>: {route, name, pluralName, icon, iconWeight}}}` — light shape, no field labels.
|
|
28
|
+
Record-type icons come only from here (`Core.Records.iconToken` / `iconWeightToken`); apps never
|
|
29
|
+
hardcode them (rule: dbchanges2 architecture → *`Core.Records.name` + icons*).
|
|
26
30
|
`fieldLabels` = `{<field>: "<label>"}` — the data-driven visible label of each field, in the session
|
|
27
31
|
language. Platform rule: **a field's visible label comes from `Core.RecordFields.labelMessageId` via
|
|
28
32
|
`fieldLabels`, never hardcoded** (platform apps' `PLATFORM.md` rule 15).
|
|
29
33
|
|
|
30
34
|
## How it works
|
|
31
35
|
|
|
32
|
-
- **Names:** `
|
|
33
|
-
`
|
|
36
|
+
- **Names:** `name` / `pluralName` from Messages via `_Model_Core_Surface::resolveMessageValues()`;
|
|
37
|
+
fallback order: Message → frozen `DefaultRecordSettings` → `Core.Records.name`.
|
|
38
|
+
- **Batch (`metaForRoutes`):** one Records query, one Messages lookup, one `DefaultRecordSettings` query for all routes.
|
|
39
|
+
- **Response path:** record-script nesting adds the payload key — batch is read at
|
|
40
|
+
`data.records.meta.records.<route>` (see [record scripts](../../api2/features/record-scripts.md)).
|
|
34
41
|
- **`fieldLabels`:** `resolveFieldLabels()`:
|
|
35
42
|
1. Caller must have record READ (`aclRecordFlags`), else `{}`.
|
|
36
43
|
2. Only fields the caller may READ (`$api->getAclFieldPermissions`, same resolver as
|
|
@@ -47,11 +54,12 @@ language. Platform rule: **a field's visible label comes from `Core.RecordFields
|
|
|
47
54
|
## Gotchas
|
|
48
55
|
|
|
49
56
|
- **⚠ Open (security): the base meta has NO record READ check.** `route`, `name`, `pluralName`,
|
|
50
|
-
`labelField`, `sublabelField` come back for any route to any caller (pre-existing
|
|
51
|
-
`fieldLabels` is gated. A caller can probe which routes exist (unknown route → 400
|
|
57
|
+
`labelField`, `sublabelField`, `icon` come back for any route to any caller (pre-existing; the
|
|
58
|
+
batch form too); only `fieldLabels` is gated. A caller can probe which routes exist (unknown route → 400
|
|
52
59
|
`No record found for route`). Not fixed yet — Desk question `lx`.
|
|
53
60
|
- `fieldLabels` is **per caller** (field ACL + language) — never cache it across users or tenants.
|
|
54
61
|
The rest of the meta is the same for every tenant.
|
|
55
62
|
|
|
56
63
|
## Change history
|
|
64
|
+
- 2026-10-18 — Added `icon` / `iconWeight`, name fallback to `Core.Records.name`, and batch `?routes=` (max 400) (TRUE-82880). (jcardinal)
|
|
57
65
|
- 2026-10-15 — Added `fieldLabels` (gated by record + field READ) and blox `fieldLabelOf`; noted the ungated base meta (TRUE-82836). (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: [jcardinal, mhammontree, bala, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- Core/
|
|
@@ -141,6 +141,19 @@ These **two tables, and only these two**, have **team-maintained primary keys**.
|
|
|
141
141
|
|
|
142
142
|
This is what makes the *Database isolation* rule practical — it is the reason a `Client_<Tenant>` migration never needs to read `Core` to find a `recordFieldId`.
|
|
143
143
|
|
|
144
|
+
### `Core.Records.name` + icons (team rule 2026-10-18, TRUE-82880)
|
|
145
|
+
|
|
146
|
+
- `name` is REQUIRED (NOT NULL), written for a person: Title Case, PLURAL, built from the route. `sales-orders` → "Sales Orders". Acronyms stay upper case: "ACL Record Permissions".
|
|
147
|
+
- Bridge record = BOTH sides are real tables/records. Both sides plural: "Locations <-> Location Attributes", "Users <-> Roles", "Users <-> Files".
|
|
148
|
+
- Child table whose second part is not its own table is NOT a bridge: one plural name ("Location Hours", not "Location <-> Hours"); table `LocationHours`, not `Locations_Hours`.
|
|
149
|
+
- Typed variants: type first: `support-methods-chat` → "Chat Support Methods".
|
|
150
|
+
- Product / feature names keep their form ("Meet With Contacts"). Uncountable words stay singular ("User Memory", "Conversation History", "Item Media"). Acronyms upper case ("AI Models").
|
|
151
|
+
- Routes match the table words: `service-request-note-types`, not `service-request-notes-types`.
|
|
152
|
+
- Change history: 2026-10-18 TRUE-82880 refined bridge/child/typed/uncountable rules.
|
|
153
|
+
- Every new `Core.Records` insert sets `name` this way.
|
|
154
|
+
- Two layers: `Core.Records.name` = base name, the fallback everywhere; `nameMessageId` / `pluralNameMessageId` Messages = translatable UI text, wins when set.
|
|
155
|
+
- Record-type icons live in `Core.Records.iconToken` (+ `iconWeightToken`): one icon per record platform-wide; the nav icon is the source when choosing. Apps read it from the records meta, never hardcode record icons. Stored as camelCase Font Awesome names without the `fa-` prefix (blox `toFaClass` normalizes).
|
|
156
|
+
|
|
144
157
|
### Inserting into them — ASK FIRST, never rely on AUTO_INCREMENT
|
|
145
158
|
|
|
146
159
|
**When a change inserts into `Core.Records` or `Core.RecordFields`, stop and ask the developer for the next `id` value(s)**, then write them as explicit literals:
|
|
@@ -400,6 +413,7 @@ The extra `SELECT … FROM ( … ) existing` layer both dodges error 1093 and pi
|
|
|
400
413
|
|
|
401
414
|
## Change history
|
|
402
415
|
|
|
416
|
+
- 2026-10-18 — Core.Records.name required + human naming; record icons in Core.Records (TRUE-82880). (jcardinal)
|
|
403
417
|
- 2026-10-17 — /finish reports central ids used + next free (TRUE-82889). (jcardinal)
|
|
404
418
|
- 2026-10-17 — Second same-folder file also allowed for interceptor rows that must run after the code deploy (TRUE-82889). (jcardinal)
|
|
405
419
|
- 2026-10-13 — Added rule 15: no MySQL `CHECK` constraints (TRUE-82715). (jcardinal)
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-10-
|
|
8
|
+
updated: 2026-10-18
|
|
9
9
|
owners: [jcardinal, ajean, apeterson, mhammontree]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -171,6 +171,10 @@ literal rather than a lookup — including in SQL embedded in PHP in `worker2` /
|
|
|
171
171
|
`_underscore`. Always add a comment naming the record/field the number refers to; a bare `4188` is
|
|
172
172
|
unverifiable later.
|
|
173
173
|
|
|
174
|
+
**Naming + icons.** `Core.Records.name` is required: Title Case, plural, from the route
|
|
175
|
+
("Sales Orders"; bridge records, both sides plural: "Users <-> Roles"; child tables are not bridges: "Location Hours"). Record icons live in `Core.Records.iconToken`
|
|
176
|
+
(+ `iconWeightToken`), never hardcoded in apps. Rule + examples: dbchanges2 architecture, same section.
|
|
177
|
+
|
|
174
178
|
This is also what makes the `dbchanges2` isolation rule above workable: a `Client_<Tenant>` change
|
|
175
179
|
never has to read `Core` to resolve a `recordFieldId`. Full detail and examples:
|
|
176
180
|
`2.0/apps/dbchanges2/architecture.md` → *`Core.Records` / `Core.RecordFields`*.
|
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)_ — 105 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) — 27 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)
|
|
@@ -19,9 +19,10 @@ project: _Underscore
|
|
|
19
19
|
client: nychh
|
|
20
20
|
type: profile
|
|
21
21
|
status: active
|
|
22
|
-
updated: 2026-10-
|
|
22
|
+
updated: 2026-10-18
|
|
23
23
|
owners: [jcardinal, apeterson, bala, akhokhani, mhammontree]
|
|
24
24
|
files:
|
|
25
|
+
- dbchanges2/Client_Nychh/2026-10-18a - NewYorkWarehouseLocation.sql
|
|
25
26
|
- dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql
|
|
26
27
|
- dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql
|
|
27
28
|
- dbchanges2/Client_Nychh/2026-09-02a - PurchaseOrderItemsTableViewRebase.sql
|
|
@@ -71,6 +72,7 @@ TOGA 2.0 client on `_underscore`, prod schema `Client_Nychh`. Supply-chain / ful
|
|
|
71
72
|
- **Worker tiers:** `worker2` (2.0) runs the NYCHH asset-tag backfill; `worker`/`library` (1.0) run the NetSuite→TOGa Supply sync and the asset-tag verification/backfill diagnostics.
|
|
72
73
|
|
|
73
74
|
## Key features (this client)
|
|
75
|
+
- **SALES_ORDERS froze `1-RUNNING` again 2026-10-15 → 10-18 (Issue 1041) — fixed by data + prod-verified 2026-10-18 (TRUE-82859).** After the 10-14 integration restart rewound cursors, TO #91091 (NS 2143621, 2021) shipped from NS location 1 "New York Warehouse" (parent of 117 H&H, 2 DOE, 15 Prudential) → `throwIfUnknownWarehouse`. Fix: `dbchanges2/Client_Nychh/2026-10-18a - NewYorkWarehouseLocation.sql` (Location 86, Warehouse, `c_netsuiteInternalLocationId` 1). Warehouse map now 1, 2, 4, 6, 9, 15, 117, 173. Do NOT enable `IS_ENABLED_SKIP_TRANSFER_ORDERS_FROM_UNKNOWN_WAREHOUSE` here — location 1 holds 5 real H&H orders (2021-22). OPEN (undecided): Location 82 "Naperville Config" maps NS 2 = "New York Warehouse : DOE" — name mismatch, not blocking.
|
|
74
76
|
- **SALES_ORDERS froze `1-RUNNING` 2026-10-15 → 10-16 (Issue 445) — fixed + prod-verified 2026-10-16 (library `f351465a`, TRUE-82859).** Stocking SO 7342575 (TO 285145) had 3 lines removed in NetSuite; their PO links blocked the line delete (FK 1451). Cursor now past `7342575`. See [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
75
77
|
- **ITEM_FULFILLMENTS froze `1-RUNNING` 2026-09-30 → 2026-10-06 (Event issue 483) — fixed + prod-verified 2026-10-06 (library).** NS order 4512137 (#165198, stocking) existed twice: stale TransferOrder 508 (pre-2026-09-03 stocking rule) + SalesOrder 168. IF reconcile tried to delete TO line 7409 that an IA line linked to → FK 1451. New reverse retire (TO→SO) + IA-link move before IF-line deletes; TO 508 retired, cursor past `4605943`. Remaining doubles: #233366 (SO 172 / TO 1811 — retires when its IF 2392 syncs), #290752 (SO 167 / TO 1958, no IFs — waits for SALES_ORDERS feed). See [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
76
78
|
- **Transfer-order "Order Shipped" email fires on the first tracking number, not the NetSuite shipped stage (2026-10-05, prod).** Warehouse marks shipped after delivery. NYCHH is the only client emailing off the NS shipped stage. See [TO notification emails](./features/transfer-order-notification-emails.md). Alarm when one stays owed > 24h: [emails owed monitor](./features/oneuptime-transfer-order-emails-owed-monitor.md) (TRUE-82758, not deployed 2026-10-16).
|
|
@@ -17,11 +17,12 @@ apps:
|
|
|
17
17
|
- togadesk
|
|
18
18
|
- toga25-desk
|
|
19
19
|
- toga25-blox
|
|
20
|
+
- toga25-iq
|
|
20
21
|
project: _Underscore
|
|
21
22
|
client: true
|
|
22
23
|
type: profile
|
|
23
24
|
status: active
|
|
24
|
-
updated: 2026-10-
|
|
25
|
+
updated: 2026-10-18
|
|
25
26
|
owners: [jcardinal, kyalamarthi, tcox, rgirish, mhammontree, akhokhani]
|
|
26
27
|
files: []
|
|
27
28
|
related:
|
package/package.json
CHANGED
|
@@ -73,6 +73,17 @@ rows for the developer to write by hand.
|
|
|
73
73
|
`Core.Records` / `Core.RecordFields` rows, the `/finish` report always lists the ids used
|
|
74
74
|
and the next free ids.
|
|
75
75
|
|
|
76
|
+
### Local work has no "deploy" step (team rule, 2026-10-18, jcardinal, TRUE-82876)
|
|
77
|
+
|
|
78
|
+
Applies to every repo. In a local environment, code is live as soon as each repo is on the
|
|
79
|
+
right branch. "Deploy" only applies to non-local environments. api2 is a PHP app — there is
|
|
80
|
+
no "reload api2" or "restart api2" anywhere in our infrastructure.
|
|
81
|
+
|
|
82
|
+
1. Run steps for a developer's **local** work never say "deploy `_underscore`", "reload api2"
|
|
83
|
+
or "restart api2".
|
|
84
|
+
2. If order matters, say "make sure `<repo>` is on branch `<TICKET>`" instead.
|
|
85
|
+
3. Real deploy steps for non-local environments stay as they are.
|
|
86
|
+
|
|
76
87
|
## No running commentary — one compiled report at the end (team rule, 2026-10-15)
|
|
77
88
|
|
|
78
89
|
Developers do other work and come back when Claude asks something. So work silently, then
|