toga-ai 1.0.227 → 1.0.229

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.
@@ -4,6 +4,8 @@
4
4
  |-----|---------|-------|
5
5
  | [Tools (1.0 Internal-Tools App) Architecture](architecture.md) | **Tools** is a standalone 1.0 (`App_`) application that houses many small internal tools behind simple interfaces, gated by Client_True staff persona. | tools/index.php, tools/_/app/framework.php, tools/_/app/frameworkindex.php, tools/assets/img/favicon/favicon.ico, tools/assets/img/favicon/favicon-32x32.png, tools/assets/img/favicon/favicon-16x16.png, tools/assets/img/favicon/apple-touch-icon.png, tools/_/app/auth.php, tools/_/app/nav.php, tools/common/header.php, tools/common/footer.php, tools/mvc/get.php, tools/mvc/_TEMPLATE/get.php, tools/docs/ADDING_A_TOOL.md |
6
6
  | [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | The first two tools shipped in the Tools app, both under the **Developers** folder and gated to personas **Development Team** / **TOGa Technology**. | tools/mvc/developers/uuid/get.php, tools/mvc/developers/password/get.php |
7
+ | [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to the Tools app — URL routing, CSRF, and DB access through `App_Databa | tools/_/app/nav.php, tools/mvc/get.php |
7
8
  | [Tools Persona-Gated Navigation (App_Nav)](features/persona-gated-navigation.md) | `App_Nav` is the Tools app's two-level, **persona-gated** navigation. | tools/_/app/nav.php, tools/mvc/get.php |
8
9
  | [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/initiate/get.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php, tools/mvc/get.php, tools/config.production.ini |
10
+ | [Talos Pricing UI (Onboarding, Dashboard, Benchmarks, Cost Factors + Estimator)](features/talos-pricing-ui.md) | The 1.0 (tools app) face of the **Talos Pricing Platform** — a "Talos Pricing" nav folder with four pages plus a client-side estimate engine. | tools/_/app/nav.php, tools/_/app/talos/estimator.php, tools/mvc/talos/onboarding/get.php, tools/mvc/talos/onboarding/post.php, tools/mvc/talos/pricing/get.php, tools/mvc/talos/benchmarks/get.php, tools/mvc/talos/factors/get.php, tools/mvc/talos/factors/post.php, tools/assets/css/style.css |
9
11
  | [Deploying Tools to Elastic Beanstalk (PHP 8.5 / Amazon Linux 2023)](workflows/deploy-to-elastic-beanstalk-al2023.md) | How the **Tools** 1.0 app boots on Elastic Beanstalk running `PHP 8.5 on 64bit Amazon Linux 2023/4.13.1 (aarch64)`. | tools/.ebextensions/004_http_to_https.config, tools/.ebextensions/006_mount-s3fs.config, tools/.ebextensions/007_setup_export_cache_folders.config, tools/.ebextensions/008_setup_ldap.config, tools/.ebextensions/009_setup_phpini.config, tools/.ebextensions/020_setup_git_libraries.config, tools/.ebextensions/050_register_instance_to_shared_application_load_balancer.config, tools/ebs/git.json |
@@ -0,0 +1,77 @@
1
+ ---
2
+ title: Tools MVC — Routing, CSRF & App_Database Access Patterns
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-29
10
+ owners: [jcardinal]
11
+ files:
12
+ - tools/_/app/nav.php
13
+ - tools/mvc/get.php
14
+ related:
15
+ - ./persona-gated-navigation.md
16
+ - ../architecture.md
17
+ - ../../library/features/mvc-page-pattern-and-app-skeleton.md
18
+ ---
19
+
20
+ ## Summary
21
+
22
+ The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to
23
+ the Tools app — URL routing, CSRF, and DB access through `App_Database` — with the gotchas
24
+ that bite if you assume the legacy 1.0 behavior. Discovered while building the Talos Pricing
25
+ UI; these apply to any tools-app page.
26
+
27
+ ## Routing
28
+ - `App_MVC::parseRoute` maps a URL to `mvc/<segments>/<method>.php`: **GET → `get.php`,
29
+ POST → `post.php`**.
30
+ - **Writes use `mvc/<route>/post.php`** — NOT the legacy
31
+ `App_FrameworkIndex::actionHandler` `targetAction → app/<x>.php` path. A form posts to the
32
+ same route as its GET page; the `post.php` handler does the write and redirects.
33
+
34
+ ## Nav persona narrowing (gotcha)
35
+ `App_Nav::personasForRoute()` returns the **union** of folder + action personas. A page that
36
+ must be **narrower** than its folder (e.g. a technical-only editor inside a broader folder)
37
+ must `requireAuth()` the narrow persona set **explicitly** — do not pass
38
+ `personasForRoute()`, or the page inherits the wider folder grant. (See
39
+ `persona-gated-navigation` for the nav model.)
40
+
41
+ ## CSRF
42
+ `App_Page::validateCrossSiteRequestForgery()` is **Origin/Referer-based**: for any non-GET
43
+ request the request host must equal `HTTP_HOST`. **No hidden token field is needed** — do not
44
+ add one expecting the framework to validate it.
45
+
46
+ ## App_Database
47
+ - Query: `App_Database::query($sql, $alias)`.
48
+ - Row helpers: `buildArrayOfRows(&$res, $includeNumericIndexes)`, `fetchOne(&$res)`,
49
+ `fetchRow(&$res, true)`.
50
+ - Escaper is `App_Database::sqlEscape()` — **not** `::escape`. (Prefer parameterized queries
51
+ per the security rules; `sqlEscape()` is the framework escaper when needed.)
52
+ - Transactions: `beginTransaction` / `commitTransaction` / `rollbackTransaction`.
53
+ - **Connections** come from `config.*.ini` `[database_<x>]` → alias `db_<x>`. A page can read a
54
+ 2.0 DB read-only through its own connection (e.g. `db_true → Client_True`); adding
55
+ `db_team → Team` is the same pattern.
56
+
57
+ ### Gotcha — by-reference row helpers
58
+ `buildArrayOfRows`, `fetchOne`, and `fetchRow` take `$res` **by reference**. Assign the
59
+ `query()` result to a **variable first**, then pass it — passing the call result inline emits
60
+ PHP "Only variables should be passed by reference", which **renders to the page** when
61
+ `display_errors` is on.
62
+
63
+ ```php
64
+ // WRONG — warning leaks to the page
65
+ $rows = App_Database::buildArrayOfRows(App_Database::query($sql, 'db_team'), false);
66
+
67
+ // CORRECT
68
+ $res = App_Database::query($sql, 'db_team');
69
+ $rows = App_Database::buildArrayOfRows($res, false);
70
+ ```
71
+
72
+ ## Autoloader
73
+ `App_Foo_Bar` → `app/foo/bar.php`, **all lowercase** (per the library CLAUDE.md). E.g.
74
+ `App_Talos_Estimator` → `_/app/talos/estimator.php`.
75
+
76
+ ## Change history
77
+ - 2026-06-29 — Documented tools MVC routing (GET→get.php / POST→post.php; writes via mvc/<route>/post.php, not the legacy actionHandler path), Origin/Referer CSRF (no token field), `App_Database` query/row/txn API + `sqlEscape()` name, the by-reference row-helper warning gotcha, the persona-narrowing gotcha, and the lowercase autoloader mapping. Discovered building the Talos Pricing UI. (jcardinal)
@@ -0,0 +1,94 @@
1
+ ---
2
+ title: Talos Pricing UI (Onboarding, Dashboard, Benchmarks, Cost Factors + Estimator)
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-29
10
+ owners: [jcardinal]
11
+ files:
12
+ - tools/_/app/nav.php
13
+ - tools/_/app/talos/estimator.php
14
+ - tools/mvc/talos/onboarding/get.php
15
+ - tools/mvc/talos/onboarding/post.php
16
+ - tools/mvc/talos/pricing/get.php
17
+ - tools/mvc/talos/benchmarks/get.php
18
+ - tools/mvc/talos/factors/get.php
19
+ - tools/mvc/talos/factors/post.php
20
+ - tools/assets/css/style.css
21
+ related:
22
+ - ./persona-gated-navigation.md
23
+ - ./mvc-data-access-patterns.md
24
+ - ../architecture.md
25
+ - ../../../2.0/apps/talos/architecture.md
26
+ - ../../../2.0/apps/talos/features/pricing-cogs-model.md
27
+ - ../../../2.0/apps/worker2/features/talos-pricing-automation.md
28
+ ---
29
+
30
+ ## Summary
31
+
32
+ The 1.0 (tools app) face of the **Talos Pricing Platform** — a "Talos Pricing" nav folder
33
+ with four pages plus a client-side estimate engine. Sales onboard a client and sign a
34
+ contract here; everything else is read-only or technical-only. All four pages read the
35
+ **Team DB** through a new `db_team` connection (`[database_team]` in `config.*.ini`; creds
36
+ added by the developer separately).
37
+
38
+ ## How it works
39
+
40
+ ### Nav
41
+ `App_Nav::definition()` gains a **"Talos Pricing"** folder with 4 actions: Onboarding,
42
+ Pricing Dashboard, Usage Benchmarks, Cost Factors. Persona-gated like every other folder
43
+ (see `persona-gated-navigation`). **Cost Factors is technical-only** — because
44
+ `personasForRoute()` returns the *union* of folder+action personas, the Factors page must
45
+ **narrow** explicitly with its own `requireAuth([technical])` rather than trusting
46
+ `personasForRoute()`.
47
+
48
+ ### Estimator (`App_Talos_Estimator`, `_/app/talos/estimator.php`)
49
+ Reads `TalosCostFactors` (with built-in fallback defaults if a key is missing) and produces
50
+ a live cost/user + monthly estimate and recommended per-band per-user fees. **Sales never
51
+ enters conversations-per-user** — that is the input they cannot know; it is derived from the
52
+ cost factors / usage data we already have. The same math drives the live JS estimate on the
53
+ Onboarding page.
54
+
55
+ ### Onboarding (`onboarding/get.php` + `post.php`)
56
+ Sales enters name / slug / offering / # users / features / # apps / min-max margin band /
57
+ org fee / consecutive-months smoothing window. Live JS shows the estimate and recommended
58
+ per-band fees; fees are editable. **"Sign contract" locks the band schedule** and writes
59
+ `TalosClients` + `TalosPricingBands` in a single transaction. Write goes through
60
+ `mvc/talos/onboarding/post.php` (the App_MVC `post.php` convention — see
61
+ `mvc-data-access-patterns`).
62
+
63
+ ### Pricing model surfaced in the UI
64
+ A flat monthly **org fee** (the adjustable margin lever) PLUS a **per-user fee that steps by
65
+ user-count band**. The band rate schedule is locked at signing; when user count crosses a
66
+ band the per-user fee auto-steps to the pre-agreed rate, then the org fee is adjusted to
67
+ restore margin. The per-user fee never changes outside the agreed schedule. (Full
68
+ methodology + COGS economics in the talos `pricing-cogs-model` doc.)
69
+
70
+ ### Service offerings + cost multipliers
71
+ Three offerings with cost multipliers stored in `TalosCostFactors`: **CHAT = 1.0** (base),
72
+ **VOICE_TO_VOICE = 0.25**, **NATURAL_VOICE = 0.50**.
73
+
74
+ ### Read-only pages
75
+ - **Pricing Dashboard** (`pricing/get.php`) — read-only per-client margin/recommendation view.
76
+ - **Usage Benchmarks** (`benchmarks/get.php`) — read-only Langfuse-derived benchmarks.
77
+ - **Cost Factors** (`factors/get.php` + `post.php`) — technical-only editor for the
78
+ `TalosCostFactors` key/value constants (the "technical tab").
79
+
80
+ ## Gotchas
81
+ - **`db_team` is read-only on the dashboards** but read/write on onboarding + factors. Add
82
+ the `[database_team]` config section (alias `db_team`) — same pattern as `db_true → Client_True`.
83
+ - **Voice multiplier wording is ambiguous** — "1/4 / 1/2 the cost of chat" is flagged; full
84
+ voice modeling is deferred. Treat 0.25 / 0.50 as placeholders.
85
+ - **Narrowing personas:** do not use `personasForRoute()` for a page stricter than its folder
86
+ (Cost Factors). See `mvc-data-access-patterns`.
87
+
88
+ ## Security
89
+ - `config.*.ini` already carries committed plaintext secrets (team-accepted, see architecture
90
+ Known issues), including a reused plaintext DB password across `config.production.ini`
91
+ sections — flagged for rotation. Location only; no values recorded. Do not add more secrets.
92
+
93
+ ## Change history
94
+ - 2026-06-29 — Built the Talos Pricing UI: nav folder + 4 pages (Onboarding w/ live JS estimate and contract-signing that locks the band schedule into `TalosClients`+`TalosPricingBands` in one txn; read-only Pricing Dashboard + Usage Benchmarks; technical-only Cost Factors editor) and `App_Talos_Estimator` (reads `TalosCostFactors`, derives conversations/user so sales need not enter it). Reads Team DB via new `db_team` connection. Offerings CHAT/VOICE_TO_VOICE/NATURAL_VOICE = 1.0/0.25/0.50 (voice ambiguous, deferred). (jcardinal)
@@ -12,6 +12,6 @@
12
12
  | [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
13
13
  | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
14
14
  | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql |
15
- | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
15
+ | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
16
16
  | [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
17
17
  | [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
@@ -11,6 +11,8 @@ owners: [jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Core/Surface.php
13
13
  - dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql
14
+ - dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql
15
+ - dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql
14
16
  - _underscore/Model/Core/SurfaceElement.php
15
17
  - _underscore/Model/Core/Action.php
16
18
  - _underscore/Model/Core/Vocabulary.php
@@ -88,6 +90,20 @@ resolver, not "later"). It is **not** named `debug()` — see gotchas.
88
90
 
89
91
  ## Gotchas
90
92
 
93
+ - **A Core RecordScript needs `Client.AclRecordScripts` rows per role to be invokable — record-level
94
+ `AclRecordPermissions` is necessary but NOT sufficient.** V2 authorizes a scripted-API call **not**
95
+ via `AclRecordPermissions` but via the Client-DB `AclRecordScripts` table
96
+ (`V2.php getRecordScriptPhpMethod: SELECT ... FROM AclRecordScripts WHERE recordScriptId=? AND
97
+ roleId IN(...)`). The original Surface ACL seed built the record-level `AclRecordPermissions` chain
98
+ (Public(1) READ on `surfaces`) but MISSED the per-role dispatch grants, so every meta call 403'd
99
+ (EZ-1) even though record-level READ was granted. Fix: grant the surfaces `meta` script to roles
100
+ Public(1)/SuperUser(3)/Base(4) and the `debug` (metaDebug) script to SuperUser(3) — mirrors the
101
+ existing TableView/Page `meta` scripts. The grant migration is **id-agnostic** (resolves
102
+ `recordScriptId` by route join, so it works on the renumbered 333 block or the originally-provisioned
103
+ 2300 block) and **NOT EXISTS-guarded** (re-runnable). ⚠ Provisioning risk: a `BLANK_CLIENT_DATABASE`
104
+ snapshot may not carry these `AclRecordScripts` rows, so a newly-cut client could repeat the 403
105
+ unless the dated migrations replay on provision — confirm BLANK-snapshot vs migration-replay before
106
+ the next client is cut.
91
107
  - **`_Model_Core_Page::meta()` (Model/Core/Page.php, ~1300 lines) is the engine being replaced** and
92
108
  is dangerous to extend: it interpolates `LIKE '$slug'` (injection-prone), **runs INSERTs during a
93
109
  read**, is N+1, hand-duplicates the 5-layer cascade 6× via UNION, and is uncached. The empirical
@@ -104,7 +120,11 @@ resolver, not "later"). It is **not** named `debug()` — see gotchas.
104
120
  framework base `_Model` defines a NON-static `debug()` (Model.php:167) and the scripted-API
105
121
  dispatch calls the method statically. Renamed to `metaDebug` and remapped the RecordScript
106
122
  (`route 'debug' → phpMethod 'metaDebug'`). Vet any new scripted-API method name against the
107
- `_Model` base (e.g. `debug`, and check others) before mapping it.
123
+ `_Model` base (e.g. `debug`, and check others) before mapping it. **Already-provisioned envs are a
124
+ latent fatal:** the rename landed in code + the renumbered seed, but envs provisioned before it
125
+ (dev.sandbox) keep the old `phpMethod='debug'` and would fatal if `GET /v2/surfaces/debug` were hit.
126
+ Ship an id-agnostic idempotent `UPDATE` (`SurfaceDebugPhpMethodFix.sql`) to repoint the existing
127
+ RecordScript on those envs.
108
128
  - **The endpoint is `GET /v2/surfaces/meta?slug=<slug>`, not `/v2/surfaces/<slug>/meta`.** TOGA
109
129
  RecordScripts read their args from the **query string**; the path form makes the engine parse the
110
130
  slug as a record uuid and return 404 EV-6. Pattern for any RecordScript:
@@ -118,6 +138,12 @@ resolver, not "later"). It is **not** named `debug()` — see gotchas.
118
138
  admin-CRUD sibling records stay Super-User-only (the resolver reads their tables server-side).
119
139
 
120
140
  ## Change history
141
+ - 2026-06-29 — Deploy fixes (round 2): grant the surfaces `meta`/`debug` RecordScripts in
142
+ `Client.AclRecordScripts` per role (Public/SuperUser/Base) — record-level `AclRecordPermissions`
143
+ alone left every meta call 403 EZ-1 because V2 dispatch authorizes via `AclRecordScripts`
144
+ (`SurfaceRecordScriptAcl.sql`, id-agnostic + NOT EXISTS-guarded). Shipped the id-agnostic idempotent
145
+ `SurfaceDebugPhpMethodFix.sql` to repoint the still-`debug` phpMethod on pre-rename envs (latent
146
+ fatal). Flagged BLANK-provision may not carry these dispatch grants. (jcardinal)
121
147
  - 2026-06-29 — Deploy fixes: renamed the scripted inspector `debug()`→`metaDebug()` (name collided
122
148
  with non-static `_Model::debug()`, fatal); corrected the endpoint to query-string form
123
149
  `GET /v2/surfaces/meta?slug=` (path form 404'd EV-6); granted CORE Public(1) READ on the `surfaces`
@@ -50,8 +50,16 @@ traffic), behavior is **byte-for-byte unchanged** — pure data, zero extra work
50
50
  them. Grant Public(1) `allowRead` on `surfaces` only (the admin-CRUD sibling records stay
51
51
  Super-User-only — the resolver reads their tables server-side). See
52
52
  [surface-resolver](../../_underscore/features/surface-resolver.md) gotchas for the full chain.
53
+ - **Record-level READ is necessary but NOT sufficient — dispatch is authorized via
54
+ `Client.AclRecordScripts`.** V2 (`getRecordScriptPhpMethod`) checks `AclRecordScripts` by
55
+ `recordScriptId` + `roleId`, not `AclRecordPermissions`. Even with Public(1) READ on `surfaces`,
56
+ meta 403'd until the `meta`/`debug` scripts were granted per role in `AclRecordScripts`. See
57
+ [surface-resolver](../../_underscore/features/surface-resolver.md) gotchas.
53
58
 
54
59
  ## Change history
60
+ - 2026-06-29 — Documented the second ACL layer: scripted-API dispatch is authorized via
61
+ `Client.AclRecordScripts` (per `recordScriptId`+`roleId`), not record `AclRecordPermissions` —
62
+ record READ alone still 403'd meta until the dispatch grants were seeded. (jcardinal)
55
63
  - 2026-06-29 — Documented the meta-endpoint ACL: normal app users (CORE Public role 1) need
56
64
  `allowRead` on the `surfaces` record or meta returns 403 EZ-1. (jcardinal)
57
65
  - 2026-06-25 — Added the `surface=<slug>` opt-in option to the V2 engine; attaches per-record action
@@ -3,5 +3,5 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
6
- | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql |
6
+ | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql |
7
7
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | How to manually stand up a new 2.0 client (tenant). | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
@@ -14,6 +14,10 @@ files:
14
14
  - dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql
15
15
  - dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql
16
16
  - dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql
17
+ - dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql
18
+ - dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql
19
+ - dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql
20
+ - dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql
17
21
  - dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
18
22
  - dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql
19
23
  - dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql
@@ -66,6 +70,24 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
66
70
  - `Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql` — Core seed + the `RecordScripts` resolve row.
67
71
  - `Core/2026-06-29a - ItemsSurfaceSeed.sql` — the Items LIST screen seed: surfaces `items-list` (TABLE → existing `items` TableView id 9, recordId 21) + `items-list-actions` (BUTTON_BAR: refresh/columns/newItem) + Actions + ~11 Messages. Single DEFAULT bundle, no Client overrides.
68
72
  - `Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql` — grants CORE role Public(1) `allowRead` on the `surfaces` record (so app users stop getting 403 EZ-1 on meta). **Id-agnostic**: matches the record by `route='surfaces'`, so it works whether the record id is 333 or the legacy 2300.
73
+ - `Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql` — id-agnostic idempotent `UPDATE` repointing the
74
+ surfaces `debug` RecordScript `phpMethod` from `debug`→`metaDebug` on already-provisioned envs (the
75
+ rename only landed in the renumbered seed; pre-rename envs would fatal on `GET /surfaces/debug`).
76
+ - `Core/2026-06-29d - VendorItemsSurfaceSeed.sql` — the VendorItems LIST screen seed: `vendor-items-list`
77
+ (TABLE → existing `vendor-items` TableView id 10, recordId 19) + `vendor-items-list-actions`
78
+ (BUTTON_BAR: Refresh / Columns[hidden+disabled] / New Vendor Item) + 2 Actions + 11 Messages.
79
+ Single DEFAULT bundle, mirrors Items. (appId supply=1.)
80
+ - `Core/2026-06-29e - InventorySurfaceSeed.sql` — the Inventory LIST screen, **presentation-only**:
81
+ `inventory-list` (TABLE, **recordId NULL + tableViewId NULL** — the page composes
82
+ purchase-orders/items/units via per-level fetchSlugs and has no single backing Record; carries
83
+ `groupByLabels` + `entityTags` in surface `config`) + `inventory-list-actions` (BUTTON_BAR:
84
+ Group by / Refresh). Actions anchored to `items(21)` because `Actions.recordId` is NOT NULL.
85
+ - `Client/2026-06-29c - SurfaceRecordScriptAcl.sql` — grants the surfaces `meta`/`debug` RecordScripts
86
+ in **`Client.AclRecordScripts`** per role (meta → Public(1)/SuperUser(3)/Base(4); debug →
87
+ SuperUser(3)). V2 authorizes scripted-API dispatch via `AclRecordScripts`, NOT record
88
+ `AclRecordPermissions`, so record READ alone left meta 403 EZ-1. Id-agnostic (resolves
89
+ `recordScriptId` by route join) + NOT EXISTS-guarded (re-runnable). See
90
+ [surface-resolver](../../_underscore/features/surface-resolver.md).
69
91
  - `Client/2026-06-25a - SurfaceClientTables.sql` — the 3 Client tables + `INSERT IGNORE Languages('en','English')`.
70
92
  - `Client/2026-06-25b - SurfaceClientSeed.sql` — `ThemeTokens` + `MessageTranslations(en)`.
71
93
  - `Client/2026-06-25c - SurfaceClientAcl.sql` — full ACL chain for the 3 CLIENT-aclDatabase records (recordId refs updated to the 333-block renumber).
@@ -80,6 +102,12 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
80
102
  or every newly provisioned client is born broken. This was appended and provisioning-tested.
81
103
  Treat it as a CI gate (provision a throwaway client; assert the surface meta endpoint returns the
82
104
  seed).
105
+ - **⚠ Unconfirmed: BLANK-provision may not carry the `AclRecordScripts` dispatch grants.** The
106
+ surface meta endpoint is authorized by `Client.AclRecordScripts` rows (see
107
+ [surface-resolver](../../_underscore/features/surface-resolver.md)), seeded by the dated
108
+ `SurfaceRecordScriptAcl.sql`. If a new client is cut from a BLANK snapshot rather than by replaying
109
+ the dated migrations, it could be born with no dispatch grants and repeat the 403 EZ-1. Confirm how
110
+ BLANK-snapshot vs migration-replay provisioning works before the next client is cut.
83
111
  - **`SurfaceOverrides.value` is a stringly-typed escape hatch** — a long `CONFIG`/label override
84
112
  would silently truncate at varchar(255); that is why `c_longValue mediumtext` exists, and the
85
113
  resolver MUST cast strictly per `attribute` (bool/int/string/json) with a logged fallback.
@@ -95,6 +123,11 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
95
123
  full 4-step ACL chain + field permissions in **each** client DB; resolve `roleId` by subselect.
96
124
 
97
125
  ## Change history
126
+ - 2026-06-29 — Added VendorItems + Inventory LIST seeds (`VendorItemsSurfaceSeed.sql` recordId 19 /
127
+ TableView 10; `InventorySurfaceSeed.sql` presentation-only, recordId+tableViewId NULL, Actions
128
+ anchored to items(21)). Added `SurfaceRecordScriptAcl.sql` (per-role `AclRecordScripts` dispatch
129
+ grants — record READ alone 403'd) and `SurfaceDebugPhpMethodFix.sql` (repoint `debug`→`metaDebug`
130
+ on pre-rename envs). Flagged BLANK-provision may miss the `AclRecordScripts` grants. (jcardinal)
98
131
  - 2026-06-29 — Renumbered the reserved seed id blocks (Records 2300-2308→**333–341**, RecordFields
99
132
  2400+→**2246–2433**) in the seed files only — known file-vs-DB drift in provisioned envs. Added
100
133
  the Items LIST seed (`ItemsSurfaceSeed.sql`) and the id-agnostic CORE Public(1) READ ACL grant on
@@ -6,8 +6,8 @@ project: TOGa IQ
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-06-16
10
- owners: [akhokhani]
9
+ updated: 2026-06-29
10
+ owners: [akhokhani, jcardinal]
11
11
  files:
12
12
  - talos/libs/aegra-api/src/aegra_api/main.py
13
13
  - talos/libs/aegra-api/src/aegra_api/settings.py
@@ -28,6 +28,9 @@ related:
28
28
  - features/mcp-servers.md
29
29
  - features/observability.md
30
30
  - features/deployment.md
31
+ - features/pricing-cogs-model.md
32
+ - ../worker2/features/talos-pricing-automation.md
33
+ - ../../1.0/apps/tools/features/talos-pricing-ui.md
31
34
  ---
32
35
 
33
36
  ## Summary
@@ -152,6 +155,31 @@ lives (env var name, config key) but never the value itself.
152
155
  user-managed** (external). Health check hits `/live`. See
153
156
  `features/deployment.md`.
154
157
 
158
+ ## Talos Pricing Platform (cross-framework)
159
+
160
+ A database-backed pricing/margin system replacing the Excel calculator. Three tiers:
161
+
162
+ - **System of record — Team DB (9 `Talos*` tables, dbchanges2):** TalosClients (contract
163
+ header), TalosPricingBands (locked per-user band schedule), TalosCostFactors (editable
164
+ key/value cost constants — the "technical tab"), TalosUsageMonthly + TalosUsageFeatureMonthly
165
+ (Langfuse import targets), TalosAwsActualsMonthly (Cost Explorer), TalosClientUserCounts
166
+ (headcount for band-breach), TalosCalibrationMonthly + TalosFeeRecommendations (cron-derived).
167
+ Keyed by `clientIdentifier` (= Langfuse `org:<slug>` tag and AWS cost tag) + `periodMonth`
168
+ (1st-of-month DATE), UNIQUE on those for idempotent upsert. Tenant link to `Core.Clients` is a
169
+ plain **indexed column, not a hard FK** (Team DB may be a separate instance). Conventions:
170
+ id+uuid, dtCreated/dtUpdated, InnoDB, utf8mb4_0900_ai_ci.
171
+ - **Sales/admin UI — tools (1.0):** onboarding (locks band schedule), dashboard, benchmarks,
172
+ technical-only Cost Factors editor + `App_Talos_Estimator`. Reads Team DB via `db_team`.
173
+ - **Automation — worker2 (2.0):** monthly crons import AWS actuals, calibrate Langfuse→AWS,
174
+ recompute margins/recommendations, send the leadership report. Uses `_underscore::DB_TEAM`.
175
+
176
+ **Data flow:** Langfuse usage (structure) + AWS Cost Explorer (dollars) → calibrate per client →
177
+ margin vs. locked band → org-fee recommendation after N consecutive out-of-band months.
178
+
179
+ **Critical rules:** Team-DB fact tables are upsert-keyed on `(clientIdentifier, periodMonth
180
+ [, toolCategory])` — never INSERT-only. The Core tenant link is an indexed column, not an FK.
181
+ Always use Langfuse's cache-corrected cost (it undercounts cache badly).
182
+
155
183
  ## Key decisions
156
184
 
157
185
  - **Agent Protocol over a custom shape** — clients (TOGa Hub, TOGa View, third-party)
@@ -13,20 +13,45 @@ related:
13
13
  - ../architecture.md
14
14
  - aegra-api.md
15
15
  - observability.md
16
+ - ../../worker2/features/talos-pricing-automation.md
17
+ - ../../../1.0/apps/tools/features/talos-pricing-ui.md
16
18
  ---
17
19
 
18
20
  ## Summary
19
21
 
20
22
  The cost basis and pricing methodology for selling Talos (TOGa IQ) chat and
21
- voice deployments. The deliverable is a **per-client Excel workbook — TALOS
22
- Pricing Calculator v9** (replaces v8) — that lives on the team's shared AI drive
23
- (under `P:\Work\AI\`), **not** in any repo. This doc captures the durable
24
- knowledge: the cost drivers, measured production rates, and the pricing
25
- decisions, so anyone can re-price Talos without re-deriving the model.
26
-
27
- There is no Talos source code behind this — it is a sales/finance model. Treat
28
- the numbers below as the **April 2026 baseline**; re-measure before relying on
29
- them in a later period.
23
+ voice deployments. This doc captures the durable knowledge: the cost drivers,
24
+ measured production rates, and the pricing decisions, so anyone can re-price
25
+ Talos without re-deriving the model.
26
+
27
+ The COGS token economics below remain valid. **The delivery mechanism has
28
+ pivoted** (2026-06-29) from the per-client Excel workbook (TALOS Pricing
29
+ Calculator v9) to a **database-backed platform**: a Team-DB system of record +
30
+ an onboarding/dashboard UI in the **tools** app (1.0) + **worker2** cron
31
+ automation that calibrates Langfuse usage to AWS actuals each month. Why: Excel
32
+ can't scale and forced sales to guess inputs (esp. avg conversations/user) they
33
+ don't know but we already have in Langfuse. See
34
+ [talos-pricing-ui](../../../1.0/apps/tools/features/talos-pricing-ui.md) and
35
+ [talos-pricing-automation](../../worker2/features/talos-pricing-automation.md);
36
+ a platform architecture doc captures the cross-framework topology.
37
+
38
+ Treat the numbers below as the **April 2026 baseline**; re-measure before relying
39
+ on them in a later period.
40
+
41
+ ## Cost methodology — calibrate Langfuse to AWS (decision, 2026-06-29)
42
+
43
+ Langfuse provides the **structure** (granular per-feature / per-user breakdown);
44
+ AWS provides the **absolute dollars**. Each month, per client:
45
+
46
+ ```
47
+ calibration_factor = AWS_actual / Langfuse_costCorrected
48
+ ```
49
+
50
+ Apply the factor to Langfuse's granular figures so structure comes from Langfuse,
51
+ dollars from AWS. New clients (no AWS history) use a **global blended factor**
52
+ (`SUM(aws) / SUM(langfuse)` across clients) until they have their own rolling
53
+ 3-month factor. **Langfuse badly undercounts cache cost** (sample: $201 recorded
54
+ vs $1,088 corrected) — always use the **cache-corrected** Langfuse figure.
30
55
 
31
56
  ## Chat COGS model
32
57
 
@@ -111,31 +136,41 @@ approach. Per-minute drivers:
111
136
 
112
137
  Adopted to **reduce client-facing price fluctuation while protecting margin**:
113
138
 
114
- 1. **Two-part price.** Client pays a **flat monthly org fee** PLUS a **fixed
115
- monthly per-user fee**. The per-user fee is a stable rate-card value and does
116
- **not** change.
117
- 2. **Margin band.** Sales sets a min and max gross-margin band. The recommended
118
- flat fee is solved so projected margin lands at the **midpoint** of the band:
119
-
120
- ```
121
- flat = COGS / (1 - mid_margin) - per_user_fee * users (floored at 0)
122
- ```
123
- 3. **Smoothing rule.** Actual COGS is tracked monthly per client. The flat fee
124
- is changed **only after** actual margin sits outside the band for a
125
- configurable number of **consecutive** months (**default 3**). The per-user
126
- fee is **never** adjusted.
127
-
128
- ### Workbook structure (TALOS Pricing Calculator v9)
129
-
130
- | Tab | Purpose |
131
- |---|---|
132
- | Sales | Rep-facing inputs + recommended flat fee + min/mid/max margin-band options |
133
- | Voice | Per-minute band pricing |
134
- | Technical | Single source of truth for all raw rates — sales never edits |
135
- | Actuals | Monthly actual-AWS-COGS entry, margin-vs-band status, consecutive-breach flags, suggested mid-band flat fee |
136
- | Engine | Hidden calculation engine |
137
-
138
- The workbook is a **per-client** file on the shared AI drive (`P:\Work\AI\`).
139
+ 1. **Two-part price.** Client pays a **flat monthly org fee** (the adjustable
140
+ margin lever) PLUS a **per-user fee that steps by user-count band**.
141
+ 2. **Locked band schedule.** The per-user **band rate schedule** is fixed at
142
+ **contract signing**. When user count crosses a band, the per-user fee
143
+ auto-steps to the **pre-agreed band rate**; then the **org fee** is adjusted
144
+ to restore margin. The per-user fee **never changes outside the agreed band
145
+ schedule**.
146
+ 3. **Smoothing rule.** Actual margin is tracked monthly per client. The org fee
147
+ change is **only recommended after** margin sits outside the min/max band for
148
+ a configurable number of **consecutive** months (`consecutiveMonths`,
149
+ per-client; default 3).
150
+
151
+ > **Pivot note (2026-06-29):** the earlier model used a *fixed* per-user fee +
152
+ > flat fee solved to the band midpoint. The current model makes the per-user fee
153
+ > a **locked band schedule** (steps with headcount) and uses the **org fee** as
154
+ > the margin lever. The band schedule is stored in `TalosPricingBands`, locked at
155
+ > signing in the tools onboarding UI.
156
+
157
+ ### Delivery (current — database platform)
158
+
159
+ The model now lives across three repos (schema home + topology in the platform
160
+ architecture doc):
161
+
162
+ - **Team DB (9 `Talos*` tables)** — `TalosClients`, `TalosPricingBands`,
163
+ `TalosCostFactors`, `TalosUsageMonthly` + `TalosUsageFeatureMonthly`,
164
+ `TalosAwsActualsMonthly`, `TalosClientUserCounts`, `TalosCalibrationMonthly`,
165
+ `TalosFeeRecommendations`. (`dbchanges2/Team/...TalosPricingTables.sql`)
166
+ - **tools app (1.0)** — onboarding (locks the band schedule), pricing dashboard,
167
+ usage benchmarks, technical-only Cost Factors editor + `App_Talos_Estimator`.
168
+ - **worker2 (2.0)** — monthly crons: import AWS actuals, recompute calibration +
169
+ margins/recommendations, send leadership report.
170
+
171
+ The previous **TALOS Pricing Calculator v9** Excel workbook (Sales / Voice /
172
+ Technical / Actuals / Engine tabs, per-client file under the shared AI drive) is
173
+ **superseded** by this platform but documents the same economics.
139
174
 
140
175
  ## Gotchas / known issues
141
176
 
@@ -150,4 +185,5 @@ The workbook is a **per-client** file on the shared AI drive (`P:\Work\AI\`).
150
185
  under the team shared AI drive; the path can move.
151
186
 
152
187
  ## Change history
188
+ - 2026-06-29 — Pricing **delivery pivoted** from the Excel calculator to a DB platform (Team DB 9 `Talos*` tables + tools 1.0 UI + worker2 crons). Cost methodology recorded as **calibrate Langfuse→AWS** (per-client factor = AWS/Langfuse_corrected; global-blended fallback for new clients; always use cache-corrected Langfuse). Methodology refined: per-user fee is now a **locked band schedule** (steps with headcount) with the **org fee** as the margin lever (was fixed per-user + midpoint-solved flat fee). COGS token economics unchanged. (jcardinal)
153
189
  - 2026-06-29 — Initial pricing/COGS model doc: chat token + feature drivers, voice per-minute drivers, two-part flat+per-user methodology with mid-band solve and consecutive-month smoothing. Captured from TALOS Pricing Calculator v9 (replaces v8). v9 verified to reproduce v8 exactly ($0.811182/conversation, $2,071.18/mo for 50 users × 20 conversations, Document Search only). History ×7 vs ×15-exchanges quirk preserved for parity and flagged for technical review. (jcardinal)
@@ -8,4 +8,4 @@
8
8
  | [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
9
9
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
10
10
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/, toga25-supply/src/hooks/useTableCellInteractions.ts |
11
- | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/fieldsConfig/index.ts |
11
+ | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts |
@@ -22,6 +22,11 @@ files:
22
22
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx
23
23
  - toga25-supply/src/pages/Items/ItemsPage.tsx
24
24
  - toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx
25
+ - toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx
26
+ - toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx
27
+ - toga25-supply/src/pages/Inventory/Inventory.tsx
28
+ - toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
29
+ - toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts
25
30
  - toga25-supply/src/fieldsConfig/index.ts
26
31
  related:
27
32
  - ../../_underscore/features/surface-resolver.md
@@ -18,6 +18,7 @@
18
18
  | [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
19
19
  | [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
20
20
  | [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
21
+ | [Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)](features/talos-pricing-automation.md) | The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model` and tools `talos-pricing-ui` docs for the other halves). | worker2/Worker/Talos/Pricing.php, worker2/Database/TalosPricingCrons.sql |
21
22
  | [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` (file `Worker/Team/Sprint.php`) is the engine behind TOGA's internal **development-sprint process and reporting**. | worker2/Worker/Team/Sprint.php |
22
23
  | [Teams Meeting Transcript Export](features/teams-transcript-export.md) | `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for Teams meeting transcripts produced by a set of organizers, classifies ea | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
23
24
  | [VAPI Webhook Handler (worker2 — AI-BDR end-of-call processing)](features/vapi-webhook-handler.md) | `_Worker_Vapi` ([worker2/Worker/Vapi.php](worker2/Worker/Vapi.php)) is the **PHP side of the AI-BDR call loop** — the webhook that receives VAPI's end-of-call r | worker2/Worker/Vapi.php, worker2/Worker/Ai/Bdr/Vapi.php |
@@ -0,0 +1,84 @@
1
+ ---
2
+ title: Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-29
10
+ owners: [jcardinal]
11
+ files:
12
+ - worker2/Worker/Talos/Pricing.php
13
+ - worker2/Database/TalosPricingCrons.sql
14
+ related:
15
+ - ./creating-worker-actions.md
16
+ - ../architecture.md
17
+ - ../../talos/architecture.md
18
+ - ../../talos/features/pricing-cogs-model.md
19
+ - ../../../1.0/apps/tools/features/talos-pricing-ui.md
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model`
25
+ and tools `talos-pricing-ui` docs for the other halves). `_Worker_Talos_Pricing` is an
26
+ **abstract** worker class with three monthly cron actions that turn raw usage/cost data
27
+ into per-client margin tracking and a leadership report. All state lives in the **Team DB**
28
+ (9 `Talos*` tables — schema home is the platform architecture doc); the worker reads/writes
29
+ it via `_underscore::DB_TEAM`.
30
+
31
+ This replaces the Excel "Actuals" tab as the monthly recompute mechanism. The COGS token
32
+ economics are unchanged — only the delivery/automation changed.
33
+
34
+ ## How it works
35
+
36
+ `initialize()` registers the Team DB connection via `_underscore::DB_TEAM` (same pattern as
37
+ `_Worker_Team_Transcripts`) so every action has it. The three actions are registered as
38
+ `Core.CronJobs` rows (no code wiring — see `creating-worker-actions`), fired on the **4th of
39
+ the month**, sequenced 06:00 / 07:00 / 08:00 Central so each step's inputs are present
40
+ before the next runs.
41
+
42
+ ### 1. ImportAwsActuals (06:00)
43
+ Calls AWS Cost Explorer `GetCostAndUsage` per client, filtered by `LINKED_ACCOUNT` or a
44
+ cost-allocation **tag** (assumed key `Client` — open item, confirm). Upserts one row per
45
+ `(clientIdentifier, periodMonth)` into `TalosAwsActualsMonthly`. The AWS Cost Explorer SDK
46
+ is vendored in `worker2/vendor`.
47
+
48
+ ### 2. RecomputeMargins (07:00)
49
+ The core calibration step. For each client/month:
50
+ - **Calibration factor** = `AWS_actual / Langfuse_costCorrected` (write to
51
+ `TalosCalibrationMonthly`, `DECIMAL(14,8)`). Always use Langfuse's **cache-corrected**
52
+ cost — Langfuse badly undercounts cache cost (sample: $201 recorded vs $1,088 corrected).
53
+ - **New clients** (no AWS history) use a **global blended factor** = `SUM(aws) / SUM(langfuse)`
54
+ across all clients, until they have their own rolling-3-month factor.
55
+ - Apply the factor to Langfuse's granular per-feature/per-user structure so **structure comes
56
+ from Langfuse, absolute dollars from AWS**.
57
+ - Compute per-client margin vs. the client's min/max band, update the consecutive out-of-band
58
+ **streak**, and only emit a `TalosFeeRecommendations` row once the streak ≥ the client's
59
+ configured `consecutiveMonths`.
60
+
61
+ ### 3. MonthlyReport (08:00)
62
+ Builds an `.xlsx` with **PhpSpreadsheet** (vendored at
63
+ `_underscore/Component/Library/PhpOffice` — first use in worker2) and sends a templated
64
+ leadership email via `_Model_Client_EmailTemplate::send(...)` to the `True` client,
65
+ rendering `{var}` placeholders through AWS SES.
66
+
67
+ ## Gotchas / open items
68
+ - **Idempotent upsert key** — every fact table is keyed `(clientIdentifier, periodMonth)`
69
+ (usage-by-feature adds `toolCategory`), with a UNIQUE on those columns, so re-running a
70
+ cron is safe.
71
+ - **`REPORT_EMAIL_TEMPLATE` is a placeholder UUID** — needs a real `EmailTemplates` record in
72
+ `Client_True` before MonthlyReport sends.
73
+ - **`_Email` attachment support unconfirmed** — verify before relying on the xlsx attachment.
74
+ - **AWS cost-tag key assumed `Client`** — confirm against the actual cost-allocation tag.
75
+ - **Importer contract (other dev):** `TalosUsageMonthly` + `TalosUsageFeatureMonthly` are the
76
+ Langfuse → MySQL targets. Map the Langfuse org tag `org:True` → `clientIdentifier` `"True"`;
77
+ upsert on `(clientIdentifier, periodMonth[, toolCategory])`.
78
+
79
+ ## Security
80
+ - Hardcoded AWS SES SMTP credentials live in `_underscore/Email.php` (pre-existing, not
81
+ introduced here) — flagged for rotation. Location only; no values recorded.
82
+
83
+ ## Change history
84
+ - 2026-06-29 — Built `_Worker_Talos_Pricing` (abstract) with 3 monthly crons — ImportAwsActuals (Cost Explorer), RecomputeMargins (calibrate Langfuse→AWS, rolling-3mo + global-blended fallback, margin/streak/recommendation), MonthlyReport (PhpSpreadsheet xlsx + templated SES email to True). Registers Team DB via `_underscore::DB_TEAM`; crons are `Core.CronJobs` rows on the 4th, 06/07/08:00 CT. Replaces the Excel "Actuals" recompute. (jcardinal)
@@ -5,14 +5,14 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 10 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 10 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 11 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **togadesk** (TOGa Desk) — 8 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
10
10
  - **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
11
11
  - **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
12
12
  - **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
13
13
  - **test** (Test) — 12 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
14
14
  - **toga** (TOGa) — 2 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
15
- - **tools** (Tools) — 5 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
15
+ - **tools** (Tools) — 7 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
16
16
 
17
17
  ## 2.0 framework
18
18
 
@@ -2,5 +2,6 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
+ | [NYCDOE Ticket Hold-Status Sync (ServiceNow ⇄ TOGaDesk)](features/hold-status-sync.md) | 1.0 | DOE ticket **hold** status must round-trip between ServiceNow (SNOW) and TOGaDesk and **stay held** — holds are SLA-bearing in both systems. | worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/send_request_item_updates.php, library/app/model/togadesk/repairorder.php, library/app/api/nycdoev2.php |
5
6
  | [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents + RITMs) into local tables, turns vendor shipment notices into NetSuite Sales Orde | worker/crons/sync/nycdoe/import_asn.php, worker/crons/sync/nycdoe/import_inc.php, worker/crons/sync/nycdoe/legacy_import_asn.php, worker/crons/sync/nycdoe/legacy_process_asn_queue.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php, worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php, worker/crons/sync/nycdoe/3_create_installation_ticket.php, worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/send_request_item_updates.php, worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php, worker/crons/sync/nycdoe/sync_nycdoe_locations.php, worker/crons/sync/nycdoe/receive_edi_purchase_orders.php, worker/crons/sync/nycdoe/send_edi_open_invoices.php, worker/crons/notifications/nycdoe/, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.notification.json, library/app/api/nycdoe.php, library/app/api/nycdoev2.php, library/app/asnprocessor/manufacturer.php, library/app/asnprocessor/apple.php, library/app/asnprocessor/lenovo.php, library/app/asnprocessor/lexmark.php, library/app/asnprocessor/acer.php, library/app/edi.php |
6
7
  | [New York City Department of Education](profile.md) | 1.0 | NYC DOE (New York City Department of Education) is a TOGA client whose entire integration runs in the **1.0 worker tier** (~30 cron scripts under `worker/crons/ | |
@@ -0,0 +1,127 @@
1
+ ---
2
+ title: NYCDOE Ticket Hold-Status Sync (ServiceNow ⇄ TOGaDesk)
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: nycdoe
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-06-29
10
+ owners: [mhammontree]
11
+ files:
12
+ - worker/crons/sync/nycdoe/send_ticket_updates.php
13
+ - worker/crons/sync/nycdoe/process_tickets.php
14
+ - worker/crons/sync/nycdoe/send_request_item_updates.php
15
+ - library/app/model/togadesk/repairorder.php
16
+ - library/app/api/nycdoev2.php
17
+ related:
18
+ - servicenow-integration.md
19
+ - ../profile.md
20
+ - ../../../1.0/apps/worker/architecture.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ DOE ticket **hold** status must round-trip between ServiceNow (SNOW) and TOGaDesk and
26
+ **stay held** — holds are SLA-bearing in both systems. This doc covers the bidirectional
27
+ hold-status sync for DOE Incidents (INC) and Request Items (RITM), the authoritative
28
+ business rule that governs it, and the TRUE-79922 fix that made hold persist. It is the
29
+ status-sync companion to the broader [ServiceNow / ASN integration](servicenow-integration.md)
30
+ doc; the underlying crons and `App_Api_NYCDOEV2` plumbing are documented there.
31
+
32
+ > DOE "tickets" are **`repair_orders`** rows (`App_Model_TogaDesk_RepairOrder`,
33
+ > `db_togadesk` / legacy `TOGaDeskSupport`, **clientid = 16**) — NOT the generic `tickets`
34
+ > table / `class.ticket.php`. All four DOE crons filter to client 16 / `NYCDOETickets`, so
35
+ > hold behavior here is **NYCDOE-only**; no other client is affected.
36
+
37
+ ## The authoritative business rule (SME-confirmed — contact-center)
38
+
39
+ 1. **A DOE hold persists until MANUALLY set to a new status.** Nothing auto-reverts a hold.
40
+ 2. **TOGaDesk is the AUTHORITATIVE system for releasing a hold.** A manual change *in
41
+ ServiceNow* does NOT release a TOGaDesk hold — the outbound cron **re-asserts On Hold**
42
+ onto SNOW every run until the hold is cleared in TOGaDesk.
43
+ 3. Scheduling metadata (ETA / assigned technician) is **separate from the hold STATE** — an
44
+ on-site visit can be scheduled weeks out (e.g. summer break) **without releasing the
45
+ hold**. ETA syncs *inside* the hold payload.
46
+
47
+ ## How it works
48
+
49
+ ### Status model (TOGaDesk side)
50
+ - There are **six `HOLD_*` status constants** on `App_Model_TogaDesk_RepairOrder`. All bucket
51
+ to main status **`Hold`** via `App_Model_TogaDesk_RepairOrder::statusDisplayMain()`.
52
+ - `Hold` has **statusRank 6**, beating `Work in Progress` (rank 5) in the downgrade-guard
53
+ added by TRUE-76812 — so a downstream sync cannot silently demote a held order.
54
+
55
+ ### Status field (ServiceNow side)
56
+ - The SNOW **native `state` field** (human-readable: `On Hold`, `In Progress`, `Assigned`,
57
+ `Resolved`, …) is the **SLA-bearing, authoritative status**. Read/write this for hold.
58
+ - The custom **`u_status_task`** field is a *separate concept* with a different vocabulary;
59
+ it can lag/disagree with `state` (observed: native `state="In Progress"` while
60
+ `u_status_task="Open"`). **Never use `u_status_task` for hold detection.**
61
+ - Numeric `state` codes seen on the wire: INC `On Hold` = **3**, `In Progress` = **2**;
62
+ RITM `On Hold` = **8**, the scheduled-update state = **"-14"**.
63
+
64
+ ### Outbound (TOGaDesk → ServiceNow)
65
+ - `send_ticket_updates.php` (INC) pushes the hold as `state:3`. The "Assigned → In Progress"
66
+ auto-start block is **guarded** so it never pushes `state:2` when the local repair order is
67
+ in any `HOLD_*` status (this is what caused the self-clobber — see Change history).
68
+ - `send_request_item_updates.php` (RITM) pushes `state:"8"` (On Hold) and carries a
69
+ defensive `HOLD_*` guard on the scheduled-update path so it can't push `state:"-14"` while
70
+ held.
71
+
72
+ ### Inbound (ServiceNow → TOGaDesk)
73
+ - `process_tickets.php` determines local status from the native **`$data->state`** field
74
+ (NOT `u_status_task`): after the `u_status_task` switch — before the completion-preserve
75
+ step and the `statusRank` downgrade-guard — if native `state` is `On Hold`
76
+ (case-insensitive) it sets `$newStatus = STATUS_HOLD`. Both inbound "Assigned-force"
77
+ `patchINC(state:2)` blocks are guarded against a local hold.
78
+
79
+ ### RITM "ETA-in-hold"
80
+ - `send_request_item_updates.php` syncs `scheduled_date` (ETA) to ServiceNow **inside the
81
+ hold block, in the same payload as `state:"8"`**, when a held order has an ETA + assigned
82
+ technician. This mirrors the INC cron, which already sends `u_eta` while holding — keeping
83
+ scheduling metadata flowing while the hold STATE stays asserted.
84
+
85
+ ## Reference facts (DOE sync debugging)
86
+
87
+ - **SNOW request/response logs:** logged to the legacy `Logs` schema, **`API` table**
88
+ (`App_Model_Logs_API`, `db_logs`, `_databaseNameOverride 'Logs'`). `requestPayload` /
89
+ `responsePayload` are queryable CHAR columns — the primary debugging avenue. Filter
90
+ `endpoint LIKE '%nycd3%'`; the custom DOE endpoint base is **`/api/nycd3/`**. The
91
+ state:3↔state:2 oscillation in TRUE-79922 was proven from this table (INC2074899,
92
+ INC2185183 showed `state:3` then `state:2` pushed seconds apart in one run).
93
+ - **`NYCDOETickets` (`App_Model_Common_NYCDOETicket`, `db_common` / legacy `Common`)** stores
94
+ the inbound SNOW record in `rawData`, a **`FIELDTYPE_BLOBSTORAGE`** field (S3 bucket
95
+ `asifiles`, key `prod/Common/NYCDOETickets/rawData/{id}`) — **NOT a queryable DB column.**
96
+ - **Worker prod code cannot run on a Windows dev box.** The DB/S3 data layer fetches EC2
97
+ instance-metadata (IMDSv2) credentials and fails off-EC2. Investigate with the toga DB MCP
98
+ and the `Logs.API` table instead of running the crons locally.
99
+
100
+ ## Gotchas / known issues
101
+
102
+ - **The self-clobber (root cause #1 of TRUE-79922):** an unguarded "Assigned → In Progress"
103
+ auto-start fought the hold push — `state:3` then `state:2` pushed seconds apart, every run,
104
+ so SNOW never stayed On Hold. Any future auto-start/auto-status block on these crons MUST
105
+ short-circuit when the local RO is in a `HOLD_*` status.
106
+ - **The wrong-field read (root cause #2 of TRUE-79922):** reading `u_status_task` instead of
107
+ native `state` meant a hold set on the SNOW side never reached TOGaDesk. Hold detection is
108
+ on native `state` only.
109
+ - Holds are **never auto-released** — if a SNOW user manually changes status, the outbound
110
+ cron correctly re-asserts On Hold; that is intended, not a bug. Clear the hold in TOGaDesk.
111
+
112
+ ## Change history
113
+ - 2026-06-29 — TRUE-79922: fixed DOE holds never persisting (reverted to In Progress, broke
114
+ SLA reporting both sides). Two coupled defects: outbound self-clobber (`send_ticket_updates.php`
115
+ unguarded Assigned→In-Progress pushing `state:2` over a held `state:3`) and inbound
116
+ wrong-field read (`process_tickets.php` read `u_status_task` instead of native `state`).
117
+ Guarded all Assigned-force blocks (INC outbound + both inbound) against local `HOLD_*`,
118
+ added native-`state`="On Hold"→`STATUS_HOLD` inbound mapping, and a RITM scheduled-update
119
+ `HOLD_*` guard. Built RITM ETA-in-hold sync (`scheduled_date` in the same `state:"8"`
120
+ payload). Recorded the SME business rule (hold persists until manual; TOGaDesk authoritative
121
+ for release). (mhammontree)
122
+
123
+ ## Related docs
124
+ - [NYCDOE ServiceNow / ASN Integration](servicenow-integration.md) — full integration map,
125
+ crons, schedules, and `App_Api_NYCDOEV2` plumbing.
126
+ - [NYC DOE client profile](../profile.md)
127
+ - [Worker (1.0) architecture](../../../1.0/apps/worker/architecture.md)
@@ -36,6 +36,7 @@ files:
36
36
  - library/app/edi.php
37
37
  related:
38
38
  - ../profile.md
39
+ - hold-status-sync.md
39
40
  - ../../../1.0/apps/worker/architecture.md
40
41
  ---
41
42
 
@@ -230,6 +231,11 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
230
231
  of the consumer query; `php -l` every touched file.
231
232
 
232
233
  ## Change history
234
+ - 2026-06-29 — Outbound status sync now keeps DOE holds authoritative (TRUE-79922). Hold
235
+ detection moved to the SNOW native `state` field (not `u_status_task`); Assigned-force
236
+ blocks guarded against local `HOLD_*` to stop the `state:3`↔`state:2` self-clobber; RITM
237
+ now syncs ETA inside the On-Hold payload. Split into dedicated
238
+ [hold-status-sync.md](hold-status-sync.md). (mhammontree)
233
239
  - 2026-06-19 — Investigated (read-only) the replacement-PO-suffix duplicate-fulfillment bug: PO-scoped serial dedupe defeated by per-manufacturer suffix-format variance (`-RPL`/`-REPL`/`-RPLE`/`-N`/`_N`); confirmed live case WR260150152/V60095WK (ROs 772308+801064) and ~14 systemic pairs across Lenovo+Lexmark. Refined business rule #6; added OPEN-BUG gotcha + adjacent anomalies (numeric-PO↔WR cluster, "NA"/integer serials). No code changes. (mhammontree)
234
240
  - 2026-06-10 — Documented the NYC DOE ServiceNow/ASN integration (ticket mirror, dual ASN ingestion, NetSuite freeze pipeline, EDI surface, dedupeKey gotchas). (mhammontree)
235
241
 
@@ -4,11 +4,13 @@ framework: "2.0"
4
4
  apps:
5
5
  - _underscore
6
6
  - tools
7
+ - worker2
8
+ - dbchanges2
7
9
  project: _Underscore
8
10
  client: true
9
11
  type: profile
10
12
  status: active
11
- updated: 2026-06-26
13
+ updated: 2026-06-29
12
14
  owners: [jcardinal]
13
15
  files: []
14
16
  related:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.227",
3
+ "version": "1.0.229",
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",