toga-ai 1.0.0

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 (62) hide show
  1. package/.claude/settings.json +119 -0
  2. package/.claude-plugin/marketplace.json +87 -0
  3. package/.claude-plugin/plugin.json +22 -0
  4. package/CLAUDE.md +161 -0
  5. package/README.md +72 -0
  6. package/agents/framework-pattern-checker.md +67 -0
  7. package/agents/harness-optimizer.md +102 -0
  8. package/agents/knowledge-writer.md +62 -0
  9. package/agents/php-build-resolver.md +51 -0
  10. package/agents/php-reviewer.md +51 -0
  11. package/agents/planner.md +88 -0
  12. package/agents/session-capture.md +101 -0
  13. package/agents/sql-reviewer.md +67 -0
  14. package/contexts/dev.md +43 -0
  15. package/contexts/research.md +49 -0
  16. package/contexts/review.md +37 -0
  17. package/knowledge/1.0/apps/library/INDEX.md +5 -0
  18. package/knowledge/1.0/apps/library/architecture.md +105 -0
  19. package/knowledge/1.0/apps/worker/INDEX.md +5 -0
  20. package/knowledge/1.0/apps/worker/architecture.md +223 -0
  21. package/knowledge/1.0/standards/backend-php.md +450 -0
  22. package/knowledge/2.0/apps/_underscore/INDEX.md +6 -0
  23. package/knowledge/2.0/apps/_underscore/architecture.md +183 -0
  24. package/knowledge/2.0/apps/_underscore/features/recursive-item-fulfillments.md +111 -0
  25. package/knowledge/2.0/apps/api2/INDEX.md +5 -0
  26. package/knowledge/2.0/apps/api2/architecture.md +162 -0
  27. package/knowledge/2.0/apps/worker2/INDEX.md +6 -0
  28. package/knowledge/2.0/apps/worker2/architecture.md +127 -0
  29. package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +135 -0
  30. package/knowledge/2.0/standards/backend-php.md +710 -0
  31. package/knowledge/CONVENTIONS.md +117 -0
  32. package/knowledge/INDEX.md +19 -0
  33. package/knowledge/clients/.gitkeep +0 -0
  34. package/knowledge/registry.json +7 -0
  35. package/knowledge.js +384 -0
  36. package/mcp-configs/README.md +72 -0
  37. package/mcp-configs/mcp-servers.json +23 -0
  38. package/package.json +50 -0
  39. package/rules/README.md +53 -0
  40. package/rules/common/coding-style.md +123 -0
  41. package/rules/common/git-workflow.md +72 -0
  42. package/rules/common/security.md +118 -0
  43. package/rules/common/testing.md +74 -0
  44. package/rules/php/app-framework.md +104 -0
  45. package/rules/php/underscore-framework.md +111 -0
  46. package/scripts/harness.js +605 -0
  47. package/scripts/hooks/evaluate-session.js +55 -0
  48. package/scripts/hooks/post-edit-validate.js +102 -0
  49. package/scripts/hooks/session-end.js +13 -0
  50. package/scripts/hooks/session-start.js +57 -0
  51. package/scripts/install.js +611 -0
  52. package/scripts/pre-commit +46 -0
  53. package/skills/capture/SKILL.md +294 -0
  54. package/skills/code-review/SKILL.md +140 -0
  55. package/skills/create-elastic-beanstalk/SKILL.md +217 -0
  56. package/skills/harness-audit/SKILL.md +152 -0
  57. package/skills/kickoff/SKILL.md +151 -0
  58. package/skills/php-patterns/SKILL.md +296 -0
  59. package/skills/session-resume/SKILL.md +156 -0
  60. package/skills/session-save/SKILL.md +158 -0
  61. package/skills/sync-team-skills/SKILL.md +87 -0
  62. package/sync-skills.js +71 -0
