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,152 @@
1
+ ---
2
+ name: harness-audit
3
+ description: Audits the health of the TOGA Technology team Claude knowledge repo. Runs validation, checks completeness, scores 0-100, and provides top action items. Trigger when the user says "harness-audit", "audit the knowledge base", "check the harness", "how healthy is the knowledge base", or "score the repo".
4
+ ---
5
+
6
+ # Harness Audit — check the health of the team knowledge repo
7
+
8
+ Perform a full health check of this repo and provide a scored report with concrete
9
+ action items. The goal is to catch drift, missing content, and integrity failures
10
+ before they cause problems in coding sessions.
11
+
12
+ ---
13
+
14
+ ## Step 1 — Locate the team repo
15
+
16
+ Use the same resolution order as `kickoff`:
17
+ 1. Claude memory `team-repo-path`
18
+ 2. Env var `CLAUDE_TEAM_REPO`
19
+ 3. Auto-discover (`./claude`, `../claude`, `../../claude`, walk up for `knowledge/INDEX.md`)
20
+ 4. Ask
21
+
22
+ From here, all commands use `node "<TEAM_REPO>/knowledge.js" …` and
23
+ `node "<TEAM_REPO>/scripts/harness.js" …`.
24
+
25
+ ---
26
+
27
+ ## Step 2 — Run automated checks
28
+
29
+ ### Check 1: Validation
30
+ ```sh
31
+ node "<TEAM_REPO>/knowledge.js" validate
32
+ ```
33
+ - **PASS:** output contains "validate: OK" and exit code 0
34
+ - **FAIL:** any `ERROR:` lines in output or non-zero exit code
35
+ - **Report:** count of errors, list first 5 error lines
36
+
37
+ ### Check 2: Harness status
38
+ ```sh
39
+ node "<TEAM_REPO>/scripts/harness.js" status
40
+ ```
41
+ Collect and display:
42
+ - Number of registered repos
43
+ - Doc counts per framework (broken down by type)
44
+ - Client count and doc counts
45
+ - Last git commit date on `knowledge/`
46
+
47
+ ### Check 3: Architecture coverage
48
+ For every repo in `registry.json`, check that
49
+ `knowledge/<framework>/apps/<repo>/architecture.md` exists.
50
+ - **PASS:** all repos have an architecture.md
51
+ - **FAIL:** list the specific missing paths
52
+
53
+ ### Check 4: INDEX.md integrity
54
+ Check `knowledge/INDEX.md` for the "Auto-generated" notice. Check each
55
+ `<fw>/apps/<repo>/INDEX.md` for the `| Doc |` table header (generated by `knowledge.js index`).
56
+ - **PASS:** all INDEX files appear auto-generated
57
+ - **FAIL:** list any INDEX.md files that look hand-edited (missing table structure)
58
+
59
+ Additionally, run `git -C "<TEAM_REPO>" log --oneline -- "*/INDEX.md"` and check
60
+ whether any INDEX.md commits were made by a human (non-`knowledge.js` author). Flag
61
+ these as warnings.
62
+
63
+ ### Check 5: clients/ directory
64
+ Check that `knowledge/clients/` exists.
65
+ - **PASS:** directory exists (even if empty)
66
+ - **FAIL:** directory missing — client knowledge has not been started
67
+
68
+ ### Check 6: 1.0/standards/ coverage
69
+ Check `knowledge/1.0/standards/` for at least one `.md` file.
70
+ - **PASS:** at least one standard exists
71
+ - **FAIL:** directory empty or missing — 1.0 has no documented standards
72
+
73
+ ### Check 7: 2.0/standards/ coverage
74
+ Check `knowledge/2.0/standards/` for at least one `.md` file.
75
+ - **PASS:** at least one standard exists
76
+ - **FAIL:** directory empty or missing — 2.0 has no documented standards
77
+
78
+ ### Check 8: Frontmatter completeness
79
+ For every doc in `knowledge/` (excluding INDEX.md files), verify presence of
80
+ required frontmatter: `title`, `framework`, `type`, `status`, `project`.
81
+ - **PASS:** all docs have all required fields
82
+ - **FAIL:** list docs with missing fields (up to 5; note count if more)
83
+
84
+ ---
85
+
86
+ ## Step 3 — Score and report
87
+
88
+ Each check is worth equal weight (100 / 8 = 12.5 points each, rounded).
89
+ Partial credit is not awarded — a check either passes or fails.
90
+
91
+ Present results in this format:
92
+
93
+ ```
94
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
95
+ Harness Audit — TOGA Technology Claude Knowledge Repo
96
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
97
+
98
+ ✓ [PASS] knowledge.js validate
99
+ ✓ [PASS] registry.json has entries (5 repos)
100
+ ✗ [FAIL] all repos have architecture.md
101
+ → Missing: 1.0/apps/library/architecture.md
102
+ ✓ [PASS] INDEX.md files appear auto-generated
103
+ ✓ [PASS] clients/ directory exists
104
+ ✓ [PASS] 1.0/standards/ has standards (1 file)
105
+ ✓ [PASS] 2.0/standards/ has standards (1 file)
106
+ ✗ [FAIL] all docs have required frontmatter
107
+ → 2.0/apps/worker2/features/clickup.md: missing "project"
108
+
109
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
110
+ Score: 75/100 (6/8 checks passed)
111
+
112
+ Top 3 action items:
113
+ 1. Create knowledge/1.0/apps/library/architecture.md
114
+ Run /capture after any Library session to generate this.
115
+ 2. Fix frontmatter in 2.0/apps/worker2/features/clickup.md: add "project: Worker"
116
+ Then run: node knowledge.js validate
117
+ 3. [next failing check...]
118
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Step 4 — Action item guidance
124
+
125
+ For each failing check, provide a specific actionable fix (not a vague suggestion):
126
+
127
+ | Check | Specific fix |
128
+ |-------|-------------|
129
+ | validate fails | Run `node knowledge.js validate`, read each `ERROR:` line, fix the listed file/frontmatter |
130
+ | missing architecture.md | Run `/capture` after the next session on that repo — architecture docs are created then |
131
+ | hand-edited INDEX.md | Run `node knowledge.js index` to regenerate — do NOT re-hand-edit |
132
+ | clients/ missing | Run `mkdir knowledge/clients && touch knowledge/clients/.gitkeep` |
133
+ | standards/ empty | During the next session on that framework, include a standards update in `/capture` |
134
+ | missing frontmatter | Open the file, add the missing field per `knowledge/CONVENTIONS.md` template, re-run validate |
135
+
136
+ After listing action items, offer: "Would you like me to fix any of these now?"
137
+ If the developer says yes, execute the fixes one at a time and re-run `validate` after
138
+ each write.
139
+
140
+ ---
141
+
142
+ ## Notes
143
+
144
+ - A score of 100 means the knowledge base is structurally complete and valid.
145
+ It does NOT mean the content is high quality — quality is a human judgment.
146
+ - A score below 60 means the knowledge base has significant structural problems
147
+ that will cause `kickoff` or `capture` to produce unreliable results.
148
+ - Run `/harness-audit` before any major PR to the `claude` repo to confirm nothing
149
+ was inadvertently broken.
150
+ - The `PostToolUse` hook in `.claude/settings.json` auto-validates any write to
151
+ `knowledge/` files, so most integrity errors are caught at the moment of writing.
152
+ This audit catches structural and coverage gaps that validate doesn't check.
@@ -0,0 +1,151 @@
1
+ ---
2
+ name: kickoff
3
+ description: Start-of-session context loader for TOGA Technology projects. Run this at the BEGINNING of a coding session to prime Claude with the right knowledge before writing any code. It asks what you're working on (framework 1.0/2.0/both, front/back/hybrid, which repo/project, which client) and loads the matching coding standards, framework-core architecture, the repo's knowledge, and relevant client knowledge from the team `claude` knowledge base. Trigger whenever the user says "kickoff", "start a session", "prime me", "I'm starting work on X", or "load context for repo/client".
4
+ ---
5
+
6
+ # Kickoff — prime a coding session from the team knowledge base
7
+
8
+ Load the right knowledge before the developer starts coding. **Never assume missing
9
+ facts — ask.** Heuristics may pre-fill a *suggested default* in a question, but record
10
+ nothing without confirmation.
11
+
12
+ ## Core data model (this is the structure you maintain — same as `knowledge/CONVENTIONS.md`)
13
+
14
+ - The team knowledge lives in the **`claude` repo** under `knowledge/`.
15
+ - **Framework partitions everything:** `1.0/` (the `App_` framework, core repo `library`)
16
+ and `2.0/` (the `_underscore` framework, core repo `_underscore`).
17
+ - Within a framework: `apps/<repo>/architecture.md` + `apps/<repo>/features/*.md`, and
18
+ `standards/*.md`. App folders are keyed by **repo name** (e.g. `worker2`), not project name.
19
+ - Client-specific knowledge is shared at the top level: `clients/<client>/{profile.md,
20
+ features/, workflows/}`, each doc tagging its `framework`.
21
+ - `knowledge/registry.json` maps **repo ↔ project ↔ framework ↔ role(core|app) ↔ dependsOn**.
22
+ - Every repo implicitly depends on its framework's `core` repo; `dependsOn` adds more.
23
+
24
+ ## Step 1 — Resolve the team-repo (`claude`) path
25
+
26
+ You need this to read the registry and knowledge. Resolve in order; **persist the result
27
+ to Claude memory** so you never ask twice:
28
+
29
+ 1. **Claude memory** — a `reference` memory named `team-repo-path` (recalled memories
30
+ appear in your context). Use it if present and it contains `knowledge/INDEX.md`.
31
+ 2. **Env var** `CLAUDE_TEAM_REPO`.
32
+ 3. **Auto-discover** — probe `./claude`, `../claude`, `../../claude`, and walk up the tree
33
+ for a directory containing `knowledge/INDEX.md`.
34
+ 4. **Ask** the developer: "Where is the team `claude` repo checked out on this machine?"
35
+
36
+ When resolved, if there was no `team-repo-path` memory, **write one** (memory type
37
+ `reference`) and add its `MEMORY.md` pointer. From here on call the helper as
38
+ `node "<TEAM_REPO>/knowledge.js" <command>`.
39
+
40
+ ## Step 2 — Interview the developer (ask the work questions FIRST)
41
+
42
+ Ask these in one message. **Present the repos/projects you already know** (read
43
+ `registry.json`) so the developer can just pick:
44
+
45
+ 1. **Framework** — 1.0, 2.0, or **both**?
46
+ 2. **Layer** — front-end, back-end, or hybrid?
47
+ 3. **Repo / project** — list the registered repos for the chosen framework(s) as options
48
+ (show `repo` and `project`), plus **"a new repo not listed"**. If they pick the new-repo
49
+ option, run **New-repo onboarding** (below) before continuing.
50
+ 4. **Client** — which client is this for, or "shared / internal"? (List `clients/` folders
51
+ you know of, plus "a new client".)
52
+ 5. **What are you building or changing?** (a sentence — used to pick relevant feature docs.)
53
+
54
+ If any answer is unclear, ask again. Do not guess the framework, repo, project, or client.
55
+
56
+ ## Step 3 — Resolve the repos this work needs (local paths, lazily)
57
+
58
+ Compute the load set with `node "<TEAM_REPO>/knowledge.js" deps --repo=<chosen-repo>`
59
+ (returns the framework core + chosen repo + transitive `dependsOn`, in load order). For
60
+ **both** frameworks, run it for each chosen repo and union the results.
61
+
62
+ For **each** repo in the load set, get its local path so you can navigate the actual code:
63
+ - Look for a Claude `reference` memory named `repo-path-<repo>` (e.g. `repo-path-worker2`).
64
+ - If absent, **ask**: "What is the path to the `<repo>` repository?" Validate it exists,
65
+ then **write a `repo-path-<repo>` memory** (+ MEMORY.md pointer).
66
+
67
+ Never ask for a repo the session doesn't touch.
68
+
69
+ ## Step 4 — Load the knowledge
70
+
71
+ Read and internalize, in this order:
72
+ 1. **Framework core architecture** — `<TEAM_REPO>/knowledge/<fw>/apps/<core-repo>/architecture.md`
73
+ (e.g. `2.0/apps/_underscore/architecture.md`). Always load this first.
74
+ 2. **The chosen repo's** `architecture.md`, then feature docs matched to the description:
75
+ `node "<TEAM_REPO>/knowledge.js" search --framework=<fw> --repo=<repo> --q=<keywords>`.
76
+ 3. **Coding standards** for the framework(s): `<fw>/standards/backend-php.md` and/or
77
+ `frontend.md` per the layer answer (plus any others that exist). Load whichever exist.
78
+ 4. **Client knowledge** (if a real client): `clients/<client>/profile.md` and that client's
79
+ `features/`/`workflows/` filtered to the selected framework(s)
80
+ (`search --client=<client> --framework=<fw>`).
81
+
82
+ For **both** frameworks, union across `1.0/` and `2.0/`.
83
+
84
+ **Do NOT create or modify any `CLAUDE.md` stub** — kickoff only reads. A blank session is a
85
+ valid choice; this skill is opt-in.
86
+
87
+ ## Step 5 — Summarize and confirm
88
+
89
+ Tell the developer concisely:
90
+ - Which framework(s), repo(s), and client are in scope, and the local path of each repo.
91
+ - Which knowledge docs you loaded (by title). Where a repo/standard has **no knowledge yet**,
92
+ say so explicitly: "No knowledge captured yet for X — `capture` will build it as you work."
93
+ - Confirm you're primed and ready for their first task.
94
+
95
+ ## New-repo onboarding (when the developer names a repo not in `registry.json`)
96
+
97
+ Ask **all** of the following — assume nothing (you may offer a suggested default, e.g.
98
+ framework based on whether the path is under a 1.0 or 2.0 tree, but require confirmation):
99
+
100
+ 1. **Repo name** — the exact on-disk folder/repo name.
101
+ 2. **Framework** — 1.0 or 2.0.
102
+ 3. **Project name** — the logical name.
103
+ 4. **Role** — framework **core** or regular **app**?
104
+ 5. **dependsOn** — any other repos it depends on beyond the framework core? (default none)
105
+ 6. **Local path** on this machine (if not already remembered).
106
+
107
+ Then: append the entry to `<TEAM_REPO>/knowledge/registry.json`, write a `repo-path-<repo>`
108
+ memory, run `node "<TEAM_REPO>/knowledge.js" validate` (fix any error before continuing),
109
+ and proceed. The `<fw>/apps/<repo>/` folder is created later by `capture` when the first
110
+ doc is written — an empty knowledge set is fine; just note it.
111
+
112
+ ---
113
+
114
+ ### Claude Code Hooks Integration
115
+
116
+ This repo has a `PostToolUse` hook configured in `.claude/settings.json` that
117
+ **automatically runs `node knowledge.js validate`** whenever any knowledge file is
118
+ written or edited. This means:
119
+
120
+ - You do NOT need to run `validate` manually after a `capture` session write — you
121
+ will see validation results inline in the terminal as Claude writes each file.
122
+ - If an error appears in the hook output (lines starting with `ERROR:`), the capture
123
+ step that caused it should be corrected before continuing.
124
+ - The hook output is condensed: it shows only `ERROR`, `WARNING`, `OK`/`FAILED`
125
+ lines, so it does not flood the terminal.
126
+
127
+ This hook is active whenever you have this repo open in Claude Code. It does not
128
+ run in other project repos (it is scoped to the `claude` repo's `.claude/settings.json`).
129
+
130
+ ---
131
+
132
+ ### Before kickoff: should you run /session-resume first?
133
+
134
+ If the developer says any of the following, suggest they run `/session-resume` **before**
135
+ this kickoff to reload their prior session context:
136
+
137
+ - "I'm continuing from yesterday"
138
+ - "I'm picking up where I left off"
139
+ - "I was in the middle of something"
140
+ - "I saved my session last time"
141
+ - Any mention of a prior unfinished feature or task
142
+
143
+ In that case, say: "It sounds like you're continuing prior work. If you saved your
144
+ session with `/session-save`, run `/session-resume latest` first — it will reload
145
+ your exact state (what worked, what failed, and your next step) before we prime
146
+ framework context with kickoff. This prevents you from re-trying approaches that
147
+ already failed."
148
+
149
+ If they have already run `/session-resume` and are now running `/kickoff`, proceed
150
+ normally — the session context they loaded will complement the framework knowledge
151
+ loaded here.
@@ -0,0 +1,296 @@
1
+ ---
2
+ name: php-patterns
3
+ description: PHP pattern reference for TOGA Technology projects covering both frameworks. Framework 1.0 (App_ prefix), Framework 2.0 (_underscore prefix), PDO prepared statements, session handling, caching, api2 envelope format. Trigger when the user says "php-patterns", "show me the pattern for X", "how do I do X in 1.0/2.0", or starts writing new PHP code.
4
+ ---
5
+
6
+ # PHP Patterns Skill
7
+
8
+ Reference skill for TOGA Technology PHP patterns across both frameworks. Load this skill when writing new PHP code, reviewing patterns, or onboarding to a repo.
9
+
10
+ ## When to invoke
11
+
12
+ - `/php-patterns` — general reference for both frameworks
13
+ - `/php-patterns 1.0` — Framework 1.0 (App_) specific patterns
14
+ - `/php-patterns 2.0` — Framework 2.0 (_underscore) specific patterns
15
+ - `/php-patterns pdo` — PDO/database patterns
16
+ - `/php-patterns queue` — queue/worker dispatch patterns
17
+ - `/php-patterns api` — API response patterns
18
+
19
+ ---
20
+
21
+ ## Framework 1.0 (App_) Patterns
22
+
23
+ ### Controller structure
24
+
25
+ ```php
26
+ <?php
27
+
28
+ class App_Controller_Orders extends App_Controller {
29
+
30
+ public function indexAction(): void {
31
+ $model = App_Registry::get('Order');
32
+ $orders = $model->getAll(['user_id' => $this->auth->getUserId()]);
33
+ $this->view->assign('orders', $orders);
34
+ $this->view->render('orders/index');
35
+ }
36
+
37
+ public function createAction(): void {
38
+ if (!$this->request->isPost()) {
39
+ $this->response->error('Method not allowed', 405);
40
+ return;
41
+ }
42
+
43
+ $params = $this->request->post(['user_id', 'items', 'address_id']);
44
+
45
+ try {
46
+ $order = App_Registry::get('Order')->create($params);
47
+ } catch (App_Exception_Validation $e) {
48
+ $this->response->error($e->getMessage(), 400);
49
+ return;
50
+ }
51
+
52
+ $this->response->success(['order_id' => $order->id]);
53
+ }
54
+ }
55
+ ```
56
+
57
+ Key points:
58
+ - Get models via `App_Registry::get('ModelName')` — never `new App_Model_*`
59
+ - Filter request input explicitly: `$this->request->post(['key1', 'key2'])`
60
+ - Return early on errors; happy path is last
61
+ - Catch specific exception types, not bare `Exception`
62
+
63
+ ### Model structure
64
+
65
+ ```php
66
+ <?php
67
+
68
+ class App_Model_Order extends App_Model {
69
+
70
+ protected string $table = 'orders';
71
+
72
+ public function getByUser(int $userId): array {
73
+ if ($userId <= 0) {
74
+ return [];
75
+ }
76
+ return $this->db->select($this->table, ['user_id' => $userId]);
77
+ }
78
+
79
+ public function create(array $params): object {
80
+ $this->validate($params); // throws App_Exception_Validation on failure
81
+ $id = $this->db->insert($this->table, [
82
+ 'user_id' => (int) $params['user_id'],
83
+ 'address_id' => (int) $params['address_id'],
84
+ 'status' => 'pending',
85
+ 'created_at' => date('Y-m-d H:i:s'),
86
+ ]);
87
+ return $this->db->find($this->table, $id);
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### Cron/scheduled task pattern (1.0)
93
+
94
+ ```php
95
+ <?php
96
+ // crons/ProcessPendingOrders.php
97
+
98
+ class App_Cron_ProcessPendingOrders extends App_Cron {
99
+
100
+ public function run(): void {
101
+ $model = App_Registry::get('Order');
102
+ $pending = $model->getPending(100); // always LIMIT — never unbounded
103
+
104
+ foreach ($pending as $order) {
105
+ try {
106
+ $model->process($order->id);
107
+ } catch (App_Exception_Processing $e) {
108
+ error_log('Cron: failed to process order ' . $order->id . ': ' . $e->getMessage());
109
+ // continue to next — do not let one failure abort the batch
110
+ }
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Framework 2.0 (_underscore) Patterns
119
+
120
+ ### Worker structure
121
+
122
+ ```php
123
+ <?php
124
+
125
+ class _Worker_ProcessOrder extends _Worker {
126
+
127
+ public function run(): void {
128
+ $orderId = (int) ($this->payload['order_id'] ?? 0);
129
+
130
+ if ($orderId <= 0) {
131
+ error_log('_Worker_ProcessOrder: missing order_id in payload');
132
+ return; // discard — bad payload, no retry
133
+ }
134
+
135
+ try {
136
+ _Model_Order::process($orderId);
137
+ } catch (_Exception_OrderNotFound $e) {
138
+ error_log('_Worker_ProcessOrder: order not found: ' . $orderId);
139
+ return; // discard
140
+ } catch (_Exception_Database $e) {
141
+ error_log('_Worker_ProcessOrder: DB error: ' . $e->getMessage());
142
+ throw $e; // re-throw — queue will retry
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ ### Worker dispatch
149
+
150
+ ```php
151
+ // Dispatch a single job
152
+ _Queue::dispatch('_Worker_ProcessOrder', ['order_id' => $orderId]);
153
+
154
+ // Dispatch with delay (seconds)
155
+ _Queue::dispatch('_Worker_ProcessOrder', ['order_id' => $orderId], 30);
156
+
157
+ // Dispatch to a specific queue name
158
+ _Queue::dispatch('_Worker_ProcessOrder', $payload, 0, 'high-priority');
159
+ ```
160
+
161
+ Never call `$worker->run()` or `new _Worker_ProcessOrder()` directly in application code.
162
+
163
+ ### API controller structure (api2)
164
+
165
+ ```php
166
+ <?php
167
+
168
+ class _Controller_Orders extends _Controller {
169
+
170
+ public function getAction(): array {
171
+ $orderId = (int) ($this->request->get('id') ?? 0);
172
+
173
+ if ($orderId <= 0) {
174
+ return ['success' => false, 'data' => null, 'errors' => ['Invalid order ID']];
175
+ }
176
+
177
+ $order = _Model_Order::find($orderId);
178
+
179
+ if ($order === null) {
180
+ return ['success' => false, 'data' => null, 'errors' => ['Order not found']];
181
+ }
182
+
183
+ return ['success' => true, 'data' => $order->toArray(), 'errors' => []];
184
+ }
185
+
186
+ public function createAction(): array {
187
+ $params = $this->request->post(['customer_id', 'items']);
188
+
189
+ try {
190
+ $order = _Model_Order::create($params);
191
+ } catch (_Exception_Validation $e) {
192
+ return ['success' => false, 'data' => null, 'errors' => $e->getErrors()];
193
+ }
194
+
195
+ return ['success' => true, 'data' => ['order_id' => $order->id], 'errors' => []];
196
+ }
197
+ }
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Cross-Framework Patterns
203
+
204
+ ### PDO prepared statements
205
+
206
+ ```php
207
+ // SELECT with parameters
208
+ $stmt = $pdo->prepare('SELECT * FROM orders WHERE user_id = ? AND status = ?');
209
+ $stmt->execute([$userId, $status]);
210
+ $orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
211
+
212
+ // INSERT
213
+ $stmt = $pdo->prepare(
214
+ 'INSERT INTO orders (user_id, status, created_at) VALUES (?, ?, ?)'
215
+ );
216
+ $stmt->execute([$userId, 'pending', date('Y-m-d H:i:s')]);
217
+ $newId = $pdo->lastInsertId();
218
+
219
+ // UPDATE with WHERE (always include WHERE — never unbounded UPDATE)
220
+ $stmt = $pdo->prepare('UPDATE orders SET status = ? WHERE id = ?');
221
+ $stmt->execute([$newStatus, $orderId]);
222
+
223
+ // IN clause with variable number of params
224
+ $ids = [1, 2, 3, 4];
225
+ $placeholders = implode(',', array_fill(0, count($ids), '?'));
226
+ $stmt = $pdo->prepare("SELECT * FROM orders WHERE id IN ($placeholders)");
227
+ $stmt->execute($ids);
228
+ ```
229
+
230
+ ### Session handling
231
+
232
+ ```php
233
+ // Start session safely
234
+ if (session_status() === PHP_SESSION_NONE) {
235
+ session_start();
236
+ }
237
+
238
+ // Regenerate after login (prevents session fixation)
239
+ session_regenerate_id(true);
240
+
241
+ // Store only IDs in session — never full objects
242
+ $_SESSION['user_id'] = $user->id;
243
+
244
+ // Access session value safely
245
+ $userId = (int) ($_SESSION['user_id'] ?? 0);
246
+ ```
247
+
248
+ ### Caching pattern
249
+
250
+ ```php
251
+ // Check cache before hitting DB
252
+ $cacheKey = 'user_profile_' . $userId;
253
+ $profile = _Cache::get($cacheKey);
254
+
255
+ if ($profile === null) {
256
+ $profile = _Model_User::getProfile($userId);
257
+ _Cache::set($cacheKey, $profile, 300); // TTL in seconds
258
+ }
259
+
260
+ // Invalidate on write
261
+ _Model_User::updateProfile($userId, $data);
262
+ _Cache::delete('user_profile_' . $userId);
263
+ ```
264
+
265
+ ### Logging pattern
266
+
267
+ ```php
268
+ // Log with context — never log sensitive fields
269
+ error_log(sprintf(
270
+ '[%s] Order %d processing failed: %s',
271
+ date('Y-m-d H:i:s'),
272
+ $orderId,
273
+ $e->getMessage()
274
+ ));
275
+
276
+ // Use log levels if the framework provides them
277
+ _Log::error('Order processing failed', ['order_id' => $orderId, 'error' => $e->getMessage()]);
278
+ // Do NOT include: passwords, tokens, card numbers, PII
279
+ ```
280
+
281
+ ### Input validation helper
282
+
283
+ ```php
284
+ // Validate and sanitize common input types
285
+ $id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
286
+ $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL) ?: '';
287
+ $page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT,
288
+ ['options' => ['min_range' => 1, 'max_range' => 1000],
289
+ 'flags' => FILTER_NULL_ON_FAILURE]) ?? 1;
290
+
291
+ // Allowlist for enum-like inputs
292
+ $allowed_statuses = ['pending', 'processing', 'complete', 'cancelled'];
293
+ $status = in_array($_GET['status'] ?? '', $allowed_statuses, true)
294
+ ? $_GET['status']
295
+ : 'pending';
296
+ ```