toga-ai 1.0.776 → 1.0.778

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.
Files changed (29) hide show
  1. package/knowledge/1.0/apps/test/INDEX.md +1 -0
  2. package/knowledge/1.0/apps/test/architecture.md +5 -0
  3. package/knowledge/1.0/apps/test/features/github-audit-script.md +60 -0
  4. package/knowledge/1.0/apps/tools/INDEX.md +3 -2
  5. package/knowledge/1.0/apps/tools/architecture.md +14 -4
  6. package/knowledge/1.0/apps/tools/features/compass-user-persona-admin.md +157 -26
  7. package/knowledge/1.0/apps/tools/features/github-audit.md +185 -0
  8. package/knowledge/1.0/apps/tools/features/mvc-data-access-patterns.md +63 -4
  9. package/knowledge/1.0/standards/backend-php.md +11 -1
  10. package/knowledge/1.0/standards/frontend.md +24 -0
  11. package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
  12. package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +58 -2
  13. package/knowledge/2.0/apps/_underscore/features/apirequest-json-content-type.md +29 -1
  14. package/knowledge/2.0/apps/_underscore/features/record-change-audit-log.md +28 -6
  15. package/knowledge/2.0/apps/_underscore/features/user-email-as-identity-and-password-reset.md +103 -0
  16. package/knowledge/2.0/apps/api2/features/login-cross-client-user-resolution.md +15 -2
  17. package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +39 -2
  18. package/knowledge/INDEX.md +4 -4
  19. package/knowledge/clients/compass-canada/profile.md +9 -0
  20. package/knowledge/clients/compass-usa/INDEX.md +1 -1
  21. package/knowledge/clients/compass-usa/features/cost-centers.md +44 -2
  22. package/knowledge/clients/compass-usa/features/people-file-user-lifecycle.md +41 -1
  23. package/knowledge/clients/nycdoe/INDEX.md +1 -0
  24. package/knowledge/clients/nycdoe/features/edi-850-receive-and-997-ack.md +108 -0
  25. package/knowledge/clients/nycdoe/features/servicenow-integration.md +5 -2
  26. package/knowledge/clients/nycdoe/profile.md +2 -1
  27. package/knowledge/clients/rate/features/aig-contract-creation.md +130 -2
  28. package/knowledge/clients/rate/features/subscription-cancellation.md +52 -2
  29. package/package.json +1 -1
@@ -9,6 +9,7 @@
9
9
  | [Create Elastic Beanstalk Environment (script)](features/create-elastic-beanstalk.md) | `team/aws/create_elastic_beanstalk.php` is a **standalone** (no `App_` framework) constants-driven PHP generator. | test/team/aws/create_elastic_beanstalk.php |
10
10
  | [Developer Generators (password, UUID)](features/dev-generators.md) | Two tiny **1.0 `App_` framework** convenience scripts for everyday developer needs. | test/team/generate_password.php, test/team/uuid.php |