@@ -0,0 +1,450 @@
1
+ ---
2
+ title: Back-End Coding Standards
3
+ framework: "1.0"
4
+ project: Library
5
+ client: shared
6
+ type: standard
7
+ status: active
8
+ updated: 2026-06-08
9
+ owners: [jcardinal]
10
+ files: []
11
+ related:
12
+ - ../apps/library/architecture.md
13
+ - ../../2.0/standards/backend-php.md
14
+ ---
15
+
16
+ # Back-End Coding Standards (1.0 Legacy — `App_` Framework)
17
+
18
+ > **Scope & posture.** These standards cover server-side PHP for the legacy **1.0**
19
+ > framework — the `App_`/`Browser_` codebase built on the **`library`** repo (the 2.0
20
+ > framework is `_underscore`; see its own backend standard). 1.0 is a mature, maintenance-mode
21
+ > codebase with **far less consistency** than 2.0. This document **describes how the legacy
22
+ > code actually works** so that new and modified code stays consistent with the surrounding
23
+ > code. Where a convention is strong and universal, follow it strictly; where the codebase is
24
+ > historically inconsistent, this is called out — match the local file you're editing and
25
+ > prefer the cleaner pattern for new code. **Do not** retrofit 2.0 conventions (UUIDs,
26
+ > `utf8mb4`, metadata-driven APIs) onto 1.0.
27
+
28
+ ## General Principals
29
+
30
+ ### Modularity and Reusability
31
+
32
+ * Ensure that the code is modular, making use of functions and classes to promote reusability and separation of concerns.
33
+
34
+ ## Framework & Project Structure
35
+
36
+ ### The `library` repo
37
+
38
+ * `library` is the shared code for **all 1.0 applications** — the `App_` framework. Target PHP 7.2+, compatible through 8.1.
39
+ * **`library/app/`** — back-end logic, class prefix `App_` (no HTML output).
40
+ * **`library/browser/`** — server-rendered UI, class prefix `Browser_` (emits HTML).
41
+ * **`resources/` repo** — purely front-end assets (JS/CSS). **Do not put front-end assets in `library`.**
42
+ * Consuming apps use **no Composer** — `library` *is* the vendor directory. Add third-party packages directly to `library` as top-level folders.
43
+
44
+ ### Bootstrap & autoloader (`_.php`)
45
+
46
+ * Every app starts with `require_once '/path/to/library/_.php';`. It sets the PHP environment (`display_errors`, `error_reporting(E_ALL)`, timezone **`America/Chicago`**), defines `__APPROOT__` (the ancestor folder containing a `_` directory) and `__LIBROOT__` (the `library/` dir), and registers the autoloader.
47
+ * **Class-to-file mapping:** replace every `_` with `/`, **lowercase the entire result**, append `.php`. Both `__APPROOT__` and `__LIBROOT__` are searched, so app classes can **shadow** library classes.
48
+
49
+ ```
50
+ App_Model_Client → app/model/client.php
51
+ Browser_Form_Input_Autocomplete → browser/form/input/autocomplete.php
52
+ ```
53
+
54
+ * All folder/file names **must be lowercase**. Class names are not case-sensitive (the autoloader lowercases before lookup). Namespace separators (`\`) normalize to `_`.
55
+
56
+ ### Conventions you must know
57
+
58
+ * **`debugVar($input, $stopExecution, $withHrLineBreaker)`** is the global debug dump helper. **Remove all `debugVar()` calls before merging.**
59
+ * **`DOA_` prefix** (on files, folders, classes, and methods) marks **deprecated/legacy code that is still in active use** ("Dead On Arrival"). Do not build new functionality on `DOA_`-prefixed code; do not assume it is safe to delete — it often still has live dependencies. Check for an explicit removal note before touching it.
60
+
61
+ ## Database/SQL
62
+
63
+ > **Legacy DB reality (verified against production):** legacy schemas (`Core`, `TOGA_<client>`,
64
+ > `Vision`, …) are **`InnoDB`** but use the **`latin1_swedish_ci`** collation throughout — not
65
+ > `utf8mb4`. Tables have an **`id`** auto-increment primary key and **no `uuid` column**, **no
66
+ > `c_` custom-field prefix**, and **almost no column/table comments**. This is the opposite of
67
+ > 2.0 on those points — do not import the 2.0 rules. The naming conventions, however, are
68
+ > shared (PascalCase tables, camelCase fields).
69
+
70
+ ### Table Design — Order of Fields
71
+
72
+ 1. `id` (mandatory — auto-increment unsigned primary key)
73
+ 2. Foreign Keys
74
+ 3. Record Identifiers (references like order number)
75
+ 4. Date/Time fields (legacy uses a `dt` prefix, e.g. `dtCreated`, `dtUpdated`)
76
+ 5. Date fields
77
+ 6. Flag/Boolean fields (`is`-prefixed)
78
+ 7. ENUM-type fields (lists)
79
+ 8. Char/Varchar/Integer/Decimal fields
80
+ 9. Text/Blob fields
81
+
82
+ > Note vs 2.0: legacy has **no `uuid` second column** and **no trailing `c_` custom-field
83
+ > block**. The primary key alone (`id`) identifies a record.
84
+
85
+ ### Primary Keys & IDs
86
+
87
+ * Every table has an `id` column: `INT UNSIGNED`, `AUTO_INCREMENT`, primary key, as the **first** column.
88
+ * There is no external/UUID identifier in legacy. Do not add a `uuid` column to a legacy table unless a feature explicitly requires it.
89
+
90
+ ### Engine, Character Set & Collation
91
+
92
+ * All tables use the **`InnoDB`** engine.
93
+ * Legacy tables use **`latin1_swedish_ci`**. New tables in a legacy database should **match the collation of the database/tables they join against** — mixing `latin1` and `utf8mb4` columns in a `JOIN`/`WHERE` causes "Illegal mix of collations" errors. Do not introduce `utf8mb4_0900_ai_ci` (the 2.0 standard) into a legacy schema without confirming every joined column is migrated.
94
+
95
+ ### Table & Field Comments
96
+
97
+ * Legacy tables and columns are almost never commented. Comments are **encouraged for new columns** but are not historically enforced here. Where you do comment, keep it short and descriptive and do not begin with "The".
98
+
99
+ ### Queries
100
+
101
+ * `SELECT *` is fine in your MySQL client but should be avoided in code (it stresses the server and breaks when columns change). Note the framework utility `App_Database::lookupRecord()` uses `SELECT *` internally by design — that is acceptable for that generic helper, not a license to use it in feature code.
102
+
103
+ #### Capitalization
104
+
105
+ * Capitalize all SQL keywords (`SELECT`, `FROM`, `INNER JOIN`, `ON`, `WHERE`, `AND`, `OR`, `LIKE`, `AS`, `ORDER BY`, `DESC`, `LIMIT`, `GROUP BY`, `HAVING`, etc.).
106
+
107
+ ```sql
108
+ # Bad
109
+ select column1 from table1
110
+
111
+ # Good
112
+ SELECT column1 FROM table1
113
+ ```
114
+
115
+ #### Spacing & SQL within PHP
116
+
117
+ > Legacy application code is **inconsistent** about SQL formatting. Framework code mostly
118
+ > follows the rules below; match this style for any new or modified SQL.
119
+
120
+ 1. Use tabs for indentation.
121
+ 2. Every clause starts on a new line; indent the column list and conditions.
122
+ 3. A line break after each comma in the `SELECT` column list.
123
+ 4. `SELECT`, `WHERE`, `GROUP BY`, `HAVING`, `ORDER BY` on their own lines.
124
+ 5. When writing SQL in PHP, put the opening/closing quotes on their own lines and indent the query.
125
+
126
+ ```php
127
+ $sql = "
128
+ SELECT
129
+ column1,
130
+ column2
131
+ FROM TableName
132
+ WHERE
133
+ val > 0
134
+ ORDER BY
135
+ column1 DESC
136
+ LIMIT 10
137
+ ";
138
+ $res = App_Database::query($sql, 'db_toga');
139
+ ```
140
+
141
+ #### Aliasing, Joining, Subqueries, Conditions
142
+
143
+ * Alias tables only when necessary (e.g. self-joins or duplicate table names); use `AS` for derived columns.
144
+ * Each `JOIN` on its own line; put `INNER JOIN`s before outer joins; prefer `LEFT OUTER JOIN` over `RIGHT OUTER JOIN`.
145
+ * Enclose subqueries in parentheses, start them on a new line, and indent them.
146
+ * Put multiple `WHERE` conditions on separate lines; start `AND`/`OR` on a new line; **use parentheses whenever mixing `AND` and `OR`**; surround operators (`>`, `<`, `=`) with spaces.
147
+
148
+ #### SQL Comments
149
+
150
+ * Prefer `#` over `--` to begin a SQL comment; keep comments concise and place them above the code they describe.
151
+
152
+ ### Encoding
153
+
154
+ * Data inserted into the database must already be decoded (not HTML- or URL-encoded) unless a column is explicitly intended to store encoded content — store the real `<` character, not `&lt;`.
155
+
156
+ ### SQL Naming Conventions
157
+
158
+ These conventions are shared with 2.0 and **do hold** across legacy schemas.
159
+
160
+ * **Case:** database names PascalCase or their formal name (e.g. `TOGA`), table names **PascalCase**, field names **camelCase**.
161
+ * **Foreign keys:** take the referenced table, singularize it, append `Id`, camelCase it — `Clients` → `clientId`, `PurchaseOrders` → `purchaseOrderId`. (Legacy FKs are `INT UNSIGNED`.)
162
+ * **Human-readable field:** call it `name` — not `label`, `type`, `displayName`, or `companyName`.
163
+ * **Bridge tables:** join the two table names with an underscore, parent first, keeping plurals aligned with the real table names (e.g. `Locations_LocationAttributes`).
164
+
165
+ ### SQL Data Types via `App_Model`
166
+
167
+ In code you rarely write raw column types — an `App_Model` declares each column as a `FIELDTYPE_*` constant that maps to the underlying type. The 1.0 constants (note the `FIELDTYPE_` prefix; 2.0 uses `FIELD_`):
168
+
169
+ | Constant | Purpose |
170
+ |---|---|
171
+ | `FIELDTYPE_PRIMARYKEY` / `FIELDTYPE_FOREIGNKEY` | unsigned int key columns |
172
+ | `FIELDTYPE_INT` / `FIELDTYPE_INT_AUTONUMBER` | integers |
173
+ | `FIELDTYPE_BOOLEAN` | `is`-flag tinyint |
174
+ | `FIELDTYPE_DECIMAL` | decimals |
175
+ | `FIELDTYPE_CHAR` / `FIELDTYPE_CHAR_CAPS` / `FIELDTYPE_CHAR_UCWORDS` / `FIELDTYPE_CHAR_HTMLENTITY` / `FIELDTYPE_CHAR_PASSWORD` / `FIELDTYPE_PHONE` | string variants |
176
+ | `FIELDTYPE_LIST` | ENUM/list |
177
+ | `FIELDTYPE_DATE` / `FIELDTYPE_DATETIME` / `FIELDTYPE_DATETIME_CREATED` / `FIELDTYPE_DATETIME_UPDATED` / `FIELDTYPE_TIME` / `FIELDTYPE_YEAR` | date/time (CREATED/UPDATED auto-managed) |
178
+ | `FIELDTYPE_BLOB` / `FIELDTYPE_BLOBSTORAGE` | binary / external blob storage |
179
+ | `FIELDTYPE_SERIALIZED` | PHP-serialized payload |
180
+
181
+ #### Flags/Booleans
182
+
183
+ * Flag fields begin with `is` (e.g. `isVisible`) and use `FIELDTYPE_BOOLEAN` (`UNSIGNED TINYINT`, `0` = false, `1` = true).
184
+
185
+ #### Integers
186
+
187
+ * Primary and foreign keys must be **unsigned** integers. Choose the smallest integer type that fits the range; use unsigned unless negative values are genuinely required.
188
+
189
+ ### SQL Injection Prevention
190
+
191
+ * Never interpolate raw user input into a query string. In order of preference:
192
+ 1. **Use the `App_Model` layer** — it builds and escapes queries for you.
193
+ 2. **When writing SQL by hand, escape every interpolated value with `App_Database::sqlEscape()`** before placing it in the query string. In `browser/` action files the helper `getVarEscaped('field')` wraps `sqlEscape()` for request values.
194
+ * `App_Database::sqlEscape()` escapes a value for safe interpolation (and handles arrays/objects recursively). `App_Database::sqlProtect()` is a **stricter, lossy filter** that *strips* `% \ / * " '` and the literal ` or` — use it only to sanitize free-form search input, never for values you intend to store intact.
195
+ * Legacy uses **mysqli** (via `App_Database`), not PDO/prepared statements; escaping discipline at the call site is the control.
196
+
197
+ ```php
198
+ // Bad — raw interpolation
199
+ $sql = "SELECT id FROM Users WHERE email = '$email'";
200
+
201
+ // Good — escape before interpolating
202
+ $email = App_Database::sqlEscape($email);
203
+ $sql = "
204
+ SELECT
205
+ id
206
+ FROM Users
207
+ WHERE
208
+ email = '$email'
209
+ ";
210
+
211
+ // Better — let the model layer handle it
212
+ $user = new App_Model_User();
213
+ // ...set lookup fields and load...
214
+ ```
215
+
216
+ ## PHP
217
+
218
+ ### PHP Tags
219
+
220
+ * Short tags (`<?`) are **not** permitted — always `<?php`.
221
+ * PHP files **MUST NOT** end with a closing `?>` tag (prevents accidental output of trailing whitespace).
222
+
223
+ ### File & Class Naming
224
+
225
+ * Files and folders are **all lowercase**; the path mirrors the class name with `_` → `/` (see autoloader above).
226
+ * Back-end logic classes are prefixed `App_`; server-rendered UI classes are prefixed `Browser_`.
227
+
228
+ ### Code Formatting
229
+
230
+ #### Indentation
231
+
232
+ * Use **tabs** for indentation, not spaces. (Universal in this codebase.)
233
+
234
+ #### Braces
235
+
236
+ * Opening braces for classes, methods, and control structures go on the **same line**.
237
+
238
+ ```php
239
+ if ($condition) {
240
+ // code
241
+ } else {
242
+ // code
243
+ }
244
+ ```
245
+
246
+ ### Naming Conventions
247
+
248
+ * **Variables:** camelCase, descriptive (`$primaryKeyId`, `$fieldChanges`).
249
+ * **Methods/functions:** camelCase (`buildModel()`, `getChangedFields()`).
250
+ * **Constants:** `UPPERCASE_SNAKE_CASE` (`FIELDTYPE_PRIMARYKEY`, `TABLENAME`, `MODE_EDIT`).
251
+ * **Classes:** PascalCase with the `App_`/`Browser_` prefix.
252
+
253
+ ### Positional Arguments (not named)
254
+
255
+ * 1.0 targets PHP 7.2 and the codebase uses **positional arguments only** — do **not** use PHP 8 named arguments (`substr(string: $x)`) here. (This is the opposite of the 2.0 standard.)
256
+
257
+ ```php
258
+ // Correct for 1.0
259
+ $piece = substr($value, 0, 8);
260
+ ```
261
+
262
+ ### Documentation
263
+
264
+ * **DocBlocks:** framework (`library`) classes/methods are well documented with a description and `@param`/`@return`/`@throws`. Application code is less consistent — document new and modified methods to the framework standard.
265
+ * **Comments:** explain the "why", not the "how"; use inline comments sparingly.
266
+
267
+ ### Performance
268
+
269
+ * Use single quotes for strings unless interpolation is needed (`'Hello'`, `"Hello $world"`, `'Hello ' . $world`). Note: legacy code is pragmatic and mixed here; prefer the rule for new code.
270
+
271
+ ### Miscellaneous
272
+
273
+ * Use `require_once` for include-once files. Avoid global variables and global state.
274
+
275
+ ## App_Model layer (the 1.0 ORM)
276
+
277
+ Each DB table has a model under `app/model/` extending `App_Model`.
278
+
279
+ ```php
280
+ class App_Model_Client extends App_Model {
281
+ const TABLENAME = 'Clients';
282
+ const DATABASE = 'db_toga';
283
+
284
+ public $id = self::FIELDTYPE_PRIMARYKEY;
285
+ public $clientName = self::FIELDTYPE_CHAR;
286
+ public $companyId = array(self::FIELDTYPE_FOREIGNKEY, 'App_Model_Company'); // FK → parent model
287
+ public $active = array(self::FIELDTYPE_BOOLEAN, 1); // [type, default]
288
+ }
289
+ ```
290
+
291
+ * Declare the table via `TABLENAME` and the connection via `DATABASE`.
292
+ * Declare each column as a public property set to a `FIELDTYPE_*` constant, **or** an array `[FIELDTYPE_*, default]`; foreign keys may carry the parent model name as a second element.
293
+ * CRUD / instance methods: `save($okayToLog = true, ...)` (insert or update by PK), `delete()`, `buildModel()` (load by PK — the constructor calls it when given an id), `getChangedFields()`.
294
+ * Static helpers: `lookupLabel($id)`, `lookupValue($id, $field)`, `exists($id)`, `fetchCount($conditions)`, `deleteWhere($include, $exclude)`.
295
+ * Models may also define UI-binding constants (`ACTION`, `VIEW`, `LISTING`, `TABS`) tying the model to its `browser/` screens.
296
+ * **Client-specific behavior** lives in `app/client/` overrides — keep tenant logic there, not in the base model.
297
+
298
+ ## App_Database layer
299
+
300
+ `App_Database` is a static wrapper over **mysqli** with result caching and query helpers. Connections are addressed by a link-registry string (e.g. `'db_toga'`, `'db_catalog'`).
301
+
302
+ * **Run a query:** `App_Database::query($sql, $linkRegistryString = '')`.
303
+ * **Read results:** `fetchRow(&$res)`, `fetchOne(&$res, $rowIndex, $colIndexOrField)`, `numRows(&$res)`.
304
+ * **Writes:** `affectedRows($link)`, `getInsertId($link)`.
305
+ * **Cached lookups:** `lookupField($table, $aryLookup, $returnField, $link, $throwIfNotFound)`, `lookupRecord($table, $idValue, $link)`.
306
+ * **Escaping:** `sqlEscape()` (safe interpolation) and `sqlProtect()` (strict, lossy filter) — see SQL Injection Prevention.
307
+
308
+ ## browser/ — Server-Rendered UI
309
+
310
+ The `browser/` tree renders HTML server-side. Components are `Browser_*` classes in lowercase files (`Browser_Datagrid` → `browser/datagrid.php`, `Browser_Form_Input_Text` → `browser/form/input/text.php`).
311
+
312
+ ### Core components
313
+
314
+ | Component | Purpose |
315
+ |---|---|
316
+ | `Browser_Datagrid` | Table/list view with filtering, sorting, pagination, bulk edit (140+ subclasses in `datagrid/`) |
317
+ | `Browser_Form` + `Browser_Form_Input_*` | Form builder and field elements (Text, Textarea, Select, Checkbox, Datepicker, Autocomplete, Button, …) |
318
+ | `Browser_Dialog` | Modal dialogs |
319
+ | `Browser_ButtonBar` + `Browser_ButtonBar_Button_*` | Button toolbars (Save, Cancel, Edit, Delete, Import, …) |
320
+ | `Browser_Breadcrumb` | Navigation trail |
321
+ | `Browser_Sidebar` | Sidebar modules |
322
+ | `Browser_Widget` | Dashboard widgets |
323
+ | `Browser_Status` | Status badges (40+ subclasses in `status/`) |
324
+ | `Browser_Message` | Alert/message boxes |
325
+
326
+ ### Page pattern — action.php + view.php
327
+
328
+ A screen is a folder under an app's `app/` tree with paired files:
329
+
330
+ * **`action.php`** — reads `$_REQUEST`/mode (`MODE_NEW`, `MODE_EDIT`), validates (`Browser_Form_Validator::isValid()`), and writes to the DB, then redirects/sets state.
331
+ * **`view.php`** — instantiates `Browser_*` components and renders them. Optional `tabs.php`, `summary.php`, `listing.php`.
332
+
333
+ ```php
334
+ // view.php (abridged)
335
+ $buttonBar = new Browser_ButtonBar();
336
+ $buttonBar->addButton(new Browser_ButtonBar_Button_Save());
337
+
338
+ $form = new Browser_Form();
339
+ $form->setAction(linkProductBundle($id, array('mode' => $mode), '..._action'));
340
+ $form->registerButtonBar($buttonBar);
341
+ $form->renderFormHeader();
342
+ $form->groupHeader('Product Bundle Information');
343
+
344
+ $element = new Browser_Form_Input_Text('name');
345
+ $element->setLabel('Name');
346
+ $element->setRequired(true);
347
+ echo $element->renderField();
348
+
349
+ $form->groupFooter();
350
+ $form->renderFormFooter();
351
+ ```
352
+
353
+ ### Datagrid pattern
354
+
355
+ Subclass `Browser_Datagrid` and configure it in the constructor — columns are added via `addField()`, not declared as properties:
356
+
357
+ ```php
358
+ class Browser_Datagrid_Accounts extends Browser_Datagrid {
359
+ public function __construct() {
360
+ parent::__construct('accounts');
361
+ $this->setTitle('Accounts');
362
+ $this->setTableJoins("
363
+ FROM Companies
364
+ LEFT OUTER JOIN Users AS AccountManagers ON ...
365
+ ");
366
+ $this->setViewLink(linkCompany('!Companies.id!')); // !Field! = row-value placeholder
367
+ $this->addField('active', 'Active', 'Companies.active', self::FILTERABLE,
368
+ array('style' => self::STYLE_BOOLEAN));
369
+ }
370
+ }
371
+ ```
372
+
373
+ * Column display uses `STYLE_*` constants (`STYLE_DATE`, `STYLE_CURRENCY`, `STYLE_BOOLEAN`, `STYLE_STOPLIGHT`, …).
374
+ * Filterability via `self::FILTERABLE` / `self::SEARCHABLE`.
375
+ * `!TableAlias.column!` placeholders are replaced per-row when building links.
376
+
377
+ ### Rendering & output
378
+
379
+ * Components render by **echoing HTML directly** (`render()` / `renderField()`), mixing PHP and HTML via inline `?> … <?php` fragments and heredocs. There is no separate template engine.
380
+ * For a string instead of direct output, components use `App_Page::startCapture()` / `App_Page::stopCapture()` (output buffering); many `render*` methods accept a `$renderAsString` flag.
381
+
382
+ ### HTML escaping (a known weak spot — do better in new code)
383
+
384
+ * Input values are escaped with `App_String::htmlEscapeQuotes()` / `htmlentities()` in some elements, **but escaping is inconsistent** across the layer (textarea, dialog, and datagrid-injected JS frequently emit unescaped strings).
385
+ * **For new/modified UI code, escape all output that contains user/DB data** (`App_String::htmlEntity()` / `htmlEscapeQuotes()`), especially anything interpolated into HTML attributes or inline `<script>`.
386
+
387
+ ### browser/ structure
388
+
389
+ * Lowercase files organized by component type: `datagrid/`, `form/`, `form/input/`, `dialog/`, `status/`, `sidebar/`, `buttonbar/`, `buttonbar/button/`, `widget/`.
390
+
391
+ ## Error Handling
392
+
393
+ * Throw exceptions for unexpected conditions: `throw new Exception('message')` is the prevailing pattern (an `App_Exception` class exists but is rarely used for throwing — match the surrounding file).
394
+ * Catch and handle exceptions where you can add value; provide meaningful messages.
395
+ * The `@` error-suppression operator is acceptable only for defensive file I/O (e.g. reading an optional cache file); do not use it to hide real errors.
396
+ * Use structured logging to capture application behavior and errors.
397
+
398
+ ## Security Best Practices
399
+
400
+ ### Input Validation and Sanitization
401
+
402
+ * Validate and sanitize all user input (see SQL Injection Prevention and HTML escaping above).
403
+
404
+ ### Secure Communication
405
+
406
+ * Always use HTTPS to encrypt data in transit.
407
+
408
+ ### Authentication and Authorization
409
+
410
+ * Use the framework's `App_Auth` / `App_Acl` for authentication and role-based access control; do not roll your own session/permission checks.
411
+
412
+ ## Performance Optimization
413
+
414
+ ### Caching
415
+
416
+ * `App_Database` provides built-in result caching for repeated lookups (`lookupField`/`lookupRecord`). Reuse it rather than re-running identical reads. Store per-request reusable data in variables; use sessions/cookies for cross-request client state.
417
+
418
+ ### Asynchronous / Background Processing
419
+
420
+ * For long-running or background work, hand off to the legacy worker tier (the `worker` / `worker1.5` apps) rather than running long tasks inline in a web request. Use AJAX for front-end interactions.
421
+
422
+ ## API Design
423
+
424
+ ### Outbound / integration APIs (`App_Api`)
425
+
426
+ * Third-party integrations live under `app/api/` as `abstract class App_Api_<Vendor> extends App_Api`, with static methods and an `initialize()` for setup; results are returned via `App_ApiResult`.
427
+ * RESTful principles apply when *we* expose endpoints, but many legacy partner/3rd-party APIs are not REST — follow the partner's contract.
428
+
429
+ > Note: the 2.0 metadata-driven API features (Record Scripts, API Payload Interceptors,
430
+ > `/v2` engine) **do not exist in 1.0**. Do not reference them in legacy code.
431
+
432
+ ## Logging and Monitoring
433
+
434
+ * Use structured logging with relevant context. Field/record change logging is available via the model layer (`App_FieldLog`) — keep `save()` logging enabled unless there's a reason not to.
435
+ * Implement monitoring/alerting to catch issues early (the `systemmonitor/` helpers support this).
436
+
437
+ ## Configuration Management
438
+
439
+ * Manage configuration through `App_Config` and per-environment config files. Keep secrets out of source where possible.
440
+
441
+ ## Code Quality and Maintainability
442
+
443
+ * Establish a thorough code review process.
444
+ * **Remove all `debugVar()` calls before merging.**
445
+ * Don't extend `DOA_`-prefixed code; plan its removal when you touch the area and dependencies allow.
446
+
447
+ ## Documentation
448
+
449
+ * Keep code well-documented with DocBlocks and usage examples.
450
+ * Maintain clear API documentation for endpoints we expose; use Postman for API testing/storage where applicable.
@@ -0,0 +1,6 @@
1
+ # _underscore (_Underscore) — 2.0 knowledge
2
+
3
+ | Doc | Summary | Files |
4
+ |-----|---------|-------|
5
+ | [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
6
+ | [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 |
@@ -0,0 +1,183 @@
1
+ ---
2
+ title: _underscore Framework Architecture
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: architecture
8
+ status: active
9
+ updated: 2026-06-08
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/_underscore.php
13
+ - _underscore/Loader.php
14
+ - _underscore/Framework.php
15
+ - _underscore/Model.php
16
+ - _underscore/Database.php
17
+ - _underscore/Query.php
18
+ - _underscore/Route.php
19
+ - _underscore/Component.php
20
+ related: []
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ `_underscore` is the shared PHP backend framework for **all 2.0 applications**. It
26
+ provides database abstraction, ORM-style models, routing, component rendering,
27
+ configuration, ACL, and third-party integrations. Every 2.0 project depends on it.
28
+ Minimum PHP 8.1 (enforced in `_underscore.php`). The current architecture is
29
+ **decoupled**: React handles the frontend; `_underscore` serves as the backend API layer.
30
+
31
+ ## Entry point & boot sequence
32
+
33
+ Every 2.0 project's `index.php` is just `<?php require '_underscore.php';`.
34
+
35
+ 1. `_underscore.php` — PHP version check, require `Initialize.php`
36
+ 2. `Initialize.php` — sets `_START_TIME`, loads interfaces, `Time.php`, `Loader.php`
37
+ 3. `Loader.php::initialize()` — registers the SPL autoloader, then in order:
38
+ `_Error::initialize()` → `_Environment::initialize()` → `_Config::initialize()`
39
+ (parses INI) → `_Time::initialize()` → **`_underscore::initialize()`** (project hook,
40
+ required) → `_Session::initialize()` → `_Route::initialize()` (web only).
41
+
42
+ ## Naming convention & autoloader
43
+
44
+ Class names map **directly** to file paths: replace each `_` with `/`; the last segment
45
+ is the `.php` filename. **Case-sensitive.**
46
+
47
+ | Class | File |
48
+ |---|---|
49
+ | `_Model_Client_User` | `Model/Client/User.php` |
50
+ | `_Model_Compass_SalesOrder` | `Model/Compass/SalesOrder.php` |
51
+ | `_Component_Ui_Button` | `Component/Ui/Button.php` |
52
+
53
+ The autoloader resolves framework root first, then the project namespace. External
54
+ libraries register via `_Framework::registerLibrary()`. There is **no Composer** for
55
+ core PHP deps; frontend libs (Bootstrap 5.1.3, Font Awesome 6) are vendored in
56
+ `Component/Library/`.
57
+
58
+ ## Database architecture
59
+
60
+ Built for multi-tenant, multi-database apps. Connections register via
61
+ `_Database::register()`; well-known categories:
62
+
63
+ | Constant | Description |
64
+ |---|---|
65
+ | `_underscore::DB_CORE` | Shared infra DB — clients, domains, environments, ACL, parameters. Models in `Model/Core/`. |
66
+ | `_underscore::DB_CLIENT` | Per-client business data; each client has an isolated DB. Models in `Model/Client/`. |
67
+ | `_underscore::DB_LOGS` | Core-level logging (non-client). |
68
+ | `_underscore::DB_CLIENT_LOGS` | Per-client logging / audit trails. |
69
+
70
+ `Database.php` handles named connections with read/write replica separation, **lazy
71
+ transactions** (a transaction starts only on first write), result caching, and
72
+ multi-tenant client-DB lookup from Core. `Query.php` wraps MySQLi with read/write
73
+ auto-routing and result helpers: `fetchRows()`, `fetchRow()`, `fetchOne()`,
74
+ `fetchRowsAssocArray()`, `fetchKeyValuePairs()`, `fetchValues()`.
75
+
76
+ ## Model layer
77
+
78
+ Each DB table has a model. Every model must define:
79
+
80
+ ```php
81
+ const DATABASE = _underscore::DB_CLIENT; // or DB_CORE
82
+ const TABLE = 'TableName';
83
+ ```
84
+
85
+ Columns are declared as `public` properties set to field-type constants. Numeric
86
+ (`FIELD_PRIMARYKEY`, `FIELD_FOREIGNKEY`, `FIELD_INTEGER`, `FIELD_BOOLEAN`,
87
+ `FIELD_DECIMAL`…), string (`FIELD_CHAR`, `FIELD_CHAR_CAPS`, `FIELD_CHAR_UUID`,
88
+ `FIELD_LIST`…), date/time (`FIELD_DATE`, `FIELD_DATETIME`, `FIELD_DATETIME_CREATED`
89
+ auto-set on insert, `FIELD_DATETIME_UPDATED` auto-set on save), binary
90
+ (`FIELD_BLOB`, `FIELD_STORAGE` for S3/filesystem), and calculated (`FIELD_SQL`).
91
+
92
+ **Foreign keys** are arrays with the target model:
93
+
94
+ ```php
95
+ public $salesOrderId = [
96
+ self::FIELD_FOREIGNKEY,
97
+ self::FIELDOPT_FOREIGNKEY_MODEL => '\_Model_Client_SalesOrder'
98
+ ];
99
+ ```
100
+
101
+ ### Calculated (SQL) fields
102
+
103
+ `FIELD_SQL` declares a computed field (always prefixed `_`). A static method of the
104
+ same name returns a raw SQL expression used inline in queries:
105
+
106
+ ```php
107
+ public $_extPrice = [self::FIELD_SQL, self::FIELDOPT_SQL_TYPE => self::FIELD_DECIMAL];
108
+ public static function _extPrice($table = self::TABLE) {
109
+ return "($table.quantity * $table.price)";
110
+ }
111
+ ```
112
+
113
+ `FIELDOPT_SQL_STORED => true` persists the result to a real column on save (must be
114
+ refreshed after bulk migrations); default `false` recalculates on every query.
115
+
116
+ ### CRUD
117
+
118
+ ```php
119
+ $po = new _Model_Team_PullRequest(); // create
120
+ $po->title = $title; $po->save();
121
+
122
+ $po = new _Model_Team_PullRequest(123); // update by PK
123
+ $po->title = $new; $po->save();
124
+
125
+ $v = new _Model_Client_TableView(); // load single
126
+ $v->slug = $slug; $v->isActive = true;
127
+ if ($v->load()) { /* found */ }
128
+
129
+ $s = new _Model_Client_State(); // search many
130
+ $s->name = $name; $states = $s->search();
131
+ ```
132
+
133
+ ### Client-specific model extensions
134
+
135
+ `Model/Client/` holds base models for all clients. A client needing custom logic gets a
136
+ directory (`Model/Compass/`, `Model/Aig/`, `Model/Canon/`) with classes extending the
137
+ base — e.g. `_Model_Compass_SalesOrder extends _Model_Client_SalesOrder`. Supports
138
+ deeply nested sub-clients (`Model/Compass/Canada/`). **This is the multi-tenant
139
+ customization hook — client behavior lives here, not in the API engine.**
140
+
141
+ ## Interceptors
142
+
143
+ Static methods on a model that intercept API calls before/after the HTTP method:
144
+ `[pre|post][HttpMethod](&$api, &$payload)` — e.g. `postPost`, `prePut`, `preDelete`.
145
+ Both args by reference; interceptors **do not return** — they mutate `$payload` and/or
146
+ perform side effects. `$api` carries user identity, JWT, client info, headers, route.
147
+
148
+ ## Scripted APIs & internal requests
149
+
150
+ A **Scripted API** is a custom static method acting as a full endpoint; first arg is
151
+ `&$api`, subsequent args come from the query string, and it **must return** a value
152
+ (JSON-encoded as the response). `$api->internalApiRequest($method, $route, $payload,
153
+ $options)` invokes another endpoint in-process (no HTTP round trip), reusing auth/ACL —
154
+ heavily used by interceptors and NetSuite traits.
155
+
156
+ ## Traits
157
+
158
+ `Trait/` composes shared model functionality: `Trait/Netsuite/` (22 ERP-sync traits:
159
+ SalesOrder, Invoice, Item, Customer, …), `Trait/Startech/`, `Trait/Ai/Bdr/`,
160
+ `Trait/NonStatic/` (`PrePost`, `Href`, `Toggle`, `ViewDataGetSet`), `Trait/Static/`.
161
+
162
+ ## Routing & components
163
+
164
+ `Route.php` parses the URI to a controller method via reflection: URI segments
165
+ (kebab-case → camelCase) become candidate method names; parameter names use the `inp`
166
+ prefix by default (`sales-orders` → `$inpSalesOrders`). `Component.php` renders
167
+ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentName>` tags.
168
+
169
+ ## Configuration
170
+
171
+ `Config/[environment].ini`, accessed via `_Config::database('username')`,
172
+ `_Config::debug('show_queries')`. Groups: `[_underscore]`, `[database]`, `[debug]`,
173
+ `[api]` (per-hostname).
174
+
175
+ ## Key abstractions
176
+
177
+ | Class | Purpose |
178
+ |---|---|
179
+ | `_Framework` | Abstract base all projects extend; `initialize()`, `pre()`, `post()`, DB/folder constants |
180
+ | `_Model` / `_Model_True` | Base ORM (field types, CRUD, FK traversal, SQL fields) |
181
+ | `_Database` / `_Query` | Connection pool + read/write routing / MySQLi wrapper with caching |
182
+ | `_Route` / `_Component` | URI router / HTML component renderer |
183
+ | `_Config` / `_Environment` / `_Loader` / `_Http` / `_Error` | config, env/debug, autoload, JWT/HTTP, errors |