toga-ai 1.0.918 → 1.0.919

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.
@@ -17,6 +17,7 @@
17
17
  | [Error Capture in 1.0 (App_Error_Capture → shared 2.0 Logs DB)](features/error-capture-1-0.md) | 1.0 error-reporting into the shared 2.0 Logs DB (`App_Error_Capture::captureException`), mirroring 2.0's Issue/Event; open when adding a 1.0 capture path or deb |
18
18
  | [HTTP 500 Error Monitor (App_SystemMonitor_500Error) — email-only count; diagnose via error.id](features/http-500-error-monitor.md) | 1.0 monitor `App_SystemMonitor_500Error`: counts `Logs.Api` responseCode=500 in the last 5 min and emails devteam; no ClickUp. |
19
19
  | [1.0 MVC Page Pattern & New-App Skeleton](features/mvc-page-pattern-and-app-skeleton.md) | Recipe for a new 1.0 (`App_`) app and for adding pages: folder-based MVC routing, page lifecycle, models, multi-DB config; open when standing up a 1.0 app or ad |
20
+ | [NetSuite Closed sales orders — keep if related records, delete if none](features/netsuite-closed-order-rule.md) | How the NetSuite → TOGa Supply sync treats a Closed SO (all clients): keep + import when anything was created from it, delete when nothing was; plus the ACL the |
20
21
  | [NetSuite File Cabinet Content Retrieval via RESTlet (fetchInvoiceFile)](features/netsuite-filecabinet-restlet.md) | How 1.0 pulls NetSuite File Cabinet binary content (invoice PDFs) via a SuiteScript RESTlet (`fetchInvoiceFile`); open when fetching NetSuite file bytes over RE |
21
22
  | [assetType from NetSuite itemtype during Item Sync (opt-in per client)](features/netsuite-item-assettype-sync.md) | Stamp `Items.assetTypeId` from NetSuite `item.itemtype` during item sync (opt-in per client, fill-NULL-only); open before enabling asset-type mapping for a clie |
22
23
  | [NetSuite classification → ItemClasses sync (opt-in per client)](features/netsuite-item-class-sync.md) | `App_Api_Toga2::getCreateItemClass()` mirrors NetSuite's **classification tree** into the client's `ItemClasses` table and stamps `Items.itemClassId`. |
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: "NetSuite Closed sales orders — keep if related records, delete if none"
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-30
10
+ owners: ["rgirish"]
11
+ files:
12
+ - library/app/api/toga2.php
13
+ - library/app/api/netsuite/rest.php
14
+ - worker/crons/toga2/netsuite/common_sync_togasupply.php
15
+ - worker/crons/toga2/netsuite/process_netsuite_order.php
16
+ - worker/crons/toga2/netsuite/delete_closed_netsuite_orders.php
17
+ related:
18
+ - ../../worker/features/netsuite-togasupply-per-client-sync.md
19
+ - ../../worker/workflows/reprocessing-one-netsuite-order.md
20
+ - ../../../2.0/apps/_underscore/features/acl-permission-chain.md
21
+ - ../../../clients/compass-usa/profile.md
22
+ ---
23
+
24
+ How the NetSuite → TOGa Supply sync treats a Closed SO (all clients): keep + import when anything was created from it, delete when nothing was; plus the ACL the delete needs.
25
+
26
+ ## Summary
27
+ - **Rule (2026-09-29, all NetSuite sync clients):** a NetSuite SO in status `Closed`:
28
+ - **with** related records (any transaction whose `createdfrom` is the SO — fulfillment, invoice, PO, return) = **completed**. Keep it and import it like any order, shipments included.
29
+ - **with none** = **duplicate**. Never import it; delete the TOGa order if it was imported before it closed.
30
+ - **Replaces the 2026-09-23 decision "delete every Closed order".** That rule skipped closed orders that really shipped, so TOGa on-hand stayed above NetSuite.
31
+
32
+ ## How it works
33
+ - `App_Api_Netsuite_Rest::hasRelatedTransactions(int $orderId)` — SuiteQL `COUNT(*)` on `transactionline` where `createdfrom = <id>` and `transaction <> <id>`.
34
+ - `App_Api_Toga2::isNetsuiteOrderClosedWithoutRelatedRecords($nsOrder)` = status `Closed` AND no related transactions. Used by the SO loop in `common_sync_togasupply.php`, `syncTransferOrderFromNetsuite`, and `process_netsuite_order.php`.
35
+ - `App_Api_Toga2::deleteClosedOrderFromNetsuite()` has its own **KEEP guard** (re-checks `hasRelatedTransactions`, logs `KEEP`, returns false). So every caller, including the one-time tool `delete_closed_netsuite_orders.php` (dry run unless `--execute`), is safe.
36
+ - The delete removes the SalesOrder/TransferOrder with its lines, invoices, fulfillments (units, tracking bridges) and bridge rows. Records on the other side of a bridge (PO, other SO, TrackingNumber, Unit) are never deleted — only the link row. Fails loud; a half-done delete is finished by the next run.
37
+ - Backfill for shipped closed orders that were skipped under the old rule: re-run their fulfillments with `process_netsuite_records.php` (see [re-processing workflow](../../worker/workflows/reprocessing-one-netsuite-order.md)).
38
+
39
+ ## Gotchas
40
+ - **The delete reads every route in `CLOSED_ORDER_DELETE_ROUTES` (toga2.php) — the client's API role (roleId 3) needs read on ALL of them:** record permission + an expression on each logic group + field permission (at least `uuid`). One gap throws, and because it runs in the SO loop, **SALES_ORDERS freezes** (and the sections after it). Find gaps per client with `App_Api_Toga2::checkClosedOrderDeleteAccess()` (run by `delete_closed_netsuite_orders.php`; it lists every gap in one run). ACL layers: [acl-permission-chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md).
41
+ - Worked example (Compass, 2026-09-29): records 312/313 (transfer-orders) had logic groups with no expression; 257 (entitlement-sales-orders) and 254 (sales-order-payments) had no `AclRecordPermissions`, then no `AclFieldPermissions` (`EZ-2` on `uuid`). Fix: `dbchanges2/Client_Compass/2026-09-29a - TransferOrderAclExpressions.sql`. Then reset `NETSUITE_EXECUTION_MODE_SALES_ORDERS` to `864000-IDLE`.
42
+ - Adding a route to `CLOSED_ORDER_DELETE_ROUTES` = a new ACL need on every client. Run the access check fleet-wide before deploy.
43
+
44
+ ## Change history
45
+ - 2026-09-30 — Created. Closed SO with related records is now kept + imported; only Closed with none is deleted (replaces 2026-09-23 "delete every Closed"). Added KEEP guard in the delete and the ACL gotcha from the Compass SALES_ORDERS freeze. (rgirish)
@@ -6,7 +6,7 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-28
9
+ updated: 2026-09-30
10
10
  owners: ["dfranks", "jcardinal", "kyalamarthi", "ajean", "rgirish"]
