toga-ai 1.0.964 → 1.0.965

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.
@@ -158,19 +158,16 @@ plain thrown warning instead.
158
158
  (`WHERE name LIKE 'NETSUITE_EXECUTION_MODE_%'`). Wrap text columns in
159
159
  `CONVERT(... USING utf8mb4) COLLATE utf8mb4_general_ci` or the UNION fails on mixed collations.
160
160
 
161
- #### Manual reset after the fix: `864000-IDLE` — get both halves right (2026-09-08)
161
+ #### Never hand-reset `NETSUITE_EXECUTION_MODE_*` (developer rule, 2026-10-06)
162
162
 
163
- A section self-heals from `1-RUNNING` on its own, but climbing ×3 from a 1-second window back to the
164
- cap takes many runs, so a backlog drains very slowly. Setting the parameter by hand skips that climb.
165
- **Only do it after the real error is fixed** — otherwise the next run just throws and shrinks again.
163
+ **Do not set the parameter by hand — not even to `864000-IDLE` after a fix.** A correct fix makes the
164
+ section climb back to the cap and settle at `IDLE` on its own; a hand reset hides whether the fix
165
+ actually worked. If a section does not recover by itself, the fix is incomplete — keep fixing the
166
+ root cause. (Replaces the 2026-09-08 "manual reset after the fix" advice.)
166
167
 
167
- The correct value is **`864000-IDLE`**. Both halves are easy to get wrong, and both were nearly
168
- mis-set during the 2026-09-08 Elite incident:
169
-
170
- - **State must be `IDLE`, not `RUNNING`.** `RUNNING` is the "prior run died / run in progress" flag.
171
- Writing `RUNNING` tells the next run to shrink the window.
172
- - **The cap is `864000` (10 days), not `86400` (1 day).** One zero short is a silent 10× smaller
173
- window, not an error.
168
+ Reading the value: the healthy resting value is **`864000-IDLE`** (cap + `IDLE`). `RUNNING` is the
169
+ "prior run died / run in progress" flag, and a window below the cap (e.g. `86400`, `1`) means recent
170
+ failures shrank it — see "Reading `NETSUITE_EXECUTION_MODE_*`" below.
174
171
 
