# CLAUDE.md

Guidance for Claude Code (claude.ai/code) working in this repository. Read this before making changes.

## Project Overview

Afaq is a legacy **Laravel 4.1** library-management application (ILS). It targets **PHP 5.6 and PHP 7.4** (both must keep working) and runs on IIS, Apache/Laragon (Windows), and LAMP (AlmaLinux/CentOS/RHEL). It is **not** a modern Laravel project — do not assume Laravel 5+ conventions (no Eloquent `HasMany` attributes, no `Kernel.php`, no `artisan make:*` scaffolding, no PSR-4 autoload for app code).

## Invariants

These rules are non-negotiable. Violate any of them and the app breaks on one of the supported environments.

- **Dual PHP support.** Every change must keep working on both PHP 5.6 and PHP 7.4. See [Switching PHP 5.6 ↔ 7.4](#switching-php-56--74).
- **Composer does not run.** `composer install`, `composer update`, `composer dump-autoload` all fail on this project (Laravel 4.1 + several pinned deps are dead on Packagist; some vendor packages like `vendor/aws/` were added manually and aren't in `composer.json`). The committed `vendor/` directory **is** the source of truth.
- **Autoloading is hand-edited.** When you add a class under `app/services/`, add its entry to `vendor/composer/autoload_classmap.php` by hand and commit the change. The other classmap dirs (`app/controllers`, `app/models`, `app/commands`, `app/database/migrations`, `app/database/seeds`) are already covered by the generated classmap and do not need new entries for new files. This is an acknowledged hack until the composer situation is fixed.
- **Public web root is `public_html/`**, not `public/`. Hardcoded in `bootstrap/paths.php` and in the root `.htaccess` `RewriteRule`.
- **Domain spellings to preserve:** `Branche`, `Biblioitem`, `biblioitems`, `biblios`, `opac`, `Bmohsin` (vendor namespace for MARC21 — lives in `workbench/bmohsin/`). The cspell dictionary (`cspell.json`) is authoritative — add new project terms there, do not "fix" existing ones.
- **`bootstrap/cache/env.php` is generated.** Never hand-edit; never commit.

## Common Commands

Run all commands from the project root. Each comment is the canonical description; the command's `$description` in `app/commands/*.php` is kept in sync with it.

```bash
# List all registered artisan commands (core + custom)
php artisan list

# ---------- Cache / compiled ----------

# Flush application cache, then also clear compiled views, session files, and bootstrap/cache/env.php (.gitignore and .gitkeep are preserved). Overridden by ClearCacheCommandOverride.
php artisan cache:clear

# Laravel core — remove bootstrap/compiled.php.
php artisan clear-compiled

# Laravel core — regenerate bootstrap/compiled.php and class loader.
php artisan optimize

# ---------- Custom queue (NOT Laravel's built-in queues) ----------

# Execute the next job in the custom queue. Run once (cron/Task Scheduler) or stay resident with --daemon. See docs/queue-daemon.md.
php artisan queue:run

# Long-running variant of queue:run: stays resident and drains the custom queue continuously, exiting cleanly on a memory/job/time ceiling so a supervisor (systemd/NSSM) restarts it. Configured in app/config/queue_daemon.php. See docs/queue-daemon.md.
php artisan queue:run --daemon

# Empty the custom queue: delete all rows from the jobs and queues tables.
php artisan queue:reset

# Delete all log files under storage/logs.
php artisan log:reset

# ---------- System jobs ----------

# Interactive menu to dispatch system jobs: system statistics, late-issue notifications, long-running jobs, or database backups. See docs/system-statistics.md.
php artisan app:jobs

# ---------- SIP2 ----------

# Long-running SIP2 socket server. Listens on the port/IP configured in app/config/sip2.php and parses incoming SIP2 protocol messages. See docs/sip2.md.
php artisan sip2:serve

# ---------- Database ops ----------

# Export the full database (schema + data) as a backup. See docs/automatic-database-backup-guide.md.
php artisan app:start_database_export

# Check, repair, and optimize tables when corrupted or experiencing issues.
php artisan app:repair_database

# MOD Only: mark a database as read-write and switch the app to use it as the main connection.
php artisan app:recover_database

# MOD Only: set up or refresh replication by copying the master DB to a slave and updating app config to read from it.
php artisan app:setup_replication

# MOD Only: execute the MOD-specific failover-switch procedure.
php artisan app:start_db_failover

# List all configured DB connections and their connection status. Useful before setting up replication or for troubleshooting.
php artisan app:list_connections

# Reverse-engineer Schema-Builder migration files from an existing MySQL database. Not perfect; output usually needs manual cleanup.
php artisan db:generate-migrations

# Custom data-migration between source_db and target_db (users, borrowers, biblioitems, items, and related tables). Temporary until we return to the official laravel migrate.
php artisan migrate:data

# ---------- Other utilities ----------

# Export all biblioitems; one Excel file per branche.
php artisan app:export_biblioitems

# Interactive menu of correction loops for one-off data-fix jobs (generate search values, refresh biblioitems cache, dedupe borrowers/items, MOE imports, etc.).
php artisan app:loops

# Send email notification to all borrowers with late items. Will soon be deprecated.
php artisan app:start_late_item_notification

# Create dummy data, dispatch all email types, execute queues, then cleanup. Check your inbox for results.
php artisan app:test_all_emails

# Merge one branch into another: dedupes borrowers, then reassigns rows from old branch to new across items, borrowers, users, item_issues, biblioitems, and biblioitem_values.
php artisan branch:update

# Compare translation keys per file between two locales and generate only files with missing keys. Each missing key includes the counterpart value from the other locale.
php artisan lang:diff
```

**Do not run:** `composer install`, `composer update`, `composer dump-autoload`, `vendor/bin/phpunit` (PHPUnit is not installed — `app/tests/` is unused Laravel 4 scaffolding), or any `artisan make:*` command (Laravel 4 doesn't ship them). See [Invariants](#invariants).

**When citing a custom command** in docs or scripts, grep the `protected $name` property in `app/commands/*.php` — several existing files in `docs/` reference command names that don't match the registered ones (e.g. "app:refresh", "app:boot_statistics", "app:sip2_socket_server" — none exist).

## Environment & Configuration

- `.env` is loaded by a **custom** loader in `bootstrap/load_env.php` (called from `bootstrap/start.php`). It is **not** vlucas/phpdotenv — no quoting tricks, no nested var expansion.
- A compiled cache is written to `bootstrap/cache/env.php`; **delete it** (or `php artisan cache:clear`) after editing `.env` on a stale-mtime system. The same loader is reused by `reloadLaravelEnv()` in `app/Helpers/general.php`.
- Config files live in `app/config/` (see `docs/configurable-settings-guide.md`). Database connections (master/slaves, disaster, sqlite/pgsql/sqlsrv) are in `app/config/database.php` and fed by env vars `MASTER_*`, `SLAVE_1_*`, `SLAVE_2_*`, `DISASTER_*`.

## Switching PHP 5.6 ↔ 7.4

Manual vendor swap, documented in `docs/php-version.md`. When moving between versions:

1. In `vendor/filp/whoops/src/`, rename `Whoops` ↔ `Whoops_php56` / `Whoops_php74` so the active folder is named `Whoops`.
2. In `vendor/laravel/framework/src/Illuminate/Exception/WhoopsDisplayer.php` line 43, toggle the signature between `display($exception)` (PHP 7.4) and `display(Exception $exception)` (PHP 5.6).

Never commit changes that only work on one of the two versions without checking the other.

## High-Level Architecture

### Request flow

1. Apache/IIS rewrites to `public_html/index.php`.
2. `bootstrap/start.php` loads `.env` (via `bootstrap/load_env.php`), then boots Laravel 4's `Illuminate\Foundation\Application`.
3. **`app/routes.php`** is the monolithic router (hundreds of routes). It is organized in large `Route::group` blocks keyed by `before` filter + `prefix`:
    - `admin_ui_prefix()` / `borrower_ui_prefix()` — configurable UI prefixes (helpers in `app/Helpers/general.php`; renamable per `docs/dir-structure.md`).
    - `opac` prefix — public catalog.
    - `api_v1` prefix — mobile API (`ApiController`), gated by `api_filter`.
    - `webhooks` before-filter — provider webhooks dispatched through `Services\Helpers\WebhookService`.
    - Feature routes are conditionally registered behind `get_settings(...)` flags (e.g. `sip2_services_enabled`).
    - `execute_next_queue` route (at `app/routes.php:46`) is an HTTP entry point for the custom queue.
4. Controllers in `app/controllers/` — grouped by area directories: `Admin/`, `Borrower/`, `Opac/`, `Search/`, `Form/`, `Reservations/`, `Suggestion/`, `Chat/`. Top-level controllers include `CirculationController`, `ApiController`, `SingleSignOnController`, `AuthController`, `HomeController`, `BaseController`, `EmailArchiveController`, `EnvSwitchController`, `FileAccessController`, `FileController`, `LicenseController`, `MoePortalImportController`, `ApiRopController`.
5. Views render through **teepluss/theme** at `public_html/themes/{borrower,chat,default,form_preview,forms,global_partials,main,opac,reservation,search,svg,vendor}/`. Each theme has `layouts/`, `partials/`, `views/`, `assets/`, `config.php`. Prefer `Theme::partial(...)` / `Theme::of(...)->render()` over plain `View::make`.

### Domain services (`app/services/helpers/`)

Centralized business logic — check here before adding logic to controllers:

- **`CirculationService.php`** — single entry point for checkin/checkout with all rules (max issues, fine cap, holds, issue rules). Usage pattern in `docs/code-base-docs.md`.
- **`MARC21.php` / `MARC21_legacy.php`** — MARC record parsing/building (cataloging). New work uses `MARC21.php`.
- **`Search.php`** — search orchestration (FULLTEXT requires `ft_min_word_len=2` / `innodb_ft_min_token_size=2` in `my.ini`).
- **`Sip2/`** (subdirectory) — SIP2 protocol (self-checkout machines): `Sip2Message`, `Sip2LogBuilder`, `Checkin`, `Checkout`, `ItemInformation`, `PatronInformation`, `PatronStatus`, `Login`, `Renew`, `RenewAll`, `FeePaid`, `ScStatus`, `FallbackSip2Message`.
- **`YAZ.php` + `Z39/`** (subdirectory: `Client`, `Record`, `ScanResponse`, `SearchRetrieveResponse`) — Z39.50 library protocol integration (YAZ PECL extension, PHP 7.4 only — see `docs/php-version.md`).
- **`AuditLogger.php`** — audit trail (helpers in `app/Helpers/audit_logger.php` and `app/Helpers/audit_log_view_helpers.php`).
- **`QueueManager.php` + `Jobs/`** (subdirectory) — custom queue system: `BuildSystemStatistics`, `DatabaseBackup`, `ImportBiblioitemsFromExcel`, `ImportMoeEmpAsStd`, `ImportMoeOmanEmp`, `ImportMoeOmanStd`, `ImportMoeOmanTeacher`, `LateIssueNotificationJob`, `LongRunningJobsProcessor`, `SendEmailJob`. Driven by the `execute_next_queue` route and `QueueWorker` command (**not** Laravel's built-in queues). `QueueWorker` runs as a one-shot by default or as a resident daemon with `queue:run --daemon` (config in `app/config/queue_daemon.php`, runbook in `docs/queue-daemon.md`); the daemon also calls `QueueManager::reapStuckJobs()` to recover jobs left in `processing` by a killed worker.
- **`SystemStatistics.php`** — computes aggregate counters listed in `app/config/system_statistics.php`; per-branch vs global toggled by a boolean.
- **`WebhookService.php`** — Mailgun and other webhook dispatch (HMAC-verified).
- **`EmailLogBuilder.php`, `OTF.php`, `InventoryHelper.php`, `SuggestionService.php`, `CacheHelpers.php`, `BiblioitemExport.php`, `Helpers.php`** — single-responsibility helpers.

### Helper files (`app/Helpers/`)

Flat PHP files autoloaded globally, grouped by domain: `afaq_api`, `afaq_sso`, `audit_log_view_helpers`, `audit_logger`, `auth`, `biblioitem_value`, `borrowers`, `circulation`, `correction_loops`, `emails`, `excel_exports`, `files_system`, `general`, `indexing_settings`, `issues`, `license`, `marc`, `migration`, `moe_import_functions`, `reservations`, `search_engine`, `settings`, `sip2`, `temp`, `yaz`. These define global functions (e.g. `get_settings`, `admin_ui_prefix`, `borrower_ui_prefix`, `reloadLaravelEnv`, `show_all_email_templates`) used throughout views, routes, and controllers.

### Custom Artisan commands (`app/commands/`)

Registered in `app/start/artisan.php`. Full list of registered custom commands:

- `QueueWorker` (supervisor target; `--daemon` flag for resident mode) → `queue:run`
- `AfaqLateNotificationScheduler` → `app:start_late_item_notification`
- `FlushQueue` → `queue:reset`
- `FlushLog` → `log:reset`
- `BootSystemJobs` → `app:jobs`
- `Sip2SocketServer` → `sip2:serve`
- `AfaqTestAllEmailsCommand` → `app:test_all_emails`
- `UpdateBranchDataCommand` → `branch:update`
- `MigrateData` → `migrate:data`
- `CompareLang` → `lang:diff`
- `GenerateMigrationsFromDatabase` → `db:generate-migrations`
- `exportBiblioitemsInExcelFile` → `app:export_biblioitems`
- `CorrectionLoops` → `app:loops`
- `RepairDatabase` → `app:repair_database`
- `StartDatabaseBackup` → `app:start_database_export`
- `SetupReplication` → `app:setup_replication`
- `RecoverDatabase` → `app:recover_database`
- `databaseReplicationFailover` → `app:start_db_failover`
- `listDBConnections` → `app:list_connections`
- `ClearCacheCommandOverride` → rebinds `cache:clear` via `App::bind('command.cache.clear', ...)` (not `Artisan::add`).

Legacy/deprecated variants live in `app/commands/legacy/` — do not register new ones from there.

### Models (`app/models/`)

Flat Eloquent-based models (classmap, not PSR-4). Some extend `LaravelBook\Ardent` for validation. Core domain models: `Biblioitem` (cataloged bibliographic item), `Item` (physical copy), `Borrower` (patron), `Issue` (loan), `Reservation`, `Branche` (library branch — Afaq spelling, **not** `Branch`), `Classification`, `Authority`, `BiblioitemValue` (MARC field values), `Custody` (acquisitions), `BorrowerFine`, `AfaqQueue` / `AfaqJob` (custom queue tables), `SystemStatistic`.

### Multi-DB / replication

Master + up to two slaves + disaster DB, all configured via `.env` (`MASTER_*`, `SLAVE_1_*`, `SLAVE_2_*`, `DISASTER_*`). Failover is manual via `php artisan app:start_db_failover`, `app:recover_database`, `app:setup_replication`, `app:list_connections`. Schema changes go through migrations in `app/database/migrations/` **and** must be reflected in the slave-setup commands.

## Working Conventions

Core values: **security, clarity, readability, maintainability**. When these conflict with cleverness, pick them.

### Task workflow

- **Non-trivial tasks** (more than a localized edit): before writing code, post a short plan — bulleted steps, risks/assumptions, files you'll touch — and wait for confirmation. Trivial edits (typo fix, rename a variable, tighten a log message) can proceed without this ceremony.
- **Ambiguous requirements:** ask a clarifying question *and* propose a best-guess plan that can be confirmed. Do not silently guess sensitive details (auth rules, table names, business logic) — if you must proceed, state the assumption first.

### Code style

- Readable over clever. Prefer clean, obvious code over one-liners that need re-parsing.
- Keep methods small and focused.
- Avoid over-abstraction — this is a legacy codebase; three similar lines are better than a new abstraction.
- New standalone functions go in one of the files in `app/Helpers/` (grouped by domain, not a new file unless the domain is new).
- **Use the short array syntax `[]`, not `array()`**, in new and edited code. Don't sweep existing `array()` files just to convert them — only change what you're already touching.

### Config & secrets

These apply to **new code and the config blocks you're already editing** — do not go through existing config files converting them on your own.

- **Read configurable/environment-specific values from `.env` via `env('KEY', 'default')`**, with a sensible default as the second argument when one makes sense. Don't hand-roll `getenv(...) !== false ? ... : ...` ternaries — the project's `env()` helper (in `bootstrap/load_env.php`) already returns the default when the variable is missing **or** blank, reads `$_ENV` first for SAPI safety, and coerces `true`/`false`/`null`.
- **Never write credentials or secrets directly into a config file.** DB passwords, API keys, tokens, bucket names, host/user/password trios, etc. belong in `.env` and are referenced with `env(...)`. Add the new key to `.env.example` (and `.env.docker.example` if Docker-relevant) so other environments know it exists.
- When adding a new env key, keep the name consistent with the existing `*_HOST` / `*_USER` / `*_PASSWORD` / `MASTER_*` / `SLAVE_*` conventions.

### Comments

- Default: **no comment**. Well-named identifiers are the documentation.
- Add a comment only when the *why* is non-obvious (hidden constraint, workaround for a specific bug, surprising behavior).
- Never add obvious comments (`# loop users`, `# set variable`).
- Single-line: `//` or `#`. Multi-line: `/* ... */`. Do not use `/** */` docblocks unless an existing surrounding block already uses them.
- **Comments are always written in English** — even when the task, the prompt, or the surrounding user-facing strings are in another language (French, Arabic, etc.). The request language never changes the comment language. This applies to code comments and commit messages; it does **not** apply to user-facing strings, which still live in `app/lang/` per [Localization](#localization).

### Security (non-negotiable)

- **Server-side validation and sanitization is the security boundary.** Client-side validation is UX only — never rely on it.
- Never trust request data from users. Treat `Input::*`, route parameters, headers, and file uploads as hostile until validated.
- Use parameter binding (`DB::select(..., [$id])`, Eloquent, or `?` placeholders) — **never** concatenate user input into SQL. This codebase has pre-existing raw `UPDATE ... REPLACE(col, '$x', '$y')` patterns that are unsafe; do not add more.
- Enforce authorization checks in the controller action, not only in views. A hidden button is not an access control.
- CSRF: follow the existing project pattern (`Form::token()` in views, verified by the relevant `before` filter). Do not invent a new scheme.
- **Never commit `*.sql` files.** Root `.gitignore` already excludes them, and SQL dumps typically contain real borrower/user data. If you need to save SQL content (a query you're iterating on, an ad-hoc dump, a repro script), put it in the `tmp/` folder at the project root **with a non-`.sql` extension** (e.g. `tmp/repro.txt`, `tmp/migration-draft.txt`). The `tmp/` folder has its own `.gitignore` that excludes everything inside, so files there will never be tracked. If a user asks you to save SQL somewhere, redirect to `tmp/` with a `.txt` name — do not override the extension rule.

### Blade and views (Laravel 4.1 specifics)

Laravel 4.1's Blade has quirks that don't match modern Laravel tutorials:

- **Echo syntax:**
  - `{{ $var }}` — **raw** output (NOT escaped, unlike Laravel 5+).
  - `{{{ $var }}}` — **HTML-escaped** output. Use this for any user-controlled data.
  - `{!! $var !!}` — technically supported (Laravel 4.1.29+) but **do not introduce new uses**; stick with `{{ }}` / `{{{ }}}`.
- **`@include` must be on a single line.** The Blade parser in 4.1 does not handle multi-line `@include(...)`. Put the prettier-ignore directive on the line immediately before every `@include`:
  ```blade
  <!-- prettier-ignore -->
  @include('path.to.partial', ['key' => $value])
  ```
- **`@php ... @endphp` is NOT supported** in Laravel 4.1. Use `<?php ... ?>` inside a Blade file if you must inline PHP (and usually you shouldn't — move the logic to a helper or controller).
- **Partials:** create one when markup repeats across pages (alerts, form fields, nav rows, table rows) or a component is complex enough to benefit from isolation. Don't create a partial for a trivial single-use block.

### Laravel 4.1 framework API differences

The Eloquent/Collection/Support API predates Laravel 5+. Methods you'd reach for from modern tutorials are often named differently or absent — using the 5+ name throws `Call to undefined method`. Known gotchas:

- **Collection: use `->lists('col')`, not `->pluck('col')`.** In 4.1, `Collection::lists($value [, $key])` returns the plain array of values (the equivalent of 5+ `pluck()->all()`). `pluck()` does **not** exist on the collection. (`Arr`/query-builder `lists()` behave the same way.)
- When unsure whether a collection/query helper exists in 4.1, check `vendor/laravel/framework/src/Illuminate/Support/Collection.php` (and `Database/Eloquent/Collection.php`) before using it — the committed `vendor/` is the source of truth.

### Rendering HTML from the backend / returning HTML to JS

- **Never concatenate HTML tag strings** in PHP (`"<div class='" . $x . "'>"`). It's unreadable and a stepping stone to XSS bugs.
- Render HTML through a Blade partial with variables, capture the rendered output, and return that string. Example pattern:
  ```php
  $html = View::make('path.to.partial', ['rows' => $rows])->render();
  return Response::json(['html' => $html]);
  ```
- The receiving JS inserts `response.html` into the DOM — no string-building on the PHP side.

### Icons

- **Default to FontAwesome** for any new icon. The admin (`main`) theme loads it in `public_html/themes/main/partials/metadata.blade.php` from `themes/opac/assets/fontawesome-5.15/css/all.css` — the folder is named `fontawesome-5.15` but actually ships **Font Awesome Pro 6.7.2**.
- Use the classic syntax the rest of the codebase uses: `<i class="fa fa-user"></i>`. The `.fa` base class is defined (defaults to the solid weight), so `fa fa-<name>` renders without `fas`/`far`/`fab`.
- **Use FA6 icon names, not v4 names.** Some v4 aliases still resolve (`fa-sign-in`, `fa-sign-out`, `fa-repeat`) but others were dropped — e.g. `fa-clock-o` → `fa-clock`, `fa-file-excel-o` → `fa-file-excel`. When unsure, grep the loaded `all.css` for `^\.fa-<name> {` before using it.
- Do **not** introduce new `glyphicon`/Bootstrap or `ionicon` icons; replace them with FontAwesome when you touch the markup.

### Localization

- **All user-facing strings live in `app/lang/`** (locales: `ar/`, `en/` — Arabic and English only).
- Views and controllers reference translations (`Lang::get('key')` / `trans('key')` / `@lang('key')`), never hard-coded user-facing text.
- When adding a new key, add it to **both** `ar/` and `en/` in the same PR. Use `php artisan lang:diff` to find drift.

### Item / inventory statuses

- The set of valid statuses for the `items` table and the inventory items table is **unified and standardized** in `app/config/inventory.php` under the `status` key.
- **Never hard-code a status list** (in a controller, view, validator, or seed). Always read it from `\Config::get('inventory.status')` so a single source of truth drives every dropdown, validation rule, and report.
- Translations for each status live in `app/lang/{ar,en}/inventories.php` keyed by the status string.
- Example (Blade dropdown):
  ```blade
  <select>
      @foreach (\Config::get('inventory.status') as $status)
          <option value="{{ $status }}">{{ trans('inventories.' . $status) }}</option>
      @endforeach
  </select>
  ```

### Project-specific routing / layout reminders

- **OPAC / visitor views:** `public_html/themes/opac/`. **Admin dashboard views:** `public_html/themes/main/`.
- **Before adding logic to a controller**, check if there's a matching service in `app/services/helpers/` or helper file in `app/Helpers/`. Controllers should orchestrate, not implement domain rules.
- **Route files are organized by UI area + filter, not by controller.** Adding routes: find the right `Route::group` and add inside — don't create new groups unless you're adding a new UI surface.
- **Testing/debugging routes** live at the top of `app/routes.php` between the `# Testing Routes` banners (lines 18 and 38). Guard destructive ones with `exit()`; several existing ones do.
- **Command `$description`** must stay in sync with the comment above the command in this file's [Common Commands](#common-commands) block. When changing one, change both.

### Git commits

Stage and commit related files into a single, well-formatted commit.

**Rules**

1. **Group by feature/fix.** Only commit files whose edits belong to the *same* feature or hotfix. If the working tree has unrelated changes, stage and commit only the files that belong together — leave the rest for a separate commit. When in doubt, run `git status` and `git diff` to inspect what changed and decide the grouping.
2. **One commit per logical change.** All files for the same feature or hotfix go into one commit.
3. **Short summary.** The message is a concise, imperative summary of the change — no fluff.
4. **NEVER add a co-author or attribution.** Do **not** include `Co-Authored-By: Claude` (or any co-author trailer), "Generated with Claude Code", or similar. The message contains only the description of the change.

**Message format** — conventional-commit prefix that matches the change, as `<prefix>: <short summary>`:

- `feat:` — a new feature
- `hotfix:` — an urgent fix to production
- `fix:` — a regular bug fix
- `refactor:` — code restructuring without behavior change
- `docs:` — documentation only
- `style:` — formatting, no code change
- `test:` — adding or fixing tests
- `chore:` — tooling, config, deps, housekeeping
- `perf:` — performance improvement

Examples: `feat: add user login with email and password` · `hotfix: fix crash when cart total is zero` · `fix: correct date formatting on invoices`

**Steps**

1. Run `git status` and `git diff` (and `git diff --staged`) to see all changes.
2. Identify the files that belong to the same feature or hotfix.
3. Stage only those files (`git add <files>`).
4. Write a short summary with the correct prefix.
5. Commit — with **no** co-author or Claude attribution.
6. Show the result with `git log -1 --stat`.

## Reference Docs (`docs/`)

Operational runbooks; check these before asking:

- `docs/dir-structure.md` — renaming `public_html`, UI prefixes.
- `docs/php-version.md` — PHP 5.6/7.4 switching + YAZ install.
- `docs/deployment.md` — mcrypt install, `my.ini` FULLTEXT settings.
- `docs/database-connections.md` — connection config reference.
- `docs/sip2.md` — SIP2 setup, Windows Task Scheduler for `queue:run`.
- `docs/queue-daemon.md` — running `queue:run --daemon` long-running + systemd/NSSM supervision.
- `docs/automatic-database-backup-guide.md` — backup setup.
- `docs/system-statistics.md` — `SystemStatistics::generateStatistics()` usage.
- `docs/code-base-docs.md` — `CirculationService` and `Sip2LogBuilder` usage examples.
- `docs/configurable-settings-guide.md` — `app/config/*` settings reference.
- `docs/LIVE-PREVIEW-IMPLEMENTATION.md` — MARC form live-preview internals.
- `docs/s3.md` — S3 storage configuration.
- `docs/moe-services-examples.md`, `docs/biblioitems-form.md`, `docs/DEBUG-classification-650-keep-old-one.md` — feature-specific notes.