11
11
  files:
12
12
  - library/app/api/netsuite/rest.php
@@ -263,8 +263,12 @@ SalesOrd per-id GET counts by `lastmodifieddate` window: ~16,600 (90d) / ~61,900
263
263
  - Field/relationship availability varies by NetSuite account **and** record type — probe the live account before assuming a column exists.
264
264
  - **`IS NOT NULL` on a custom `transactionline` column inside an aggregate query is a ~15× planner trap — prefer `> 0` (or `NVL(col,0) <> 0`).** Filtering a custom employee-ref column (`custcol_sales_rep_line`) with `AND tl.custcol_sales_rep_line IS NOT NULL` inside a `GROUP BY` / power-sum aggregate (`MOD(SUM(MOD(id*id,P)),P)`) made the planner scan pathologically: **~122s** vs **~8s** for the semantically identical `AND tl.custcol_sales_rep_line > 0` (employee internalIds are always positive) — a ~15× regression purely from the predicate form. Measured against the Forecast2 reconciliation checker (TRUE-79968). Use `> 0` / `NVL(col,0) <> 0` for a custom ref column you only need to test for presence in an aggregate; reserve `IS NOT NULL` for non-aggregate row filters.
265
265
  - **⚠ `> 0` is NOT a universal fix — it is column-dependent.** A predicate on a *different* custom column, `tl.custcol_item_sku > 0`, **never returned even as a STANDALONE aggregate** (not just inside the UNION+MOD fingerprint), while `tl.custcol_sales_rep_line > 0` in the same query stayed fast (~8s). Merely **SELECTing** the custom column is cheap either way. **Durable rule when a custom-column predicate hangs:** keep the fast predicates in SQL, **SELECT** the problem column, and apply that gate in **PHP** — do not assume `> 0` will rescue every custom column. (TRUE-79973, `test/@dave/checker.php` / `fixer.php`.)
266
+ - **Inventory quantities: use `aggregateitemlocation`, not `inventorybalance`.** `inventorybalance.quantityavailable` does NOT subtract SO commitments for many items, so it will not match the NetSuite UI. `aggregateitemlocation` gives `quantityonhand` / `quantityavailable` / `quantitycommitted` per item + location and matches the UI — use it to check TOGa inventory against NetSuite (Elite, 2026-09-29).
266
267
 
267
268
  ## Related
268
269
 
269
270
  - `netsuite-suiteql-rest-shim.md` — deep field semantics (signs, `tl.id`==line, `iscogs`, ShipItem cost, `createdFrom`) that sit on top of these mechanics.
270
271
  - `../../worker/features/forecast2-netsuite-reconciliation.md` — the audit/trueup tooling that consumes this API.
272
+
273
+ ## Change history
274
+ - 2026-09-30 — Gotcha: `inventorybalance.quantityavailable` ignores SO commitments; use `aggregateitemlocation`. (rgirish)
@@ -22,5 +22,5 @@
22
22
  | [isFulfillable Multi-Client Backfill (all togasupply clients)](workflows/isfulfillable-multi-client-backfill.md) | One-time backfill catching up `Items.isFulfillable` on existing items across all 17 togasupply clients; open when running/re-running it or deciding whether rema |
23
23
  | [Deduplicating NetSuite duplicate Items across client DBs (recursive MERGE)](workflows/netsuite-duplicate-items-dedup.md) | Per-client cleanup collapsing duplicate `Items` rows (same `partNumber`) to one canonical via recursive MERGE; open before running it — and read the catalog-bli |