11
11
  | [Forecast vs NetSuite Discrepancy Analysis](features/forecast-netsuite-discrepancy-analysis.md) | `team/forecast-netsuite/discrepancy_analysis.php` detects discrepancies between our **Forecast database** and **NetSuite** (the source of truth for all sales da | test/team/forecast-netsuite/discrepancy_analysis.php |
12
+ | [GitHub Audit Script (team/github_audit.php)](features/github-audit-script.md) | `team/github_audit.php` is a **standalone** (no `App_` framework) browser/CLI script that reports lines added/removed, commits, unique authors, and active repos | test/team/github_audit.php |
12
13
  | [@goagilant.com → @togatech.com Email-Domain Migration (1.0 + 2.0)](features/goagilant-to-togatech-email-migration.md) | Reference + technique for migrating the company email domain `@goagilant.com` → `@togatech.com` across **both** platforms. | migrate_goagilant_to_togatech_2026-06-26.sql, migrate_goagilant_to_togatech_LEGACY_2026-06-26.sql |
13
14
  | [Static (no-DB) Regression Harness for 1.0 Logic + Source Drift Guard](features/static-no-db-regression-harness.md) | 1.0 has **no PHPUnit**, and most of its business logic sits inside methods that also write SQL, so "just call it" means standing up a client database. | test/@Mark/AIG/test_multi_email.php, test/@Mark/TRUE-80952/test_queued_email_attachments.php, library/app/database.php, library/app/api/toga2.php |
14
15
  | [TableView Builder (2.0 TableViews SQL generator)](features/tableview-builder.md) | `team/tableViewBuilder/` generates SQL `INSERT` statements for the **2.0 `TableViews`**, `TableViewFields`, and `TableViewJoins` tables from a plain SQL `SELECT | test/team/tableViewBuilder/TableViewGenerator.php, test/team/tableViewBuilder/index.php, test/team/tableViewBuilder/Instructions.md |
@@ -22,6 +22,7 @@ related:
22
22
  - ./features/dev-generators.md
23
23
  - ./features/talos-kb-pipeline.md
24
24
  - ./features/goagilant-to-togatech-email-migration.md
25
+ - ./features/github-audit-script.md
25
26
  ---
26
27
 
27
28
  ## Summary
@@ -85,6 +86,7 @@ manually against the target database; they do not execute changes themselves.
85
86
  | `generate_password.php`, `uuid.php` | 1.0 | dev utility | [dev-generators](./features/dev-generators.md) |
86
87
  | `talos/kb_uploader.php`, `talos/kb_processor.php` | standalone | S3 + AWS Bedrock KBs | [talos-kb-pipeline](./features/talos-kb-pipeline.md) |
87
88
  | `migrate_goagilant_to_togatech_*.sql` | standalone | 2.0 `Client_` DBs + 1.0 schemas | [goagilant-to-togatech-email-migration](./features/goagilant-to-togatech-email-migration.md) |
89
+ | `github_audit.php` | standalone | GitHub org (agilantsolutions) | [github-audit-script](./features/github-audit-script.md) |
88
90
 
89
91
  **Retired / not documented:** `team/DOA_gitbook/` is being retired.
90
92
 
@@ -95,3 +97,6 @@ manually against the target database; they do not execute changes themselves.
95
97
  processor in [talos-kb-pipeline](./features/talos-kb-pipeline.md).
96
98
  - 2026-06-26 — Documented the @goagilant.com → @togatech.com email-domain migration SQL
97
99
  artifacts in [goagilant-to-togatech-email-migration](./features/goagilant-to-togatech-email-migration.md).
100
+ - 2026-09-04 — Documented `team/github_audit.php`, the standalone monthly GitHub org
101
+ code-churn audit (later ported into Tools) in
102
+ [github-audit-script](./features/github-audit-script.md).
@@ -0,0 +1,60 @@
1
+ ---
2
+ title: GitHub Audit Script (team/github_audit.php)
3
+ framework: "1.0"
4
+ repo: test
5
+ project: Test
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-04
10
+ owners: [jcardinal]
11
+ files:
12
+ - test/team/github_audit.php
13
+ related:
14
+ - ../architecture.md
15
+ - ../../tools/features/github-audit.md
16
+ ---
17
+
18
+ ## Summary
19
+
20
+ `team/github_audit.php` is a **standalone** (no `App_` framework) browser/CLI script that
21
+ reports lines added/removed, commits, unique authors, and active repos across the
22
+ `agilantsolutions` GitHub org for one calendar month. Quick, run-it-locally monthly audit.
23
+
24
+ This is the **original build** that was then ported into the Tools app as
25
+ [`/developers/github-audit`](../../tools/features/github-audit.md). The app version is the
26
+ maintained one; this script is the lightweight local runner.
27
+
28
+ ## How it works
29
+
30
+ 1. **Config** via `const` at the top of the file: `AUDIT_MONTH` (`YYYY-MM`) and
31
+ `GITHUB_ORG`.
32
+ 2. **Token** is read from the `GITHUB_AUDIT_TOKEN` **environment variable** — never
33
+ hardcoded, so it can't be committed.
34
+ 3. Same **per-commit exact counting** as the app tool: list each commit in the month window,
35
+ fetch each commit's stats, skip merge commits and forks.
36
+ 4. Same **reliability**: `202` "stats not ready" retry, secondary rate-limit retry, and a
37
+ primary rate-limit floor that stops the run.
38
+ 5. Same **month-range validation** (`^\d{4}-\d{2}$` plus month number 01–12, so `2026-99`
39
+ is rejected rather than silently overflowed by `DateTime`).
40
+
41
+ Fits the `test` repo's documented **`team/` scripts** surface (see architecture).
42
+
43
+ ## Gotchas
44
+
45
+ - **Token must be in the environment.** With `GITHUB_AUDIT_TOKEN` unset the run fails — set
46
+ it before running; do not paste a PAT into the file.
47
+ - **Shared org rate limit.** A big audit consumes the org's GitHub budget; avoid running it
48
+ in parallel with the Tools GitHub Audit or the Design tool (same org token pool).
49
+
50
+ ## Change history
51
+
52
+ - 2026-09-04 — Created. Standalone monthly org code-churn audit script; token from
53
+ `GITHUB_AUDIT_TOKEN`. Later ported into the Tools app as `/developers/github-audit`.
54
+ (jcardinal)
55
+
56
+ ## Related docs
57
+
58
+ - [Test Architecture](../architecture.md)
59
+ - [Tools GitHub Audit (app port)](../../tools/features/github-audit.md)
60
+ </content>
@@ -6,12 +6,13 @@
6
6
  | [/clickup/aliases — ClickUp Label-Alias Admin (Tools → 2.0 Team schema)](features/clickup-label-aliases-admin.md) | The human side of the NetSuite→ClickUp **Stakeholders / End Customer** labels. | tools/mvc/clickup/aliases/get.php, tools/mvc/clickup/aliases/post.php, tools/_/app/clickup/aliases.php, tools/_/app/nav.php |
7
7
  | [Developer Dashboard (ClickUp Sprint, tools /developer)](features/clickup-sprint-dashboard.md) | A native **ClickUp sprint dashboard** in the 1.0 `tools` app at route `/developer` (the **"Developer Dashboard"**, renamed from `/clickup/react` / "Clickup" — s | tools/_/app/clickup/sprint.php, tools/v2/sprints/tile/index.php, tools/v2/sprints/current/index.php, tools/v2/sprints/status-breakdown/index.php, tools/v2/sprints/worktype-breakdown/index.php, tools/v2/sprints/points-by-dev/index.php, tools/v2/sprints/burndown/index.php, tools/assets/clickup/sprint-dashboard.html, tools/mvc/developer/get.php, tools/mvc/login/get.php, tools/_/app/nav.php |
8
8
  | [CloudFront Client Setup](features/cloudfront-client-setup.md) | An SSO-gated admin tool at **`/devops/cloudfront-clients`** in the Tools 1.0 app that onboards a client onto **CloudFront + Route 53 across multiple AWS account | tools/_/app/devops/cloudfront.php, tools/mvc/devops/cloudfront-clients/get.php, tools/mvc/devops/cloudfront-clients/post.php, tools/assets/js/cloudfront-clients.js, tools/assets/css/cloudfront-clients.css, tools/_/app/nav.php, tools/_/app/frameworkindex.php, tools/config.production.ini |
9
- | [Compass User & Persona Admin (tools /compass/users, /compass/personas)](features/compass-user-persona-admin.md) | Two pages in the Tools `developers` nav folder that let staff do, in a browser, the two Compass requests that used to mean "email a developer, who writes a `dbc | tools/_/app/compass.php, tools/mvc/compass/users/get.php, tools/mvc/compass/users/post.php, tools/mvc/compass/personas/get.php, tools/mvc/compass/personas/post.php, tools/_/app/nav.php, tools/config.production.ini |
9
+ | [Compass User & Persona Admin (tools /compass/users, /compass/personas)](features/compass-user-persona-admin.md) | Two pages in the Tools `developers` nav folder that let staff do, in a browser, the Compass requests that used to mean "email a developer, who writes a `dbchang | tools/_/app/compass.php, tools/mvc/compass/users/get.php, tools/mvc/compass/users/post.php, tools/mvc/compass/personas/get.php, tools/mvc/compass/personas/post.php, tools/_/app/nav.php, tools/config.production.ini |
10
10
  | [Design Demo Admin](features/design-demo-admin.md) | A self-serve admin UI at **`/design`** in the SSO-protected **Tools** app that lets the design team publish self-contained "Claude Design" HTML exports as **ver | tools/_/app/design/github.php, tools/mvc/design/get.php, tools/mvc/design/post.php, tools/assets/css/design.css, tools/assets/js/design.js, tools/_/app/frameworkindex.php, tools/_/app/nav.php, tools/composer.json |
11
11
  | [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, tools/mvc/developers/password/post.php |
12
12
  | [/errors Curation Console (Tools → shared Core Logs DB)](features/errors-curation-console.md) | Internal-only triage/curation screen for the 2.0 Issue/Event error-reporting pipeline, built as a 1.0 Tools MVC page reading the **shared Core Logs DB** through | tools/mvc/errors/get.php, tools/mvc/errors/post.php, tools/mvc/errors/issue/get.php, tools/assets/css/style.css, tools/_/app/nav.php, tools/config.production.ini |
13
+ | [GitHub Audit (monthly code-churn report)](features/github-audit.md) | An internal report at **`/developers/github-audit`** (Developers nav group, personas `['Development Team','TOGa Technology']`) that shows, for one calendar mont | tools/_/app/github/audit.php, tools/mvc/developers/github-audit/get.php, tools/mvc/developers/github-audit/post.php, tools/_/app/nav.php |
13
14
  | [Legacy Email Notifier (tools /email-migration/notify)](features/legacy-email-notifier.md) | An SSO-gated admin tool at **`/email-migration/notify`** (nav group **Email Migration** > **Legacy Notifier**, personas `['TOGa Technology','Development Team']` | tools/mvc/email-migration/notify/get.php, tools/mvc/email-migration/notify/post.php, tools/_/app/nav.php |
14
- | [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, library/app/error.php, library/app/database.php |
15
+ | [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, library/app/error.php, library/app/database.php, library/app/string.php |
15
16
  | [OneUptime Monitor Status Panel & Outage Alerting (tools /developer)](features/oneuptime-monitor-status-panel.md) | A live **operational monitor column** on the wall-display dashboard at `/developer` (the route was renamed from `/clickup/react` — the internal asset path `asse | tools/assets/clickup/sprint-dashboard.html, tools/_/app/clickup/monitors.php, tools/v2/monitors/status/index.php, tools/mvc/developer/get.php |
16
17
  | [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 |
17
18
  | [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, tools/config.local.ini |
@@ -32,6 +32,7 @@ related:
32
32
  - ./features/persona-gated-navigation.md
33
33
  - ./features/developer-tools.md
34
34
  - ./features/oneuptime-monitor-status-panel.md
35
+ - ./features/github-audit.md
35
36
  - ../library/architecture.md
36
37
  - ../library/features/mvc-page-pattern-and-app-skeleton.md
37
38
  ---
@@ -42,10 +43,12 @@ related:
42
43
  simple interfaces, gated by Client_True staff persona. It is modeled on `togaview` and depends on
43
44
  the `library` core. Auth comes via the SAML gateway `?saml=` handoff; the app owns its own
44
45
  session and reads `Client_True` **read-only** — but it is **no longer a read-only app**: as of
45
- PR tools#13 (unmerged) it also **WRITES** to client tenant databases (`Client_Compass` /
46
- `Client_CompassCanada`) through new `[database_compass]` / `[database_compasscanada]` ini
47
- sections that reuse the `[database_true]` account. That is the first non-read-only surface in
48
- Tools. Adding a tool = create `mvc/<route>/get.php`
46
+ PR tools#13 (**merged to `_production` 2026-09-04**) it **WRITES** to client tenant databases
47
+ (`Client_Compass` / `Client_CompassCanada`) through the `[database_compass]` /
48
+ `[database_compasscanada]` ini sections, and PR tools#14 (**open**) adds writes to `Users`
49
+ identity fields (email, phone, cost center) plus staff-actor audit rows in `Logs_Compass` /
50
+ `Logs_CompassCanada` via two more ini sections — all four reusing the `[database_true]` account.
51
+ That is the first non-read-only surface in Tools. Adding a tool = create `mvc/<route>/get.php`
49
52
  (copy `mvc/_TEMPLATE`) + add one `App_Nav::definition()` entry + `App_Auth::requireAuth([...])`.
50
53
 
51
54
  **Critical rules:** Do NOT use bare `<header>` elements in this app — they inherit the legacy
@@ -68,6 +71,13 @@ persona list (`['Development Team']`) — never `App_Nav::personasForRoute()`**,
68
71
  UNION of the folder's personas and the action's personas and so widens the grant straight back to
69
72
  everyone who can see the nav group. Also: **persona checks read a 4-day session snapshot**, so
70
73
  removing someone's persona does **not** revoke their access — their session must be killed.
74
+ **`App_Database` transactions are GLOBAL** — `beginTransaction()` / `commitTransaction()` /
75
+ `rollbackTransaction()` take no arguments and act on every registered connection, so an audit/log
76
+ row on another connection must be written **before** `beginTransaction()` or it is rolled back with
77
+ the work, and a bare `rollbackTransaction()` leaves autocommit off for the rest of the request
78
+ (silently dropping every later write). **A staff-editable `Users.email` is a login/reset identity**
79
+ — never ship one without a `Core.ClientEmailDomains` domain check and a cross-row duplicate
80
+ refusal, because no Compass tenant has a unique key on `email`.
71
81
 
72
82
  ## Boot & structure
73
83
 
@@ -5,7 +5,7 @@ repo: tools
5
5
  project: Tools
6
6
  client: shared
7
7
  type: feature
8
- status: draft
8
+ status: active
9
9
  updated: 2026-09-04
10
10
  owners: [ajean]
11
11
  files:
@@ -22,31 +22,40 @@ related:
22
22
  - ./saml-sso-auth.md
23
23
  - ../architecture.md
24
24
  - ../../../2.0/apps/_underscore/features/effective-persona-resolution.md
25
+ - ../../../2.0/apps/_underscore/features/record-change-audit-log.md
26
+ - ../../../2.0/apps/_underscore/features/user-email-as-identity-and-password-reset.md
25
27
  - ../../../clients/compass-usa/features/people-file-user-lifecycle.md
26
28
  - ../../../clients/compass-usa/features/persona-model-and-levy-gating.md
29
+ - ../../../clients/compass-usa/features/cost-centers.md
27
30
  - ../../../clients/compass-usa/workflows/granting-persona-bundle-access.md
28
31
  ---
29
32
 
30
33
  ## Summary
31
34
 
32
- Two pages in the Tools `developers` nav folder that let staff do, in a browser, the two Compass
35
+ Two pages in the Tools `developers` nav folder that let staff do, in a browser, the Compass
33
36
  requests that used to mean "email a developer, who writes a `dbchanges2` SQL file": **activate /
34
- deactivate a Compass user**, and **grant a persona**. Both tenants are covered by one tenant
35
- switcher — **US** (`Client_Compass`) and **Canada** (`Client_CompassCanada`).
37
+ deactivate a Compass user**, **grant a persona**, and (as of 2026-09-04) **fix a user's email,
38
+ phone number and cost center**. Both tenants are covered by one tenant switcher — **US**
39
+ (`Client_Compass`) and **Canada** (`Client_CompassCanada`).
36
40
 
37
41
  **This is the first thing in `tools` that WRITES to a client tenant database.** Until now the app
38
42
  read `Client_True` read-only. That single fact drives most of the design and every open risk below.
39
43
 
40
- Status: built on PR **tools#13**, **not yet merged**. `status: draft` until it ships.
44
+ Status: the two pages shipped on PR **tools#13**, now **merged to `tools/_production`**. The
45
+ field-editing work (email / phone / cost center) is PR **tools#14**, branch
46
+ `feature/compass-user-field-edits`, base `_production` — **open, not merged, not deployed.**
41
47
 
42
48
  ## Key files / entry points
43
49
 
44
50
  - **`tools/_/app/compass.php`** — `App_Compass`, all data access for both pages (search, persona
45
- resolution, activate/deactivate, grant).
46
- - **`tools/mvc/compass/users/{get,post}.php`** — user search + activate/deactivate + grant persona.
51
+ resolution, activate/deactivate, grant, and the three field setters).
52
+ - **`tools/mvc/compass/users/{get,post}.php`** — user search + activate/deactivate + grant persona
53
+ + edit email / phone / cost center.
47
54
  - **`tools/mvc/compass/personas/{get,post}.php`** — inspect one persona and who reaches it.
48
55
  - **`tools/_/app/nav.php`** — the two nav entries, inside the existing `developers` folder.
49
- - **`tools/config.production.ini`** — new `[database_compass]` / `[database_compasscanada]` sections.
56
+ - **`tools/config.production.ini`** — `[database_compass]` / `[database_compasscanada]`, plus the
57
+ two logs sections `[database_compasslogs]` (`Logs_Compass`) and `[database_compasscanadalogs]`
58
+ (`Logs_CompassCanada`).
50
59
 
51
60
  ## How it works
52
61
 
@@ -57,6 +66,7 @@ Status: built on PR **tools#13**, **not yet merged**. `status: draft` until it s
57
66
  3. **See the personas that user reaches, and WHY** — direct row, own location, or sector.
58
67
  4. **Inspect a persona** and list who reaches it, from all three sources.
59
68
  5. **Grant a persona** — one `INSERT` into `Users_Personas`.
69
+ 6. **Edit email, phone number and cost center** (2026-09-04, PR tools#14).
60
70
 
61
71
  The "why" column is the point of the tool. Answering "who has persona N" from `Users_Personas`
62
72
  alone badly under-reports on Compass, because most users hold their catalogue through their
@@ -65,6 +75,40 @@ framework's own — see
65
75
  [Effective persona resolution](../../../2.0/apps/_underscore/features/effective-persona-resolution.md),
66
76
  which also carries the measured prod numbers and the "reverse lookups must be set-based" rule.
67
77
 
78
+ ### The three field edits (PR tools#14)
79
+
80
+ `App_Compass` gained `setEmail()`, `setPhone()`, `setCostCenter()`, the cost-center helpers
81
+ `searchCostCenters()`, `costCenterLocation()`, `personasForLocation()`, `collapsePersonaRows()`,
82
+ the audit writer `logChange()`, and the validators `normaliseEmail()`, `normalisePhone()`,
83
+ `emailDomainIsRegistered()`, `usersWithEmail()`, `isCrossRegionProtectedEmail()`.
84
+
85
+ **No schema change.** Each edit writes the `Users` row **and** the matching primary `Contacts`
86
+ child row inside **one transaction** — the same mirror the nightly importer maintains (see the
87
+ Contacts-mirror gotcha in
88
+ [PEOPLE-file User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md)).
89
+
90
+ **A staff-editable `Users.email` is a security-sensitive control, not a cosmetic field** — it is
91
+ the key the 2.0 forgot-password flow and login resolve on. Read
92
+ [User email as identity & the password-reset chain](../../../2.0/apps/_underscore/features/user-email-as-identity-and-password-reset.md)
93
+ before touching this code. These are the rails the email edit shipped with, and they are the
94
+ conditions of the decision below — **do not remove one without re-opening it**:
95
+
96
+ 1. The address must be **typed twice** and match.
97
+ 2. Its **domain must exist in `Core.ClientEmailDomains`** (`emailDomainIsRegistered()`).
98
+ 3. The edit is **refused if any other `Users` row in that tenant already holds the address**
99
+ (`usersWithEmail()`) — the database will not stop you, see the gotcha below.
100
+ 4. The edit is **refused until the operator confirms** when the target row has duplicate twins
101
+ (the same human on two `Users` rows).
102
+ 5. The four `ensureCrossRegionUsersActive()` addresses are **blocked as both source and target**
103
+ (`isCrossRegionProtectedEmail()`), because that hardcoded worker2 list matches on email.
104
+ 6. The **audit row is written before the write**, on its own connection (see the transaction
105
+ gotcha below).
106
+
107
+ Cost center is a **picker over distinct codes**, never a type-ahead (the table is ~910k active Unit
108
+ rows — see [Cost Centers](../../../clients/compass-usa/features/cost-centers.md)), and it forces a
109
+ **before/after persona diff** to be shown and confirmed, because a location change moves the user's
110
+ sector and therefore their catalogue.
111
+
68
112
  ### Access gating is deliberately tighter than the folder
69
113
 
70
114
  Both pages `requireAuth(['Development Team'])` — narrower than the `developers` folder's own
@@ -72,16 +116,35 @@ persona set. That is on purpose: these are tenant **writes**, not read-only deve
72
116
  Passing `App_Nav::personasForRoute()` here would widen the grant back to the folder's set; see the
73
117
  nav-narrowing gotcha in [MVC data access patterns](./mvc-data-access-patterns.md).
74
118
 
119
+ ### Audit trail — the tenant Logs DB, no schema change
120
+
121
+ The "no staff-actor audit trail exists" open item is **resolved**. A staff-actor row **can** be
122
+ written to `Logs_<Tenant>.Record` with no schema change, because `Record.userId` is **nullable**:
123
+
124
+ - `recordId` = **2** (`Core.Records` id for `Users`), `primaryKeyId` = the target `Users.id`,
125
+ `clientId` = the tenant's `Core.Clients` id, `userId` = **NULL**, staff identity in **`note`**.
126
+ - This is the shape `_Model_Compass_ApprovalDecision`
127
+ (`_underscore/Model/Compass/ApprovalDecision.php:1344-1366`) already hand-writes, so it is an
128
+ established pattern, not a new invention.
129
+ - **`Record.note` is `varchar(255)` = 255 CHARACTERS.** Truncate with **`mb_substr`**, never
130
+ `substr` — byte truncation of a 262-character note produced 155 bytes of invalid UTF-8 in a live
131
+ test.
132
+
133
+ That is why `[database_compasslogs]` and `[database_compasscanadalogs]` exist. Details and the
134
+ cross-cluster rules: [Record Change Audit
135
+ Log](../../../2.0/apps/_underscore/features/record-change-audit-log.md).
136
+
75
137
  ### Database connections
76
138
 
77
- New `[database_compass]` and `[database_compasscanada]` sections in `config.production.ini` reuse the
78
- **existing `[database_true]` account** — same client cluster, only `dbname` differs. **No new secret
79
- value was introduced.** See the accepted risk below.
139
+ The `[database_compass]` / `[database_compasscanada]` sections, and the two logs sections, all
140
+ reuse the **existing `[database_true]` account** — same client cluster, only `dbname` differs.
141
+ **No new secret value was introduced.** See the accepted risk below.
80
142
 
81
143
  ### Schema footprint: none
82
144
 
83
- The tool creates and alters **nothing**. It reads/writes only `Users`, `Users_Personas`,
84
- `Locations_Personas`, `Locations`, and reads `Personas` for names.
145
+ The tool creates and alters **nothing**. It reads/writes only `Users`, `Contacts` (+ its primary
146
+ email/phone children), `Users_Personas`, `Locations_Personas`, `Locations`, reads `Personas` for
147
+ names, reads `Core.ClientEmailDomains` / `Core.Clients`, and inserts into `Logs_<Tenant>.Record`.
85
148
 
86
149
  ## Decisions
87
150
 
@@ -92,6 +155,19 @@ The tool creates and alters **nothing**. It reads/writes only `Users`, `Users_Pe
92
155
  and then **closed unmerged** — do not resurrect them without re-opening that decision.
93
156
  - **2026-09-04 — Reuse the `[database_true]` credential** rather than split out a least-privilege
94
157
  Compass account. Raised twice in security review and consciously declined for now (see below).
158
+ - **2026-09-04 — Ship all three fields (email, phone, cost center), not phone-only (ajean).** The
159
+ independent architecture and security reviews both recommended shipping **phone only** and
160
+ deferring email. The developer was shown both and chose to ship all three **with the six safety
161
+ rails listed above**. Those rails are the conditions the decision was made under: removing the
162
+ domain allowlist, the cross-row duplicate refusal, the typed-twice confirmation, or the
163
+ audit-before-write re-opens the decision and needs a fresh security review.
164
+ - **2026-09-04 — Cost-center edits are deliberately TEMPORARY (ajean).** `setCostCenter()` writes
165
+ `Users.locationId` and `Users.c_erpEntityId` but **not** `Users.c_erpSystemEntityId`, so for any
166
+ user in the nightly PEOPLE file the change is undone that night when the importer re-resolves
167
+ `locationId` from the employee key. Chosen over corrupting the employee directory key —
168
+ `c_erpSystemEntityId` is also copied into `c_hrEmpDirectoryKey` and treated as unique per
169
+ employee. Same one-way-durability shape as the activation gotcha. Detail:
170
+ [Cost Centers](../../../clients/compass-usa/features/cost-centers.md).
95
171
 
96
172
  ## Gotchas
97
173
 
@@ -100,34 +176,89 @@ The tool creates and alters **nothing**. It reads/writes only `Users`, `Users_Pe
100
176
  absent from that day's file — so an activation only sticks if the person is actually in the file.
101
177
  Deactivation holds, because the cron never reactivates anybody. Full lifecycle:
102
178
  [PEOPLE-file User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
179
+ - **⚠ A cost-center edit is undone the same night for anyone in the PEOPLE file** — by design, see
180
+ the decision above. Set expectations with the requester; it is a stop-gap, not a fix.
181
+ - **⚠ The database will silently accept a duplicate email — the duplicate check MUST live in code.**
182
+ Prod-verified 2026-09-04: **neither** `Client_Compass.Users` nor `Client_CompassCanada.Users` has
183
+ a `UNIQUE KEY` on `email` (nor any index on it). The blank-client template's `UNIQUE KEY email`
184
+ does not describe these tenants. `usersWithEmail()` is the only thing standing between a typo and
185
+ two accounts sharing one login identity. Full index list:
186
+ [PEOPLE-file User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
187
+ - **⚠ A per-tenant email-domain allowlist must span BOTH Compass clients.** Real Compass Canada
188
+ users are on `compass-canada.com`, which is registered in `Core.ClientEmailDomains` to
189
+ **client 2 (Compass Group / `Compass_Usa`)**, not client 43. Scoping the domain check to the
190
+ tenant being edited rejects legitimate Canadian staff. `Users.clientId` is also **NULL** on
191
+ ~178,615 of 178,616 `Client_Compass` rows, so the client id cannot be derived from the user row.
192
+ See [Compass Canada profile](../../../clients/compass-canada/profile.md).
103
193
  - **A cross-region user still needs a code change and a deploy.** The only durable way to hold one
104
194
  active is the hardcoded four-address list in `ensureCrossRegionUsersActive()` in
105
- `worker2/Worker/Client/Compass/PeopleFile.php`. This tool does not change that.
195
+ `worker2/Worker/Client/Compass/PeopleFile.php`. This tool does not change that — and because that
196
+ list matches on **email**, those four addresses are blocked from this tool's email edit entirely.
106
197
  - **Every user search is a full table scan.** `Client_Compass.Users` has no index on `email`,
107
198
  `c_hrEmpUsername`, `c_hrEmpPersonnelNbr` or `isActive` (176k rows; `EXPLAIN` reports `type=ALL`,
108
199
  no possible keys). Same on `Client_CompassCanada`. Keep result sets bounded and do not add
109
- per-row lookups against those columns in a loop.
200
+ per-row lookups against those columns in a loop. The cost-center search is the same story —
201
+ `Locations` has no usable index for it either, which is why it is a picker, not a type-ahead.
110
202
  - **⚠ Accepted risk — the tool widens the blast radius of one shared credential.** It points the
111
- `[database_true]` account at two more tenant schemas. `config.production.ini` is committed and was
112
- web-readable until the 2026-07-24 `FilesMatch` deny blocks, so **rotation of that account is still
113
- owed**. An inline comment in the ini records this and lists the exact grants a split-out Compass
114
- account would need. Never paste the value anywhere.
203
+ `[database_true]` account at four more schemas (two tenants + two logs). `config.production.ini`
204
+ is committed and was web-readable until the 2026-07-24 `FilesMatch` deny blocks, so **rotation of
205
+ every secret in that file is still owed**. A least-privilege Compass account would shrink the
206
+ blast radius of that shared credential, which also reaches `Client_True` — where the
207
+ `Development Team` persona rows that gate this very tool live. An inline comment in the ini
208
+ records this and lists the exact grants a split-out account would need. Never paste the value
209
+ anywhere.
115
210
  - **⚠ Persona revocation does not take effect for up to 4 days.** Tools snapshots persona names into
116
211
  `$_SESSION` at login and sessions last 4 days, so removing someone's `Development Team` persona
117
212
  does not lock them out of these pages until their session dies. See
118
- [SAML SSO & persona-gated auth](./saml-sso-auth.md).
119
- - **No staff-actor audit trail exists for these writes.** `Logs_Compass.Record.userId` is a **tenant**
120
- `Users.id`, so it cannot record a `Client_True` staff member — it is not usable as the audit log
121
- for this tool, and `Logs_Compass.Record` is the client-facing activity log (3.6M rows) besides.
122
- See
123
- [Record Change Audit Log](../../../2.0/apps/_underscore/features/record-change-audit-log.md).
124
- Auditing internal-tool actions remains an open item.
213
+ [SAML SSO & persona-gated auth](./saml-sso-auth.md). Now that these pages write user identity
214
+ fields, a **narrower persona for the write actions** is worth considering.
215
+ - **⚠ `App_Database` transactions are GLOBAL, so the audit write must happen BEFORE
216
+ `beginTransaction()`.** `beginTransaction()` / `commitTransaction()` / `rollbackTransaction()`
217
+ take no arguments and act on **every** registered connection. A `Logs_<Tenant>` insert issued
218
+ inside the transaction belongs to it and disappears on rollback — exactly when you most want the
219
+ audit row. A bare `rollbackTransaction()` also leaves autocommit off for the rest of the request.
220
+ Full footgun: [MVC data access patterns](./mvc-data-access-patterns.md).
125
221
  - **Detecting a no-op UPDATE needs `App_Database::affectedRows()`** — `App_Database::query()` returns
126
222
  the `App_Query` execute result, not a row count. Details in
127
223
  [MVC data access patterns](./mvc-data-access-patterns.md).
224
+ - **Strip line breaks out of anything you log or email.** `App_Page::getVarIfSet()` does **not**
225
+ remove newlines, so a request value can forge extra `error_log()` lines. Use
226
+ `App_String::stripBreaksAndTabs()`. It matters for the email field too: a stray line break or
227
+ degree sign survives the filter, stores fine, escapes fine, and then makes PHPMailer silently
228
+ drop the recipient. See [MVC data access patterns](./mvc-data-access-patterns.md).
229
+
230
+ ## Open follow-ups (other repos)
231
+
232
+ - **dbchanges2** — add index `Locations (locationTypeId, isActive, c_erpSystemEntityId)` in **both**
233
+ Compass tenants; the cost-center search currently reads the whole table.
234
+ - **_underscore** — fix `_Model_Compass_SalesOrder::_costCenter()`
235
+ (`_underscore/Model/Compass/SalesOrder.php:287-305`): it joins
236
+ `INNER JOIN Contacts ON Contacts.id = Users.id`, which should be `Users.contactId`.
237
+ - **worker2** — scope `ensureCrossRegionUsersActive()` to a `Users.id` list instead of matching on
238
+ email; that would also lift this tool's block on those four addresses.
239
+ - **ops** — rotate every secret in `tools/config.production.ini`; consider a least-privilege
240
+ Compass account.
128
241
 
129
242
  ## Change history
130
243
 
244
+ - 2026-09-04 — Added **email, phone and cost-center editing** (PR tools#14,
245
+ `feature/compass-user-field-edits`, **open / not merged / not deployed**): new `App_Compass`
246
+ setters plus the `normaliseEmail` / `normalisePhone` / `emailDomainIsRegistered` /
247
+ `usersWithEmail` / `isCrossRegionProtectedEmail` validators, a cost-center picker with a mandatory
248
+ before/after persona diff, and a `Users` + primary `Contacts` write in one transaction. No schema
249
+ change. Recorded the decision to ship **all three** fields against independent
250
+ architecture/security advice to defer email, and that the six safety rails (typed twice,
251
+ `Core.ClientEmailDomains` domain check, cross-row duplicate refusal, duplicate-twin confirmation,
252
+ cross-region address block, audit-before-write) are the conditions of that decision. Recorded that
253
+ **cost-center edits are deliberately temporary** because `c_erpSystemEntityId` is not overwritten.
254
+ **Resolved the "no staff-actor audit trail" open item** — `Logs_<Tenant>.Record.userId` is
255
+ nullable, so `recordId = 2` (`Users`) + `primaryKeyId` + staff identity in `note` works with no
256
+ schema change (`note` is 255 **characters** — truncate with `mb_substr`); hence the new
257
+ `[database_compasslogs]` / `[database_compasscanadalogs]` ini sections, still on the existing
258
+ `[database_true]` account. Prod-verified that **neither tenant has a `UNIQUE KEY` on
259
+ `Users.email`**, so the duplicate check must live in code, and that `compass-canada.com` is
260
+ registered to Compass **USA** (client 2), so an email-domain allowlist must span both Compass
261
+ clients. Marked PR tools#13 **merged** and the doc `active`. (ajean)
131
262
  - 2026-09-04 — Initial: built the two-page Compass user/persona admin (PR tools#13, unmerged) — the
132
263
  first `tools` pages that write to a client tenant DB, gated to `Development Team` only, with a
133
264
  US/Canada switcher and new `[database_compass]` / `[database_compasscanada]` ini sections reusing
@@ -0,0 +1,185 @@
1
+ ---
2
+ title: GitHub Audit (monthly code-churn report)
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-04
10
+ owners: [jcardinal]
11
+ files:
12
+ - tools/_/app/github/audit.php
13
+ - tools/mvc/developers/github-audit/get.php
14
+ - tools/mvc/developers/github-audit/post.php
15
+ - tools/_/app/nav.php
16
+ related:
17
+ - ../architecture.md
18
+ - ./design-demo-admin.md
19
+ - ../../../standards/frontend.md
20
+ - ../features/persona-gated-navigation.md
21
+ - ../../test/features/github-audit-script.md
22
+ ---
23
+
24
+ ## Summary
25
+
26
+ An internal report at **`/developers/github-audit`** (Developers nav group, personas
27
+ `['Development Team','TOGa Technology']`) that shows, for one calendar month, the **lines
28
+ added/removed across every repo in the `agilantsolutions` GitHub org**, plus commit count,
29
+ unique authors, and active repo count. Built so the team gets a monthly code-churn report
30
+ without leaving GitHub.
31
+
32
+ It is the app port of the standalone
33
+ [`test/team/github_audit.php`](../../test/features/github-audit-script.md)
34
+ script (built first, then moved into Tools). The engine reuses the **Design tool's GitHub
35
+ plumbing** — same token, same log-detail/return-generic error pattern.
36
+
37
+ ## Key files / entry points
38
+
39
+ - `_/app/github/audit.php` — class **`App_Github_Audit`**: a static, read-only GitHub REST
40
+ engine (not a Controller/Model). Mirrors the sibling `App_Design_Github` static-engine
41
+ pattern.
42
+ - `mvc/developers/github-audit/get.php` — the page: native `<input type="month">` picker
43
+ (defaults to last month), a Run button, a progress bar, and results (summary cards +
44
+ per-repo table). Themed with `--c-*` tokens (light/dark).
45
+ - `mvc/developers/github-audit/post.php` — AJAX JSON endpoint. `requireAuth` +
46
+ `App_Page::validateCrossSiteRequestForgery()`; allowlisted actions only.
47
+ - `_/app/nav.php` — one nav action under the `developers` group.
48
+
49
+ ## How it works
50
+
51
+ ### Shared token (depends on the Design tool)
52
+
53
+ The audit engine does **not** define its own token config. It reuses
54
+ `App_Design_Github::token()` — the `GITHUB_TOKEN` env property first, then the
55
+ `[design] github_token` config fallback. So the GitHub token is **shared** between the
56
+ Design tool and the audit tool, which drives the concurrency lock below.
57
+
58
+ ### Accuracy — per-commit exact counting
59
+
60
+ To get exact line counts, the engine lists **each commit** in the month window
61
+ (`since`/`until`), then fetches **each commit's stats**. Merge commits are **skipped** (they
62
+ would double-count), and forks are **skipped**.
63
+
64
+ ### Excluded repos
65
+
66
+ `App_Github_Audit` has a top-of-class `const EXCLUDED_REPOS = ['test', 'claude'];` — a small
67
+ list of repo names to skip entirely. Matched **case-insensitively** in `fetchRepos()`, so
68
+ those repos never enter the run. Add more names here to skip them later. This is in addition
69
+ to the existing skip-forks / (optional) skip-archived filters.
70
+
71
+ ### Two-phase run + real progress bar
72
+
73
+ The report can hit hundreds of API calls, so the run is split so the page can show true
74
+ progress:
75
+
76
+ 1. `action=repos` — one call that lists all org repos.
77
+ 2. `action=repo` — audits **one** repo per request (repo N of total).
78
+
79
+ The client accumulates totals and authors across the per-repo responses and renders at the
80
+ end. If the run stops midway, it shows **partial results**.
81
+
82
+ ### Reliability — retries and a budget gate at 1000 calls
83
+
84
+ - GitHub `202` "stats not ready" → retried, then counted as `0` if still not ready.
85
+ - Secondary / abuse rate limit (`403` + `Retry-After`) → retried.
86
+ - **Budget gate** — `const MIN_REMAINING_TO_RUN = 1000;` sets the floor. Enforced at two
87
+ points, both throwing a user-safe (`ERR_USER_SAFE`) message:
88
+ - **Pre-flight:** `App_Github_Audit::assertBudget()` calls `GET /rate_limit` (which does
89
+ **not** count against the rate limit) at the start of a run. If the shared token's core
90
+ `remaining` is **at or below 1000**, it refuses to start and the scan never begins.
91
+ - **Mid-run:** `assertRateLimitOk()` **stops** the run once `x-ratelimit-remaining` drops
92
+ to 1000 — read from headers already returned, so no extra calls — and shows partial
93
+ results.
94
+ - This replaced the earlier tiny `RATE_LIMIT_FLOOR=25` safety. Rationale: the GitHub token is
95
+ **shared with the Design tool**, so the audit must leave a healthy budget for other work.
96
+
97
+ ### Live API-call + budget counter
98
+
99
+ - `App_Github_Audit::callCount()` — calls made in this PHP request.
100
+ - `App_Github_Audit::rateLimit()` — GitHub `x-ratelimit-*` headers (used / remaining /
101
+ limit / reset).
102
+
103
+ The page shows a live line: `API calls this run: N · GitHub budget: X of 5,000 left`, and
104
+ the total in the results caption.
105
+
106
+ ### Per-session concurrency lock (shared-token protection)
107
+
108
+ `post.php` takes an **APCu lock** (`apcu_add`, key `gha_audit_lock_<session_id>`, 300s TTL)
109
+ so only one audit request runs at a time per session. Reason: the GitHub token is **shared**
110
+ with the Design tool, so parallel scans would burn the org's rate limit for everyone. If
111
+ APCu is unavailable the lock is skipped cleanly.
112
+
113
+ - **Known limit:** APCu is per-EB-instance, so the lock is **per-instance**, not global.
114
+
115
+ ### Error sanitizing
116
+
117
+ Only a **user-safe** `RuntimeException` (the rate-limit notice, thrown with code
118
+ `App_Github_Audit::ERR_USER_SAFE`) reaches the browser. Every other `RuntimeException` /
119
+ `Throwable` is logged and returned as a **generic** message — GitHub's raw text and repo
120
+ names never surface. Same log-detail / return-generic pattern as
121
+ `App_Design_Github::commitError()`.
122
+
123
+ ### Input validation & SSRF guard
124
+
125
+ - **Month:** must match `^\d{4}-\d{2}$` **and** month number 01–12. The regex alone accepts
126
+ `2026-99`, which `DateTime::createFromFormat` silently overflows.
127
+ - **Repo name:** must match `^[A-Za-z0-9._-]{1,100}$` and is rejected if it contains `..`.
128
+ - `org` / `repo` / `sha` are `rawurlencode`'d.
129
+ - `$_POST` reads are guarded with `is_string()` before the `(string)` cast — an array value
130
+ would raise an **uncatchable** PHP warning → `App_Error` exit → HTML instead of JSON.
131
+ - **SSRF:** host is pinned to `api.github.com`; Link-header pagination is followed only when
132
+ the host is still `api.github.com`; curl is pinned to `CURLPROTO_HTTPS` with SSL verify on.
133
+
134
+ ## Access control
135
+
136
+ Behind Tools SSO. `get.php` and `post.php` self-guard with `requireAuth` +
137
+ `App_Page::validateCrossSiteRequestForgery()`; the nav action is gated to the
138
+ `Development Team` and `TOGa Technology` personas.
139
+
140
+ ## GitHub token
141
+
142
+ No new config. Uses `App_Design_Github::token()` — see
143
+ [Design Demo Admin › GitHub token](./design-demo-admin.md). Value never recorded, logged, or
144
+ echoed.
145
+
146
+ ## Client variations
147
+
148
+ None — internal/shared developer tool.
149
+
150
+ ## Gotchas / known issues
151
+
152
+ - **The `hidden` attribute is beaten by a class `display:` rule.** The results/spinner box
153
+ had the HTML `hidden` attribute but still showed on load (looked like the tool auto-ran),
154
+ because an author `display:flex` class rule beats the UA `[hidden]{display:none}`. Fix: a
155
+ scoped guard `.<scope> [hidden] { display:none !important; }` (or `:not([hidden])` on the
156
+ display rule). General server-rendered-UI trap.
157
+ - **`curl_close()` is fatal on PHP 8.5** in the 1.0 framework (deprecation → `ErrorException`
158
+ → aborted request). The audit engine avoids `curl_close()` — same rule as the Design tool.
159
+ See [Tools architecture](../architecture.md) and framework-rules.
160
+ - **APCu lock is per-EB-instance**, so the single-run guard is per-instance, not org-wide.
161
+ - **Shared rate-limit budget** — this tool and the Design tool draw on the same GitHub token,
162
+ so a big audit eats into the Design tool's budget and vice versa.
163
+
164
+ ## Change history
165
+
166
+ - 2026-09-04 — Added `EXCLUDED_REPOS = ['test','claude']` (case-insensitive skip in
167
+ `fetchRepos()`). Replaced `RATE_LIMIT_FLOOR=25` with a 1000-call budget gate
168
+ (`MIN_REMAINING_TO_RUN`): pre-flight `assertBudget()` via free `GET /rate_limit`, plus
169
+ mid-run `assertRateLimitOk()` stop — both user-safe — to leave budget for the shared Design
170
+ tool token. (jcardinal)
171
+ - 2026-09-04 — Created. New `/developers/github-audit` report over the `agilantsolutions` org
172
+ (lines +/-, commits, unique authors, active repos, per month). Per-commit exact counting
173
+ (merges/forks skipped), 202 + secondary-rate-limit retries with a primary-limit floor,
174
+ two-phase repos→repo progress, live API-call/budget counter, per-session APCu concurrency
175
+ lock (shared Design-tool token), user-safe-only error surfacing, and month/repo input +
176
+ SSRF hardening. Ported from `test/team/github_audit.php`. (jcardinal)
177
+
178
+ ## Related docs
179
+
180
+ - [Tools Architecture](../architecture.md)
181
+ - [Design Demo Admin](./design-demo-admin.md) — source of the shared GitHub token and error pattern
182
+ - [Front-End Standards](../../../standards/frontend.md)
183
+ - [github_audit.php standalone script](../../test/features/github-audit-script.md)
184
+ </content>
185
+ </invoke>