175
172
  (`MAX_TIME_WINDOW_TO_FETCH_FROM_NETSUITE_SECONDS` is per-client overridable, but as of 2026-09-15
176
173
  **no wrapper overrides it** — Elite's old 2592000 is gone. So `864000` is the right value for every
@@ -626,11 +623,12 @@ database's `Parameters` table:
626
623
  sorts below it. Always write `'2026-01-01 00:00:00|0'`, never carry the old id back.
627
624
  2. **The datetime is on the Eastern clock** (`America/New_York`), not UTC and not the server's. See
628
625
  the wrong-clock bugs above — this is the same clock they settled on.
629
- 3. **Reset the paired execution-mode key at the same time.** `NETSUITE_EXECUTION_MODE_<SECTION>`
626
+ 3. **Check the paired execution-mode key first — but never hand-reset it.** `NETSUITE_EXECUTION_MODE_<SECTION>`
630
627
  holds `<seconds>-<STATE>`; a prior crash leaves it `RUNNING`, and the next run then **shrinks**
631
628
  the window ÷3 instead of using the full cap. Observed live 2026-09-15: Elite's
632
- `NETSUITE_EXECUTION_MODE_SALES_ORDERS` was stuck at `864000-RUNNING`. Set it to
633
- `<the client's own cap>-IDLE` — see the manual-reset section above for both halves.
629
+ `NETSUITE_EXECUTION_MODE_SALES_ORDERS` was stuck at `864000-RUNNING`. A stuck `RUNNING` means the
630
+ crash's root cause is not fixed yet — fix that before rewinding; the mode climbs back to
631
+ `<cap>-IDLE` on its own (see "Never hand-reset" above).
634
632
  4. **One run only advances `MAX_TIME_WINDOW_TO_FETCH_FROM_NETSUITE_SECONDS`** — 864000s (10 days) by
635
633
  default, and **Elite does not override this one**. So a rewind to 1 Jan 2026 needs **~26 runs** to
636
634
  catch up. On the 5-minute cron that is fine; on a daily cron it would take ~26 days. **Run the
@@ -1160,6 +1158,16 @@ same `Parameters` cursors: whoever writes last wins and the other's window is si
1160
1158
  (and windows shrink). Any manual backfill must either pass the cron's filename to
1161
1159
  `isProcessRunning()` explicitly or be run with the cron's schedule entry disabled.
1162
1160
 
1161
+ ## Fix rules (team directives for any sync/reconcile change)
1162
+
1163
+ 1. **Self-heal to match NetSuite.** Re-processing a record must leave the DB matching the source, including deleting stale/duplicate rows. A one-time drain is allowed only after the code fix.
1164
+ 2. **Repair by code, never SQL.** Wrong sync data is fixed in sync code (self-heals on re-run), then cursors are rewound to replay. Also ship a single-order runner keyed by the NetSuite order number / internal id (not the Toga id). No data-repair or backfill SQL. Schema/metadata/ACL SQL in dbchanges2 is fine.
1165
+ 3. **Fail loud, fix upstream.** No `error_log()`-and-`continue` skips (worker stdout is discarded): throw so the section stays RUNNING and lands in `Logs.Event`. On a failed match, look higher up for the real cause (e.g. the REST shim returning bad lines) before adding any skip.
1166
+ 4. **Dependencies import first.** Import a record's parents before it (SO before IF/Invoice, PO before ItemReceipt, IF before InventoryAdjustment). If the NetSuite record declares a parent and we end with no real linked Toga parent, throw before creating. Never skip, orphan, or advance the cursor past it.
1167
+ 5. **Never step the cursor forward past a poison record.** A section frozen on a throwing record is the correct state; root-cause that record. Rewinding to re-import is fine; a forward skip is never acceptable (silent data loss).
1168
+ 6. **Traceability/bridge links are accurate data.** Never drop a valid link and never log-and-skip one that should exist; fix the match so the link is made. Skip-and-log only when there is genuinely nothing to link (e.g. a service/non-shipped line with no fulfillment counterpart). Prove the current match rule is wrong (NetSuite + toga-db + prod Logs) before changing it.
1169
+ 7. **Compass reconcile boundary.** `syncSalesOrderFromNetsuite` on `Client_Compass` may delete/alter Agilant orders, POs and link/bridge rows so the Agilant SO matches NetSuite. It must NEVER touch the Compass (customer) order or customer PO. Compass has two opposite-direction bridges (UPSTREAM `PurchaseOrders_SalesOrders`/`PurchaseOrderItems_SalesOrderItems`; DOWNSTREAM `SalesOrders_PurchaseOrders`/`SalesOrderItems_PurchaseOrderItems`) plus a multi-tier SO-PO-SO-PO chain: establish which records are Agilant vs customer before writing delete logic, and confirm the boundary with the developer before any prod delete. See [bridge direction](../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md).
1170
+
1163
1171
  ## Data model
1164
1172
 
1165
1173
  Per-client `Parameters` table (2.0 client DB, e.g. `Client_Canon.Parameters`; columns
@@ -1253,6 +1261,8 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
1253
1261
 
1254
1262
  ## Gotchas / known issues
1255
1263
 
1264
+ - **Ops fix steps: verify current values first.** Before telling Ops to change a NetSuite field, query the record's current values. Name the exact record (order #, adjustment #), write "currently X → change to Y", and omit any field already correct. Never write a generic "find the order and set…" when the data can identify it.
1265
+
1256
1266
  - **The 2026-09-17 transient-read retry covers NetSuite reads only** (`App_Api_Netsuite_Rest::send`), **not** api2 calls via `App_Api_Toga2::send`. An api2 blip (e.g. "cURL error with API transaction: Empty response body from API (HTTP 200)") still aborts the section for that run and drops the window to `288000-IDLE`; it self-heals next run. One such failure is not a collapse — check the window grows back before acting. Seen NYCHH ITEM_FULFILLMENTS 2026-09-25, issue 873 (`common_sync_togasupply.php:1287` → `toga2.php:6630`).
1257
1267
 
1258
1268
  - **⚠ Sales-order downstream LINE links were deleted on every PO re-sync (fixed 2026-09-24).** In
@@ -2116,6 +2126,7 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
2116
2126
  [NetSuite Sync Alert Monitor](../../library/features/netsuite-sync-alert-monitor.md).
2117
2127
 
2118
2128
  ## Change history
2129
+ - 2026-10-06 — Added "Fix rules" section: self-heal, repair-by-code, fail-loud, dependencies-first, no forward cursor skip, links-are-data, Compass delete boundary. (jcardinal)
2119
2130
  - 2026-10-06 — NYCHH ITEM_FULFILLMENTS frozen 09-30→10-06 (Event issue 483, FK 1451 on IA→IF line link) fixed: reverse retire TO→SO + IA-link move before every IF-line delete (library only). Corrected "NYCHH once a day" — it runs every 5 min. (jcardinal)
2120
2131
  - 2026-10-05 — Closed-order delete now per-client SO scoped (Compass dual-SO freeze, Issue 940); details in [closed-order rule](../../library/features/netsuite-closed-order-rule.md). Library only. (jcardinal)
2121
2132
  - 2026-10-01 — `Items.description` now from the NetSuite item record, refreshed every touch ([doc](../../library/features/netsuite-item-description-sync.md); library + worker deploy together). Gotchas: invoice billing an SO older than the SO cursor (Adyen); receipt lines never rebuilt on an existing receipt (Elite). (rgirish)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-10-02
9
+ updated: 2026-10-06
10
10
  owners: ["jcardinal", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/process_netsuite_order.php
@@ -69,6 +69,7 @@ php crons/toga2/netsuite/process_netsuite_inventory_adjustment.php <client> --id
69
69
  - `process_netsuite_item_fulfillment.php` overlaps `process_netsuite_records.php --fulfillment` (built in parallel).
70
70
 
71
71
  ## Gotchas
72
+ - **Convention:** every single-record `process_*` CLI tool takes `--id=<id>[,<id>...]` from the start: validate all ids up front, dedupe, run in order, stop at the first error and report how many were done.
72
73
  - A TO skipped by the sync (out-of-scope customer) prints `NOT imported` — read the error log, not a script bug.
73
74
  - Needs `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` true in the wrapper for TO routing; otherwise the order syncs as a sales order.
74
75
 
@@ -5,7 +5,7 @@ project: Library
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-09-18
8
+ updated: 2026-10-06
9
9
  owners: [jcardinal, rgirish, mhammontree, ajean]
10
10
  files: []
11
11
  related:
@@ -553,6 +553,10 @@ Note: the 2.0 metadata-driven API features (Record Scripts, API Payload Intercep
553
553
  - **Remove all `debugVar()` calls before merging.**
554
554
  - Don't extend `DOA_`-prefixed code; plan its removal when you touch the area and dependencies allow.
555
555
 
556
+ ### Changing a shared `library` function signature: grep ALL repos for callers
557
+
558
+ Grep the function name across every repo (all `*.php` in `library` AND `worker`, plus any other consumer), never just the edited file. `worker` crons call `library` classes through the autoloader with no `require`, so an import search misses them; only a name grep finds them. A missed direct caller fails at runtime ("Too few arguments") and can freeze a sync after deploy. Tell reviewers the caller search spans repos. Worked case: [per-client sync](../apps/worker/features/netsuite-togasupply-per-client-sync.md) (three call sites, one a direct cron).
559
+
556
560
  ## Binary downloads: build the whole file, then send headers
557
561
 
558
562
  Never stream a generated file straight to the browser. `App_Error::handleError` promotes every
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-21
9
+ updated: 2026-10-06
10
10
  owners: ["jcardinal", "mhammontree", "tcox", "bala", "apeterson", "rgirish"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -407,6 +407,15 @@ with **no** grant is not automatically a defect; compare against a healthy clien
407
407
  first (see
408
408
  [client schema-drift audit](../../dbchanges2/workflows/client-schema-drift-audit.md)).
409
409
 
410
+ ## Role model: two role spaces, no inheritance, Public baseline
411
+
412
+ - **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
+ - `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
+ - **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.
416
+ - **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
+ - Related: no backend ACL cache in 2.0 — a stale 403 is the UI cache or ACL logic, never a backend cache.
418
+
410
419
  ## Where ACL rows live: Core vs. Client database
411
420
 
412
421
  `Core.Records.aclDatabase` decides which database holds the ACL rows for that record:
@@ -248,6 +248,16 @@ This lifecycle is automated by a local browser tool — see the [TOGa 2.0 Client
248
248
  8. **Never reference a database other than the folder's own target.** No `Core.Table` in a `Client_<Name>/` file, no `Client_X.Table` in a `Core/` file, and **no database qualifier at all** in the fan-out folders (`Client/`, `Logs_Client/`, `_modules/`). These databases are on **separate production clusters**; the query works locally and dies in production. Resolve foreign ids with a hardcoded v4 UUID literal or an in-database slug/natural key, or split the work into one file per database folder. See *Database isolation* above — enforced by the `dbchanges2-cluster-isolation` hook.
249
249
  9. **Inserting into `Core.Records` or `Core.RecordFields`? ASK the developer for the next `id`** (the developer takes it in Tools → Developers → Central IDs — the source of truth; never guess from MAX(id), branches hold unreleased ids). These are the **only two tables on the platform with team-maintained primary keys**. Write the assigned `id` as an explicit literal — **never** let `AUTO_INCREMENT` assign it. Conversely, **always hardcode** these `id`s where they are referenced, including in foreign keys (`recordId`, `recordFieldId`) from other databases. See *`Core.Records` / `Core.RecordFields`* above.
250
250
  10. **A fan-out `Client/` file must never reference a `customRecordFieldId` or a client-specific label.** Unlike `Core.RecordFields` ids, `CustomRecordFields` ids are **per-client `AUTO_INCREMENT`** — the same numeric id names a **different field in every client database**. A `Client/` file that hardcodes one silently wires the wrong field for 29 of 30 tenants, and there is no error. The same applies to `ClientCustomRecordFieldSettings` label rows. Move anything touching a custom field into the client's own `Client_<Name>/` file, even when it is "the same change" for several clients. Worked example: the all-clients `inventory_units` table view shipped in `Client/2026-08-11a`, but Elite's four custom columns (`grade`, `chargingBrick`, `chargingCable`, `wipeStatus` via `c_deviceStatus`) and their labels shipped separately in `Client_Elite/`.
251
+ 11. **`ALTER TABLE … ADD COLUMN` appends at the END of the table — always add `AFTER <predecessor>` (or `FIRST`)** so the column lands in its slot per the field-order standard ([backend-php → Order of Fields](../../standards/backend-php.md)): FKs go immediately after `uuid`. Fix a misplaced column with `MODIFY COLUMN … AFTER <col>` (keeps the FK constraint). Keep the `_Model` field declaration order consistent with the column order.
252
+ 12. **UUID literals are fresh random v4, never patterned; `Core.RecordFields` inserts set `isIdentifier` (`1` for `uuid`, `0` otherwise).** See [UUIDs](../../standards/backend-php.md) and rule 6.
253
+ 13. **Fan-out lookup seed (seeding baseline rows into every `Client_*` DB) = one guarded, re-runnable batched insert:** `INSERT INTO t (…) SELECT … FROM (SELECT <lits> UNION ALL SELECT …) AS src LEFT JOIN t existing ON existing.<naturalKey> = src.<naturalKey> WHERE existing.id IS NULL;`
254
+ - Guard on the natural key (slug/name) backed by a real UNIQUE key: re-run is a no-op and a tenant that already has the rows keeps its own uuids.
255
+ - Resolve a child FK with `INNER JOIN` on the parent's natural key, not a scalar sub-select (a miss writes NULL silently; the join drops the row). Seed the parent table first in the same file.
256
+ - `WHERE NOT EXISTS(…)` with no FROM is a 1064 syntax error; the UNION ALL derived table is the source.
257
+ - Reusing the same literal uuid across tenants is OK only for a per-tenant lookup table not in the cross-tenant scatter (scatter identity = uuid).
258
+ - No prose preamble in a migration file: one short line per statement; narrative goes to the ticket.
259
+ - Before running on prod, confirm each tenant's existing natural-key values match EXACTLY, or a mismatch inserts a duplicate instead of a no-op.
260
+ 14. **Never assume a migration has or has not run — verify (ask, or SELECT the live schema) before claiming state or editing a file.** An applied file is immutable: to undo it, RESTORE the original file to match what ran, then add a NEW dated forward-only file that drops/changes it (fresh envs run both and converge; run envs run only the new one). MySQL has no `DROP COLUMN IF EXISTS`, so restoring the add is what makes the later drop safe everywhere.
251
261
 
252
262
  ## Bulk data loads — batch, and stage large sets in a temp table
253
263
 
@@ -6,7 +6,7 @@ project: SAML SSO Gateway
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-06
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - saml/Controller/Index.php
@@ -55,6 +55,7 @@ Probe reuses the **internal TOGA client** (`clientIdentifier "True"`, the test t
55
55
 
56
56
  ## Gotchas
57
57
 
58
+ - **Critical-path rule: prove auth edits with a real run, not `php -l`.** Lint hid a blank-page prod outage twice on `/acs` (`display_errors=0`, so any fatal is a white page): a `$domain`/`$client` reload placed after the client-DB connect, and `empty($user->id)` on a magic field. Keep monitors additive and self-contained (`diff` vs deployed shows 0 removals on the live path); never refactor the live path to share code. Verify with a real login or the token-gated `/healthcheck` before prod, and keep a temporary in-body error string only while diagnosing, then strip it.
58
59
  - **Do not use `empty()`/`isset()` on the resolved user's magic fields.** `getAuthenticatedSsoUser()` returns a fully-loaded `_Model` user, but `!empty($user->id)` reads **false** even when `$user->id` is a real value (this session: id 3264 read as "no id"). Use a plain truthy check `if ($user->id)` (what `/acs` itself does) or copy to a local first. Full rule: [_Model magic-field access](../../_underscore/features/model-magic-field-access.md).
59
60
  - **The token is a credential** — it lives only as the EB env property `SAML_HEALTHCHECK_TOKEN`, never committed and never written into a doc.
60
61
  - **Inbound IdP SAML signature verification is still commented out** (see architecture) — re-enabling is per-client, needs each IdP signing cert, and must not break live logins (tracked separately).
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-09-28
9
+ updated: 2026-10-06
10
10
  owners: [jcardinal, ajean, rgirish]
11
11
  files:
12
12
  - worker2/Controller/Index.php
@@ -151,6 +151,7 @@ For **programmatic** enqueue via `_Worker::runTask()`, the MySQL-first rule was
151
151
 
152
152
  ## Gotchas
153
153
 
154
+ - **Never give `?controller=` URL test steps for worker2.** `_.php` sets `DEFAULT_CONTROLLER_METHOD` to `_Controller_Index::worker()`, so every URL routes there and shows a blank page; the "Access via: ?controller=…" comments in `Controller/*Test.php` are misleading. Prove pure logic with a CLI runner (stub `_Component`/`_Controller`, require the file, call `run()`) or a worker action POSTed as `{action, parameters}`.
154
155
  - **Shrinking `WorkerJobs.output` MEDIUMTEXT → TEXT (planned, not yet done).** TEXT holds 65,535 **bytes** (utf8mb4 = up to 4 bytes/char), not chars. Trim byte-safe before the ALTER, repeating until 0 rows: `UPDATE WorkerJobs SET output = LEFT(output, CHAR_LENGTH(output) - (LENGTH(output) - 65535)) WHERE LENGTH(output) > 65535 LIMIT 1000`. After the change, every producer must truncate first or the result UPDATE fails with 1406 Data too long (strict mode) and the job result is lost: PHP check-out (both paths in `Controller/Index.php`) and the JobScheduler watchdog Lambda UPDATE (repackage + redeploy it). The `MODIFY` must restate the column COMMENT. On MySQL 8 the type change rebuilds the table.
155
156
 
156
157
  - **⚠ OBSERVED `Core.WorkerJobs` retention is ~30 days, not 90 — unexplained.** Measured 2026-09-15 in prod: `MIN(dtCreated)` across the **whole** table is **2026-08-16 02:00:46** (790,597 rows). Either the cleanup cron does not behave as described, or something else prunes the table. **Not investigated** — no cause is asserted.
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: draft
9
- updated: 2026-09-16
9
+ updated: 2026-10-06
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Team/Github.php
@@ -92,6 +92,16 @@ approved Task/Hotfix/Feature deploys through `MergeTaskToProduction` — see
92
92
  non-mirror `_production` merge, so a stray queue message can't deploy. Re-fires are stopped by the
93
93
  per-repo `GET_LOCK` + the persisted deploy-claim (`EnvironmentBranchMerges` `branch='_production'`).
94
94
 
95
+ ## Built / pending
96
+
97
+ Whole QA/QC automation build is built and reviewed: Task + Hotfix + Stage + Feature flows, plus BE auto-approve from GitHub PR approvals (`_Worker_Clickup_BeReview::FromPr`, all of a task's `_production` PRs approved).
98
+
99
+ ### Pending / deferred
100
+ - Real unit tests: `runUnitTests()` is a stub; developer excluded it for now.
101
+ - Post-merge Stage "shipped-vs-held" summary: developer said skip.
102
+ - 300s budget: `finalizeFeatureSubtasks` and the QC-entry subtask loops make O(N) ClickUp calls in one job. Dispatch per subtask if features get many subtasks.
103
+ - Helper duplication (`fieldById`/`dropdownName`/`optionIdByName`/`isChecked` and similar) across QaReview/QcReview/QcBatch/StageBatch/FeatureReview/`_Worker_Clickup`: extract to one shared place.
104
+
95
105
  ## Related
96
106
 
97
107
  - [QA/QC review pipeline](./qa-qc-review-pipeline.md)
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-09-16
8
+ updated: 2026-10-06
9
9
  owners: [jcardinal, mhammontree, dfranks, bala, ajean]
10
10
  files: []
11
11
  related:
@@ -55,6 +55,8 @@ conventions.
55
55
 
56
56
  * All records inserted in the 2.0 database must be given a random UUID in the second column of each table called `uuid`
57
57
  * Do not use the built-in `UUID()` function provided by MySQL because they are not fully random and instead are time-based which causes UUIDs to be nearly identical, making it hard for developers to read.
58
+ * **UUID literals in seeds/migrations** (cannot call PHP) are fresh random v4 values, one distinct value per row, hardcoded. Never patterned/incremented (`…0001`, `…0002`), never a one-nibble variation, never `UUID()`. Generate with a real v4 generator (`crypto.randomUUID()`).
59
+ * `Core.RecordFields` inserts must set `isIdentifier`: `1` for the `uuid` field, `0` for every other field. Never omit the column.
58
60
  * Instead, generate a fully-random UUID in PHP with **`_String::generateUuid()`** (defined in `_underscore/String.php`). It builds a random 32-hex value formatted as a standard UUID. Do not rely on a MySQL-side function for this.
59
61
 
60
62
  #### **Custom Fields**
@@ -399,6 +401,11 @@ Authors_Posts
399
401
 
400
402
  * In code you rarely write these raw types directly. A `_Model` declares each column as a `FIELD_*` constant that maps to the underlying SQL type — e.g. `FIELD_PRIMARYKEY` / `FIELD_FOREIGNKEY` (unsigned int keys), `FIELD_INTEGER`, `FIELD_BOOLEAN` (the `is*` tinyint flag), `FIELD_DECIMAL`, `FIELD_CHAR` / `FIELD_CHAR_UUID`, `FIELD_LIST` (ENUM), `FIELD_DATETIME_CREATED` / `FIELD_DATETIME_UPDATED` (auto-managed timestamps), `FIELD_BLOB`, `FIELD_STORAGE`, and `FIELD_SQL` (calculated). The SQL types below are the storage targets these constants resolve to; follow both this section and the model field-type conventions when defining columns.
401
403
 
404
+ **ENUM / `FIELD_LIST`**
405
+
406
+ * Values are `CAPITAL_SNAKE_CASE`: `ENUM('TICKET','SERVICE_REQUEST')`, `ENUM('OPEN','IN_PROGRESS')`. Never lowercase-hyphen.
407
+ * Slug / natural-key string columns (`slug='draft'`, `new-ticket`) are not enums: they stay lowercase-hyphen.
408
+
402
409
  **Flags/Booleans**
403
410
 
404
411
  * Flag fields should begin with `is`, for example: `isVisible`
@@ -19,7 +19,7 @@ related:
19
19
  - framework-rules-cases.md
20
20
  ---
21
21
 
22
- Core 2.0 `_underscore` framework rules — class naming, the worker contract & dispatch, the api2 `{success,data,errors}` envelope, dbchanges2 schema/isolation rules, and `c_` columns (dynamic in /v2, declared for direct `_Model` use); open when writing/reviewing any 2.0 code.
22
+ Core 2.0 `_underscore` framework rules — class naming, the worker contract & dispatch, the api2 `{success,data,errors}` envelope, dbchanges2 schema/isolation rules, and `c_` columns (/v2 exposes one only when the client model declares it, plus ACL rows); open when writing/reviewing any 2.0 code.
23
23
 
24
24
  These rules apply to all 2.0 repos: `_underscore` (core), `worker2`, and `api2`. They are
25
25
  loaded on-demand by `/kickoff` when a 2.0 repo is in scope (not always-on) — see the
@@ -6,7 +6,7 @@ project: Claude Harness
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-06
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - claude/scripts/hooks/remind-sql-review.js
@@ -38,6 +38,8 @@ The harness ships PostToolUse hooks that force a specialist review after certain
38
38
  - The hook only nudges; enforcement depends on Claude honouring the directive. It is a routing reminder, not a mechanical guarantee that the reviewer ran.
39
39
  - Signals match on *newly-written* text only. An edit that touches a SQL file/PHP query without the matched keywords in the new chunk will not fire.
40
40
 
41
+ - **PreToolUse deny = exit 2 + reason on STDERR (`console.error`).** The harness reads the deny reason from stderr only; a reason printed to stdout still blocks but shows the opaque `hook error: No stderr output`, so nobody sees why. UserPromptSubmit is the opposite: its stdout is injected into context. Payload arrives as JSON on stdin (`tool_name`, `tool_input.file_path`, `tool_input.command`); env has `CLAUDE_PROJECT_DIR`. Anchor per-project lock files on `CLAUDE_PROJECT_DIR`, not `process.cwd()`.
42
+
41
43
  ## Related
42
44
 
43
45
  - [Harness Distribution](../workflows/harness-distribution.md)
@@ -6,7 +6,7 @@ project: Claude Harness
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-06
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - claude/scripts/install.js
@@ -41,6 +41,11 @@ There are **two copies** of `knowledge.js` on a developer's machine, and the ski
41
41
  - **`npx toga-ai` can silently fail to update the knowledge repo** (open issue, not yet fixed). On a non-zero `git pull --ff-only` exit, `tryGitPull()` returns `{ pulled: false, newDocs: 0, message: null }` — the git stderr is discarded and **nothing is printed to the developer**. A teammate whose pull is refused (e.g. local edits to a tracked file the incoming commits also touch — `.claude/settings.json` is the usual culprit) will believe they are up to date while still running old tooling. Suggested fix, not applied: surface the git stderr in the `pulled: false` path so a refused pull is visible. Same defect class as the swallowed-stderr bug in the publish pipeline.
42
42
  - Until that is fixed, if a harness fix "didn't take", check the pull manually rather than trusting `npx toga-ai`'s output.
43
43
 
44
+ - **The harness `.claude` lives only at the workspace root, never in a sub-repo.** Hook commands must anchor on `$CLAUDE_PROJECT_DIR` (`node "$CLAUDE_PROJECT_DIR/.claude/hooks/toga/X.js"`), not relative paths: relative paths resolve against cwd, so a sub-repo cwd gives MODULE_NOT_FOUND. A hook error naming a sub-repo `.claude` path is a path-resolution bug, not a folder to create. The settings file ships from the bundle, so the fix must live upstream or `npx toga-ai` reverts it.
45
+ - **Publish Action (`.github/workflows/publish.yml`, on push to `_main`: bump patch, `npm publish`).** Devs get a change only after a version bump (kickoff runs `npx toga-ai@latest` when installed != latest).
46
+ - `npm publish` **E404** ("could not be found or you do not have permission") = auth failure, not a missing package: the `NPM_TOKEN` secret in the repo's **Actions environment** expired. Fix: new npm Automation token, update the secret, re-run only the latest failed run.
47
+ - A publish ships the whole tip of `_main`, so earlier failed runs are not lost. Do not retry old runs (they hit "version already published").
48
+
44
49
  ## Related
45
50
 
46
51
  - [Knowledge Base Publish / Push Pipeline](knowledge-publish-pipeline.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.964",
3
+ "version": "1.0.965",
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",