24
24
  | [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer; open when onboarding a client, and read the vendor-seed / ACL / Parameters-see |
25
- | [Re-processing one NetSuite order (process_netsuite_order.php operator CLI)](workflows/reprocessing-one-netsuite-order.md) | Run the real SO/TO sync + its vendor POs for ONE NetSuite order on ONE client, without touching cursors; open to heal or debug a single order. |
25
+ | [Re-processing NetSuite orders, fulfillments and receipts (process_netsuite_order.php / process_netsuite_records.php)](workflows/reprocessing-one-netsuite-order.md) | Run the real sync for given NetSuite ids on ONE client, without touching cursors: one order (+ its vendor POs), or a list of fulfillments/receipts. |
26
26
  | [Tracing a 1.0 worker cron run in production (no stdout, silent skips)](workflows/tracing-a-worker-cron-run-in-production.md) | How to answer "did this cron run, and what did it do?" on the 1.0 worker tier (no usable stdout, skipped runs leave no trace); open before concluding a cron "ne |
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
9
+ updated: 2026-09-30
10
10
  owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -141,6 +141,18 @@ can show an OLD line; trust the newest `Event.context.errorLine` for the real sp
141
141
  gives `memoryPeakMb` — a small peak (e.g. 20 MB) rules out "giant record / OOM" and points at a
142
142
  plain thrown warning instead.
143
143
 
144
+ **Fastest path to the real error (2026-09-29):**
145
+
146
+ - `N-RUNNING` with N shrinking run after run = that section throws every run. SALES_ORDERS aborts the whole
147
+ script, so the sections after it freeze too — fix SO first.
148
+ - Read the api2 rejection in `Logs_<Client>.Api`: `responseCode >= 400 AND responsePayload LIKE '%"api":"Agilant"%'`,
149
+ newest first, `LIMIT 1` + tight date filter.
150
+ - **`Logs.Issue` keeps only the FIRST trace per fingerprint** — its trace can name the wrong client and line.
151
+ Use `Logs.Event` (has `clientId`) or `Logs_<Client>.Api` instead.
152
+ - Fleet check: one query on prod-client, `UNION` over every `Client_*.Parameters`
153
+ (`WHERE name LIKE 'NETSUITE_EXECUTION_MODE_%'`). Wrap text columns in
154
+ `CONVERT(... USING utf8mb4) COLLATE utf8mb4_general_ci` or the UNION fails on mixed collations.
155
+
144
156
  #### Manual reset after the fix: `864000-IDLE` — get both halves right (2026-09-08)
145
157
 
146
158
  A section self-heals from `1-RUNNING` on its own, but climbing ×3 from a 1-second window back to the
@@ -499,42 +511,13 @@ record; every section behaves the way SALES_ORDERS always did (throw → freeze
499
511
  poison record re-tried each run → go read the error). This is an application of the universal
500
512
  coding-style rule *never catch-and-silently-ignore*, not a new engine-specific policy.
501
513
 
502
- ### WARNING: a `Closed` NetSuite order is SKIPPED BEFORE the transfer-order branch (fixed 2026-09-08)
503
-
504
- `common_sync_togasupply.php` (~L746) does
505
-
506
- ```php
507
- if ($nsSalesOrder->status === 'Closed') { continue; }
508
- ```
509
-
510
- **before** the `isTransferOrder()` branch. So a transfer order that was **closed in NetSuite never
511
- reaches `App_Api_Toga2::syncTransferOrderFromNetsuite()`**, and that function's existing
512
- `case 'Closed'` never runs. **`Closed` is the only status the loop drops** - Pending Fulfillment,
513
- Partially Fulfilled, Billed and Cancelled all pass through and map fine.
514
-
515
- There is no other way in. The item-fulfillment route (`syncSalesOrderFromNetsuite`) and 2.0's
516
- `_Trait_Netsuite_TransferOrder::syncNetsuiteTransferOrder` both need a **fulfillment or item
517
- receipt**, which an order closed with nothing shipped will never have. Result on NYCHH: **18 transfer
518
- orders stuck on `Pending`, the oldest from 2022.**
519
-
520
- **The fix - a stage-only write from INSIDE the closed block, before the `continue`:**
514
+ ### Closed NetSuite orders (rule 2026-09-29)
521
515
 
522
- - new `App_Api_Toga2::updateTransferOrderStageToClosed(&$nsOrder, array &$clientConfiguration): bool`
523
- plus the constant `TRANSFER_ORDER_STATUS__CLOSED = 'closed'` (now also used by the `case 'Closed'`
524
- in the status switch);
525
- - called only when `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` **and** `isTransferOrder()`;
526
- - it writes **only `transferOrderStage`** - never line items, so the URI-length / **HTTP 414** blowup
527
- that the skip exists to avoid cannot happen;
528
- - a transfer order we never imported is **left alone**, not created.
529
-
530
- **Why not just route closed transfers into the full `syncTransferOrderFromNetsuite`:** that function
531
- **CREATES** a transfer order when it finds none, so it would invent rows out of years-old closed
532
- NetSuite orders for Adyen, Elite and GroWrk. **17 of the 20 launchers have
533
- `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` false**, so nothing changes for them; the three that have it
534
- true all use `ZERO_DOLLAR_HOLD` detection.
535
-
536
- **Status: written, NOT committed and NOT deployed** as of 2026-09-08. Touches `worker` and `library`,
537
- so both deploy together (the standing rule for this pair).
516
+ A Closed SO **with** related records (fulfillment, invoice, PO…) is completed — imported like any order.
517
+ A Closed SO with **none** is a duplicate — never imported, deleted if already in TOGa. Replaces the
518
+ 2026-09-23 "delete every Closed" rule and the 2026-09-08 stage-only `updateTransferOrderStageToClosed`
519
+ (removed). Full rule, KEEP guard and the ACL the delete needs (a gap freezes SALES_ORDERS):
520
+ [netsuite-closed-order-rule](../../library/features/netsuite-closed-order-rule.md).
538
521
 
539
522
  ### ⚠ An OUT-OF-SCOPE transfer-order destination customer wedged INVENTORY_ADJUSTMENTS — the importer now SKIPS it (fixed + deployed 2026-09-14)
540
523
 
@@ -1093,6 +1076,9 @@ discovered per-client.
1093
1076
  scheduled wrapper runs. Uses `NETSUITE_SYNC_LOOKUPS_ONLY` (early return after LOOKUPS in
1094
1077
  `common_sync_togasupply.php`).
1095
1078
 
1079
+ **Given fulfillments / receipts, any client:** `process_netsuite_records.php <client> --fulfillment=ids --receipt=ids`
1080
+ — same [workflow doc](../workflows/reprocessing-one-netsuite-order.md).
1081
+
1096
1082
  Two CLI scripts written for the Elite backfill live under
1097
1083
  `test/@srija/Elite Testing/Service Requests/` (they are operator tools, **not** scheduled crons —
1098
1084
  do not add them to `cron.worker.sync.json`):
@@ -1771,30 +1757,13 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
1771
1757
  onboarding effort.
1772
1758
  Both are pre-existing and unrelated to this session's fixes.
1773
1759
 
1774
- - **⚠ A serialized unit's OWN location can differ from its receipt line's — auto-create it before the
1775
- `/units` write or api2 400s EV-12 (fixed 2026-08-29, `library/app/api/toga2.php`
1776
- `syncItemReceiptFromNetsuite`).** The unit carries its own `custitemnumber_location` (referenced on the
1777
- `/units` write by `c_netsuiteInternalLocationId`); when that location differs from the receipt line's
1778
- and isn't provisioned in the client DB, api2 rejects the unit write **EV-12**. The fix now **POSTs
1779
- `/locations` for the unit's own location before the write**, mirroring the existing item-line location
1780
- create one level up. Shared/general fix (any client running serialized item receipts), surfaced by
1781
- **GroWrk** location **1569**. Deployed.
1782
- - **The inventory-ADJUSTMENT unit path now has the SAME auto-create (fixed 2026-08-30) — it did not
1783
- before, which froze GroWrk.** `syncInventoryAdjustmentFromNetsuite`'s serialized-unit processing took
1784
- the unit's location from the inventory number's `custitemnumber_location` (the unit's own current
1785
- location, falling back to the adjustment line location), but — unlike `syncItemReceiptFromNetsuite` —
1786
- **never auto-created a missing warehouse**, so `POST /units` failed **EV-12** "unable to find a unique
1787
- match" on `location` (`searchCriteria c_netsuiteInternalLocationId=1596`; **0 rows** in
1788
- `Client_Growrk.Locations` for 1596), leaving `NETSUITE_EXECUTION_MODE_INVENTORY_ADJUSTMENTS` stuck at
1789
- `1-RUNNING`. **Fix:** capture the unit location's NAME (`$unitNetsuiteLocationName` from the
1790
- `custitemnumber_location` ref, carried in `$unitsToGet` as `netsuiteLocationName`) and, before **both**
1791
- unit-location reference points (the per-unit PUT-create path and the batch `POST /units` path),
1792
- `POST /locations` when the id is not already in `$lookupWarehouseByNetsuiteLocationInternalId` and a
1793
- name is present — same payload as the receipt sync (`name`, `c_netsuiteInternalLocationId`,
1794
- `locationType = WAREHOUSE_LOCATION_TYPE_UUID`, `isActive=true`, `allowReceiving`/`allowSending=false`),
1795
- then register it in the warehouse lookup. Guarded by `!isset(lookup[id]) && !empty(name)` so it fires
1796
- only for genuinely-missing locations and never duplicates. Shared 1.0 path — benefits all
1797
- serialized-adjustment clients.
1760
+ - **The sync NEVER auto-creates a warehouse Location (rule 2026-09-29, all clients).** An unknown NetSuite
1761
+ location throws via `App_Api_Toga2::throwIfUnknownWarehouse()` (toga2.php): "Verify it, create the Location with
1762
+ `c_netsuiteInternalLocationId` = <id>, then rerun." It replaced 7 `POST /locations` spots: receipt pre-check over
1763
+ ALL lines before any write, receipt + adjustment unit paths, TO origin + destination. A person verifies and creates
1764
+ the Location (client needs a Warehouse `LocationTypes` row, shared uuid `78c24f51-b33b-438b-e3f2-b6232d9ecb75`).
1765
+ - **Unit location comes from the receipt/adjustment LINE, not `custitemnumber_location`.** That field is a bin/bay
1766
+ custom list whose ids overlap real location ids, so reading it as a location put units in the wrong warehouse.
1798
1767
 
1799
1768
  - **⚠ The exact api2 rejection detail lives in `Logs_<Client>.Api`, not `Logs.Event` (which TRUNCATES
1800
1769
  it).** When a section throws on an api2 non-success, `Logs.Event` records the error envelope but
@@ -1983,17 +1952,6 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
1983
1952
  invoice against a duplicated SO it will false-throw the same way, and **their duplicates have a DIFFERENT
1984
1953
  cause than Compass's Office-Depot dual-source.** Also a data-quality question: why do six non-Compass
1985
1954
  clients have duplicate SalesOrders rows at all.
1986
- - **⚠ ITEM_RECEIPTS auto-creates a warehouse Location — the client's `LocationTypes` MUST have a
1987
- "Warehouse" row or it EV-12-freezes (fixed 2026-09-17, Prudential).** `syncItemReceiptFromNetsuite`
1988
- (`library/app/api/toga2.php`) `POST /v2/locations` with `locationType.uuid = WAREHOUSE_LOCATION_TYPE_UUID
1989
- = 78c24f51-b33b-438b-e3f2-b6232d9ecb75` (:3997). `Client_Prudential.LocationTypes` had only a "Shipping"
1990
- row — no "Warehouse" — so the reference didn't resolve → **EV-12** (prod `Logs.Issue` 757, clientId 5,
1991
- ITEM_RECEIPTS `1-RUNNING`, cursor stuck 2026-09-14 15:12:43; confirmed via `Logs_Prudential.Api` `POST
1992
- /v2/locations` 400). **Fix:** guarded/re-runnable INSERT of the Warehouse `LocationTypes` row using the
1993
- **same literal uuid every client uses** (the documented shared-lookup-uuid exception to "uuids must be
1994
- random"; `Client_Nychh` has it as id 2) —
1995
- `dbchanges2/Client_Prudential/2026-09-17 - SeedWarehouseLocationType.sql`. **The live `LocationTypes`
1996
- column is `name`, NOT the older `type`.** The section self-heals once the row exists (no manual mode reset).
1997
1955
  - **⚠ ROOT CAUSE of the duplicate `Items`: the inventory-adjustment item-create OMITTED the catalog
1998
1956
  (fixed 2026-08-31).** `getCreateItem` (`toga2.php:6233`) and the fulfillment create (~4951) stamp
1999
1957
  `'catalog' => ['name' => 'Agilant']`; `syncInventoryAdjustmentFromNetsuite`'s item-create did **not** →
@@ -2086,6 +2044,7 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
2086
2044
  [NetSuite Sync Alert Monitor](../../library/features/netsuite-sync-alert-monitor.md).
2087
2045
 
2088
2046
  ## Change history
2047
+ - 2026-09-30 — Closed SO rule: keep if related records, delete only if none ([doc](../../library/features/netsuite-closed-order-rule.md)). Sync never auto-creates warehouse Locations (`throwIfUnknownWarehouse`); unit location from the line, not `custitemnumber_location`. Added `process_netsuite_records.php` and the stuck-sync diagnosis path. (rgirish)
2089
2048
  - 2026-09-24 — TO↔PO links: two-direction reconcile (vendor POs no longer upstream; retireStale moves downstream SO links to 326/330); fixed SO downstream line-link deletes on PO re-sync; new `process_netsuite_order.php` + `NETSUITE_SYNC_LOOKUPS_ONLY`. (jcardinal)
2090
2049
  - 2026-09-22 — **The `Logs.Issue` 589 doom-loop's framework root cause is now fully explained and has
2091
2050
  tooling, but this sync is NOT yet patched.** Three independent reasons the 2006 reconnect was dead
@@ -1,24 +1,26 @@
1
1
  ---
2
- title: "Re-processing one NetSuite order (process_netsuite_order.php operator CLI)"
2
+ title: "Re-processing NetSuite orders, fulfillments and receipts (process_netsuite_order.php / process_netsuite_records.php)"
3
3
  framework: "1.0"
4
4
  repo: worker
5
5
  project: Worker
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-24
10
- owners: [jcardinal]
9
+ updated: 2026-09-30
10
+ owners: ["jcardinal", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/process_netsuite_order.php
13
+ - worker/crons/toga2/netsuite/process_netsuite_records.php
13
14
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
14
15
  - library/app/api/netsuite/rest.php
15
16
  related:
16
17
  - ../features/netsuite-togasupply-per-client-sync.md
17
18
  - ../../library/features/transfer-order-purchase-order-link-reconcile.md
18
19
  - ../../../clients/nychh/features/netsuite-transfer-order-import.md
20
+ - ../../library/features/netsuite-closed-order-rule.md
19
21
  ---
20
22
 
21
- Run the real SO/TO sync + its vendor POs for ONE NetSuite order on ONE client, without touching cursors; open to heal or debug a single order.
23
+ Run the real sync for given NetSuite ids on ONE client, without touching cursors: one order (+ its vendor POs), or a list of fulfillments/receipts. Open to heal, debug, or backfill specific records.
22
24
 
23
25
  ## Summary
24
26
  - Manual CLI, **not scheduled** — never add to `cron.worker.*.json`.
@@ -39,14 +41,24 @@ Steps the script runs:
39
41
  2. Defines `NETSUITE_SYNC_LOOKUPS_ONLY` and requires the wrapper → `common_sync_togasupply.php` builds config + lookups, then returns before any section.
40
42
  3. `--tranid` → `App_Api_Netsuite_Rest::findSalesOrderIdByTranId()` (format-validated).
41
43
  4. Refuses an order whose customer / end-user customer does not map to this client.
42
- 5. Closed → `deleteClosedOrderFromNetsuite`; TO (detection mode) → `syncTransferOrderFromNetsuite` (includes the [two-direction PO link reconcile](../../library/features/transfer-order-purchase-order-link-reconcile.md)); else → `syncSalesOrderFromNetsuite`.
44
+ 5. Closed with no related records → `deleteClosedOrderFromNetsuite` ([rule](../../library/features/netsuite-closed-order-rule.md)); Closed with related records syncs like any order; TO (detection mode) → `syncTransferOrderFromNetsuite` (includes the [two-direction PO link reconcile](../../library/features/transfer-order-purchase-order-link-reconcile.md)); else → `syncSalesOrderFromNetsuite`.
43
45
  6. `listPurchaseOrderIdsCreatedFromOrder()` → `syncPurchaseOrderFromNetsuite` for every vendor PO created from the order.
44
46
 
45
47
  Verify: grep the PHP error log for `reconcileTransferOrderPurchaseOrderLinks` and `syncPurchaseOrderFromNetsuite: DELETE`.
46
48
 
49
+ ### Fulfillments / receipts: `process_netsuite_records.php`
50
+ ```
51
+ php crons/toga2/netsuite/process_netsuite_records.php <client> --fulfillment=<id>[,<id>] --receipt=<id>[,<id>]
52
+ ```
53
+ - Same guards: refuses while `sync_togasupply_<client>.php` runs; `NETSUITE_SYNC_LOOKUPS_ONLY`; no cursor writes; fail-loud.
54
+ - Runs the real `syncItemReceiptFromNetsuite` (receipts first; the receipt sync also syncs its PO) then `syncItemFulfillmentFromNetsuite`.
55
+ - Scope checks: a fulfillment's source SO customer (or end-user customer) must map to this client; with `IS_ENABLED_ITEM_RECEIPT_OWNED_WAREHOUSE_SCOPE` a receipt needs a line at an owned warehouse (`allowReceiving = 1`). Otherwise it throws "refusing to import it".
56
+ - Used 2026-09-29 to backfill 7 Elite closed-order shipments skipped under the old closed rule.
57
+
47
58
  ## Gotchas
48
59
  - A TO skipped by the sync (out-of-scope customer) prints `NOT imported` — read the error log, not a script bug.
49
60
  - Needs `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` true in the wrapper for TO routing; otherwise the order syncs as a sales order.
50
61
 
51
62
  ## Change history
63
+ - 2026-09-30 — Added `process_netsuite_records.php` (fulfillments/receipts by id); Closed routing follows the new keep/delete rule. (rgirish)
52
64
  - 2026-09-24 — Created; prod-verified on NYCHH TO 11. (jcardinal)
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 29 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 30 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 40 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
10
10
  - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
@@ -11,7 +11,7 @@ project: Worker
11
11
  client: adyen
12
12
  type: profile
13
13
  status: draft
14
- updated: 2026-09-28
14
+ updated: 2026-09-30
15
15
  owners: ["rgirish"]
16
16
  files:
17
17
  - worker/crons/toga2/netsuite/sync_togasupply_adyen.php
@@ -64,7 +64,7 @@ Adyen tests the tracking API on **prod**. `2026-09-22a - AdyenApiTestSeed.sql` s
64
64
  - **Why this prevents duplicates:** the sync matches existing rows by `c_netsuiteInternalSalesOrderId` / `c_netsuiteInternalItemFulfillmentId` (`_underscore/Trait/Netsuite/SalesOrder.php`, `ItemFulfillment.php`). It will replace the dummy `SalesOrderItems` (itemId 6).
65
65
  - **⚠ Leftovers:** dummy POs `PO-SR%` and vendor `V-TEST-001` still exist for the linked SOs.
66
66
 
67
- **⚠ The Adyen NetSuite sync has never run.** `Parameters` cursors stuck at `2026-08-20 00:00:00|0`, the `Customers` row has no `c_netsuiteInternalCustomerId`, no `Locations`. Set these up before expecting sync data.
67
+ **⚠ The Adyen NetSuite sync has never run.** `Parameters` cursors stuck at `2026-08-20 00:00:00|0`, the `Customers` row has no `c_netsuiteInternalCustomerId`, no `Locations`. Set these up before expecting sync data. **Also (2026-09-30): the launcher `sync_togasupply_adyen.php` is missing the constants `SHIPPING_LOCATION_TYPE_UUID`, `WAREHOUSE_LOCATION_TYPE_UUID` and `NETSUITE_CUSTOM_FIELD_ID__END_USER_CUSTOMER`** (Elite's launcher has them) — add them before the first run.
68
68
 
69
69
  **How to match an Adyen SR to its NetSuite SO — there is NO shared key.**
70
70
 
@@ -21,7 +21,7 @@ project: _Underscore
21
21
  client: compass-usa
22
22
  type: profile
23
23
  status: active
24
- updated: 2026-09-25
24
+ updated: 2026-09-30
25
25
  owners: [jcardinal, bala, tcox, apeterson, dfranks, akhokhani, ajean, rgirish]
26
26
  files: []
27
27
  related:
@@ -219,6 +219,7 @@ separate, related client (see its own profile).
219
219
  the ⚠ rule that **deactivation is raw bulk SQL, so no model hook ever fires on it**.
220
220
 
221
221
  ## Notes
222
+ - **⚠ SALES_ORDERS froze 2026-09-29 on ACL gaps the closed-order delete hit** (transfer-orders 312/313, entitlement-sales-orders 257, sales-order-payments 254). Fixed by `dbchanges2/Client_Compass/2026-09-29a - TransferOrderAclExpressions.sql`, then mode reset to `864000-IDLE`. Details: [closed-order rule](../../1.0/apps/library/features/netsuite-closed-order-rule.md).
222
223
  - **⚠ The 1.0 NetSuite sync links item fulfillments ONLY to `customerId=3` / `locationId=615286`
223
224
  SalesOrders (2026-09-16).** This Compass-specific hardcoded scope filter lives in
224
225
  `library/app/api/toga2.php` (`syncItemFulfillmentFromNetsuite` ~L5062-5064,
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Doc | Framework | Summary |
4
4
  |-----|-----------|---------|
5
- | [Elite inventory quantities — drop ship is excluded from On Hand / Available ONLY](features/inventory-quantities-drop-ship.md) | 2.0 | Elite's Inventory page showed **negative Qty On Hand / Qty Available** (worst: `SVC-CI-RETAINER-RS` at **-235**). |
5
+ | [Elite inventory quantities — owned-warehouse STOCK lines + Transfer Orders; drop ship excluded](features/inventory-quantities-drop-ship.md) | 2.0 | How Elite's `_qtyOnHand` / `_qtyAvailable` are computed (`_Model_Elite_Item`) so they match NetSuite: only STOCK lines at warehouses Elite owns, plus Transfer O |
6
6
  | [Elite — NetSuite → TOGa Supply inbound sync (TRUE-80499 onboarding)](features/netsuite-togasupply-sync.md) | 1.0 | Elite is the 18th client on the shared NetSuite → TOGa Supply importer ([engine](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)). |
7
7
  | [Elite SalesOrder → NetSuite Push (postPost/postPut interceptors → worker2)](features/salesorder-netsuite-push.md) | 2.0 | Elite's event-driven SalesOrder → NetSuite push (postPost/postPut interceptors → worker2); open for the push trigger, interceptor registration, or the 2026-08-2 |
8
8
  | [Elite — Sales Order stage change posts a reply on the TOGa Desk (1.0) ticket](features/salesorder-status-togadesk-reply.md) | 2.0 | How an Elite sales-order stage change posts a reply back onto the originating TOGa Desk (1.0) ticket; open for the cross-DB join path or the dedup key. |
@@ -1,89 +1,49 @@
1
1
  ---
2
- title: "Elite inventory quantities — drop ship is excluded from On Hand / Available ONLY"
2
+ title: "Elite inventory quantities — owned-warehouse STOCK lines + Transfer Orders; drop ship excluded"
3
3
  framework: "2.0"
4
4
  repo: _underscore
5
5
  project: _Underscore
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-17
9
+ updated: 2026-09-30
10
10
  owners: [rgirish]
11
11
  files:
12
12
  - _underscore/Model/Elite/Item.php
13
13
  - _underscore/Model/Client/Item.php
14
+ - worker/crons/toga2/netsuite/sync_togasupply_elite.php
14
15
  related:
15
16
  - ../profile.md
16
17
  - ./netsuite-togasupply-sync.md
17
18
  - ../../../2.0/apps/_underscore/features/calculated-sql-fields.md
18
19
  - ../../nychh/features/transfer-order-inventory-quantities.md
19
- - ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
20
+ - ../../../1.0/apps/library/features/netsuite-suiteql-api-reference.md
20
21
  ---
21
22
 
22
- ## Summary
23
-
24
- Elite's Inventory page showed **negative Qty On Hand / Qty Available** (worst:
25
- `SVC-CI-RETAINER-RS` at **-235**). Cause: **drop-ship lines are fulfilled but never received into
26
- stock**, so the base `received - fulfilled` arithmetic went negative.
23
+ How Elite's `_qtyOnHand` / `_qtyAvailable` are computed (`_Model_Elite_Item`) so they match NetSuite: only STOCK lines at warehouses Elite owns, plus Transfer Order lines.
27
24
 
28
- The rule Elite runs on: **exclude drop ship from `_qtyOnHand` and `_qtyAvailable` ONLY.** Every
29
- other quantity counts **all** lines — a drop-ship line really was ordered and really did ship.
25
+ ## Summary
26
+ - **On hand** = received − shipped (STOCK SO lines at an owned location + Transfer Order lines with an owned origin) + adjusted.
27
+ - **Available** = on hand − committed on STOCK SO lines at an owned location − committed on Transfer Order lines with an owned origin (`qtyCommittedTransferOrders`, new 2026-09-29).
28
+ - **Owned** = `Locations.allowReceiving = 1`. Today only Elite Location 1 = NetSuite 196 "Elite" (set by `Client_Elite/2026-09-28e`).
29
+ - Why Transfer Orders: Elite's $0 hold-invoice SOs import as TransferOrders, so their shipped and committed qty sit on `TransferOrderItems` and were ignored before.
30
+ - Verified on prod 2026-09-29 against NetSuite `aggregateitemlocation` (not `inventorybalance` — see [SuiteQL gotcha](../../../1.0/apps/library/features/netsuite-suiteql-api-reference.md)).
30
31
 
31
32
  ## How it works
33
+ - Private helpers on `_Model_Elite_Item`, used only by the two composites: `qtyFulfilledStockOnly`, `qtyFulfilledTransferOrders`, `qtyCommittedStockOnly`, `qtyCommittedTransferOrders`. Shipped = fulfillment status slug `shipped`.
34
+ - SO lines need `SalesOrderItems.locationId` at an owned location. The Elite sync stamps it from the NetSuite line location (`IS_ENABLED_NETSUITE_LINE_LOCATION`, see [Elite sync](./netsuite-togasupply-sync.md)). Lines shipped from NS 5 "Main" have NULL `locationId` and drop out.
35
+ - Leaf fields `_qtyFulfilled`, `_qtyCommitted`, `_qtyInvoiced`, `_qtyBackordered`, `_qtyOnOrder` stay inherited and count all lines (drop ship included).
36
+ - Base `_Model_Client_Item` composites use `static::` (not `self::`) so this override takes effect — framework-wide, see [calculated fields](../../../2.0/apps/_underscore/features/calculated-sql-fields.md).
32
37
 
33
- Two `private static` helpers on `_Model_Elite_Item`, used **only** by the two composites:
34
-
35
- | Helper | Used by |
36
- |---|---|
37
- | `qtyFulfilledStockOnly()` | `_qtyOnHand` |
38
- | `qtyCommittedStockOnly()` | `_qtyAvailable` |
39
-
40
- `_qtyFulfilled` and `_qtyCommitted` themselves stay **inherited and unfiltered** — the filtering
41
- lives in the composites, not in the leaf fields, so anything reading the leaf number still sees the
42
- true shipped/committed total.
43
-
44
- **Quantities that count ALL lines (drop ship included):** `_qtyOrdered`, `_qtyFulfilled`,
45
- `_qtyInvoiced`, `_qtyBackordered`, `_qtyOnOrder`.
46
-
47
- ### Two deliberate decisions — do not "improve" either
48
-
49
- - **No `GREATEST(..., 0)` floor.** Negatives must stay **visible** so the underlying data problem
50
- can be traced. Flooring hides a real defect behind a clean-looking zero. This was decided
51
- explicitly, after one iteration.
52
- - **A NULL `fulfillmentType` is NOT treated as `STOCK`.** Only a line positively marked stock counts
53
- toward on-hand. (NULL `fulfillmentType` has its own causes — see the
54
- [importer doc](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).)
55
-
56
- ### The base model had to change too (`self::` → `static::`)
57
-
58
- `_Model_Client_Item`'s four composites (`_qtyOnHand`, `_qtyAvailable`, `_qtyBackordered`,
59
- `_qtyOnOrder`) called their leaf methods with **`self::`**, which silently ignores a child override.
60
- They were switched to **`static::`** in the same session. That is a **framework-level** change
61
- affecting **all 25 client `Item` models** — full detail and the NYCHH watch item on
62
- [FIELD_SQL calculated fields](../../../2.0/apps/_underscore/features/calculated-sql-fields.md).
63
-
64
- ## Verification
65
-
66
- Run against prod data after the change: **0 negatives on all 79 non-service items.** The **7**
67
- remaining negatives are all `SVC-` service items (received 0, fulfilled > 0), which the developer
68
- explicitly said to ignore.
38
+ ### Decisions — do not "improve"
39
+ - **No `GREATEST(..., 0)` floor.** Negatives stay visible: they mean a missing receipt or an unsynced `fulfillmentType`.
40
+ - **NULL `fulfillmentType` is NOT stock.** Only `'STOCK'` counts.
69
41
 
70
42
  ## Gotchas / known issues
71
-
72
- - **The Inventory TableView still filters services by `(Items.partNumber:excludes:SVC-)`**, which
73
- **misses 18 service items** that sit under service `ItemClasses` trees but carry normal part
74
- numbers. Undecided and not implemented. Facts established while investigating:
75
- - **`_isServiceClass` does not exist** anywhere in the code (grepped all of `Model/` and
76
- `Trait/`) — the migration comment referencing it is **stale**.
77
- - The table is **`ItemClasses`**, not `ItemClassifications`, and it is a **tree**
78
- (`parentItemClassId`).
79
- - Service roots are **Lifecycle Services (1)**, **Advisory and Modernization (22)**, **True
80
- Partner Services (40)** — but **6 non-service items** sit under the latter two, so matching on
81
- root alone is too broad.
43
+ - **Inventory page filter** is data, in `Client_Elite/2026-09-28b`: hides `SVC-`/`SVT-`/`LIC-`/`CON-` parts, AppleCare, service item classes and drop-ship-only items; shows only rows with nonzero on hand or available.
44
+ - **Known gap: Docking 1 is below NetSuite** — RMA return receipts (created from a Return Authorization) are not imported by the sync, so returned stock is missing. Open.
45
+ - Owned-warehouse scope is Elite-only today; other clients have fake "bay" warehouses (see [Elite sync](./netsuite-togasupply-sync.md)).
82
46
 
83
47
  ## Change history
84
- - 2026-09-17 — Documented Elite's drop-ship exclusion rule: drop ship is removed from `_qtyOnHand`
85
- and `_qtyAvailable` only, via two `private static` helpers, while every other `_qty*` counts all
86
- lines; no `GREATEST` floor (negatives must stay visible) and NULL `fulfillmentType` is not treated
87
- as stock. Verified 0 negatives on all 79 non-service items in prod. Also recorded the paired base
88
- change from `self::` to `static::` on `_Model_Client_Item`'s composites, and the open
89
- `partNumber:excludes:SVC-` TableView filter that misses 18 service items. (rgirish)
48
+ - 2026-09-30 — New formula: owned-warehouse (`allowReceiving = 1`) STOCK SO lines + Transfer Order shipped/committed (`qtyCommittedTransferOrders`); verified vs NetSuite `aggregateitemlocation`. Inventory filter 2026-09-28b; RMA receipt gap recorded. (rgirish)
49
+ - 2026-09-17 — Drop ship excluded from `_qtyOnHand` / `_qtyAvailable` only; no floor; NULL `fulfillmentType` not stock; base composites `self::` → `static::`. (rgirish)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-24
9
+ updated: 2026-09-30
10
10
  owners: ["snaredla", "jcardinal", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/sync_togasupply_elite.php
@@ -409,8 +409,20 @@ configuration problem, and it only appears **after a section catches up** — wh
409
409
  Elite's PO backfill just did. **NOT FIXED** (shared file, 22 clients). Full root cause on the
410
410
  [engine doc](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
411
411
 
412
+ ### Elite-only scope flags (2026-09-29) — Elite stock = its own warehouse only
413
+
414
+ Set in `sync_togasupply_elite.php`; they feed the [inventory formula](./inventory-quantities-drop-ship.md).
415
+ - `IS_ENABLED_ITEM_RECEIPT_OWNED_WAREHOUSE_SCOPE` — a receipt at an owned warehouse (`Locations.allowReceiving = 1`) is Elite stock even when the PO has no Elite customer.
416
+ - `IS_ENABLED_NETSUITE_LINE_LOCATION` — stamps `SalesOrderItems.locationId` from the NetSuite line location. Needs the API-role write grant on that field (`Client_Elite/2026-09-28e`).
417
+ - `IS_ENABLED_SKIP_TRANSFER_ORDERS_FROM_UNKNOWN_WAREHOUSE` — a TO shipped from a non-Elite NS location (e.g. 5 "Main") is skipped + logged instead of throwing.
418
+ - **Only Elite is safe for the owned-warehouse flag today.** Audit 2026-09-29: other clients have fake "bay" warehouses, and Trividia Locations 3/4 hold SBA customers 1171/22311 (open bug). Audit a client's `Locations` before turning it on.
419
+ - Owned today: Location 1 = NS 196 "Elite" (`allowReceiving = 1` via `2026-09-28e`, with its ACL).
420
+
412
421
  ## Gotchas / known issues
413
422
 
423
+ - **Data fixes 2026-09-28/29 (`dbchanges2/Client_Elite/`):** `2026-09-28b` inventory filter, `28d`, `28e` (ACL + `allowReceiving = 1` on NS 196), `2026-09-29a - EliteReceiptLinkAndMissingSalesOrder.sql` (relinks receipt 58882 line to PO line 1765; inserts missing SO 289992). 7 closed-order shipments backfilled with `process_netsuite_records.php` after the [closed-order rule](../../../1.0/apps/library/features/netsuite-closed-order-rule.md) change.
424
+ - **Open 2026-09-30:** Elite sync cron re-enabled locally in `worker/schedules/cron.worker.sync.json` — pending deploy. RMA return receipts are not imported (Docking 1 below NetSuite).
425
+
414
426
  - **⚠ OPEN, unrelated: `GET /v2/notes/{uuid}/files` returns 405 `EV-7`** ("method not allowed /
415
427
  route does not exist") repeatedly for Elite. Found while diagnosing the 2026-09-08 transfer-order
416
428
  incident; it is **not** part of it. It does **not** abort the sync, and it was still occurring
@@ -450,6 +462,7 @@ Elite's PO backfill just did. **NOT FIXED** (shared file, 22 clients). Full root
450
462
  rewinding `NETSUITE_LAST_SYNC_DATETIME_SALES_ORDERS`.
451
463
 
452
464
  ## Change history
465
+ - 2026-09-30 — Elite-only scope flags (owned-warehouse receipts, NS line location, skip TOs from unknown warehouse); NS 196 marked owned; receipt/SO data fixes; 7 closed-order shipments backfilled. (rgirish)
453
466
  - 2026-09-24 — TO↔PO links now via the two-direction reconcile; some upstream rows were vendor POs; SO downstream line links were being deleted on every PO re-sync (55 of 97 SO-linked vendor POs had none) — fixed. (jcardinal)
454
467
  - 2026-09-18 — **Stale-SalesOrder backfill is live: 218 → 187.** The retirement method needed
455
468
  four more FK dependencies before it would run (fulfillment **header** `salesOrderId`, the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.918",
3
+ "version": "1.0.919",
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",