Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/agents/implementer.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ Project rules live in `CLAUDE.md`; read it. The traps that matter most here:
- **Guard tests are load-bearing.** If a `tests/*SeamTest.php`, `tests/ResponseInterfaceConsumerTest.php`, `tests/AbstractClientConfigDriftTest.php`, `tests/HttpTransportCurlOptionsTest.php` or `tests/Functional/HttpTransportTest.php` starts failing, you are undoing a settled decision, not fixing a stale test. Stop and report it. Never delete or weaken one as a cleanup.
- **Exceptions** come from the `CNIC\Exception` hierarchy. Never a bare `\Exception`.
- Every new or modified file carries `declare(strict_types=1);`, typed properties, and return types.
- Do not add dependencies. Do not add mocking frameworks — use `ResponseTemplateManager::addTemplate()` or the existing spies.
- Do not add dependencies. Do not add mocking frameworks — register canned responses on a `ResponseTemplateManager` **instance** and pass it in (`new Response($id, templates: (new RTM())->addTemplate(…))`), or use the existing spies. The static `RTM::addTemplate()` form is gone (RSRMID-2941); do not reintroduce a static template container.
- `MIGRATION.md` and `docs/agents/architecture.md` are only touched for a genuine `BREAKING CHANGE:`, which is a main-thread decision, not yours.

Before reporting done, run `composer lint` and `composer test` and let the results stand. Note `.github/phpunit.xml` sets `stopOnDefect="true"` — a green run can mean the suite stopped early, so check how many tests actually executed.
Before reporting done, run `composer lint` and `composer test` and let the results stand. `.github/phpunit.xml` sets `stopOnDefect="true"` alongside `failOnWarning`/`failOnNotice`/`failOnRisky`, so every category that halts the run also fails it (RSRMID-2964) — a green run is now a complete one. A red run stops at the first defect, so the remaining count says nothing about what else is broken.

**Do not commit or push** unless the task explicitly says to.

Expand Down
3 changes: 3 additions & 0 deletions .github/phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true"
failOnWarning="true"
failOnNotice="true"
failOnRisky="true"
stopOnDefect="true">

<extensions>
Expand Down
26 changes: 13 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ Facts below; the class inventory is derivable from `src/` and the **full deep di

- **Namespace root:** `CNIC\` mapped to `src/` (PSR-4). Brand sub-namespaces: `CNR`, `IBS`, `MONIKER`.
- **Shared abstracts (in `CNIC\`):** `AbstractClient`, `AbstractSocketConfig`, `HttpTransport`, `AbstractResponseTemplateManager`, `AbstractResponseTranslator`, `AbstractResponse`. Shared **concretes:** `Record`, `Column` (templated on its value type), `ApiDateTime`. Enum: `System` (`OTE`/`LIVE`) — derived from the configured URL, never stored.
- **Brands are siblings, not parent/child:** `CNR\Response`/`IBS\Response` both extend `AbstractResponse`. `MONIKER\Client extends IBS\Client` (same platform; only `SocketConfig` differs) and reuses IBS's Response. There is no brand `Record`, and `CNR\Column extends CNIC\Column<string>` is the only brand `Column`.
- **Brands are siblings, not parent/child:** `CNR\Response`/`IBS\Response` both extend `AbstractResponse`. `MONIKER\Client extends IBS\Client` (same platform; only `SocketConfig` differs) and reuses IBS's Response. No brand declares a `Record` or a `Column` — both are shared concretes, and a value-type narrowing goes in a native return type on the interface (`getStringByIndex()`/`getStringByKey()`), never in a generic.
- **Response construction is a template method:** brands implement the `translate()`/`populate()`/`newRecord()`/`newResponseParser()` hooks — never reimplement `AbstractResponse::__construct()`.
- **The `request()` lifecycle is a template method too** (`AbstractClient::performRequest()`), and public `request(array $cmd = [], string $path = "")` is symmetric across brands. Vary a brand only through `buildCommand()`/`newResponse()`/`newSocketConfig()`.
- **Config-driven:** each `SocketConfig` (extends `AbstractSocketConfig`) carries endpoints/params/flags as typed properties (no `config.json`).
- **Connection configuration lives on the `SocketConfig`, never on the client** — reach it via `AbstractClient::getSocketConfig()` (covariant `CNR\Client::getSocketConfig(): CNR\SocketConfig` is the one narrowing point).
- **Type-hint against interfaces:** `ColumnInterface`, `RecordInterface`, `ResponseInterface`, `ExtendedResponseInterface`, `RoleCredentialsInterface`, `ResponseParserInterface`, `TransportInterface`, `LoggerInterface`, `LogSinkInterface`.
- **Type-hint against interfaces:** `ColumnInterface`, `RecordInterface`, `ResponseInterface`, `ExtendedResponseInterface`, `RoleCredentialsInterface`, `ResponseParserInterface`, `ResponseTemplateManagerInterface`, `TransportInterface`, `LoggerInterface`, `LogSinkInterface`.
- **An interface declaration must match its implementation's signature** — a parameter that exists only on the implementation is unreachable to the interface-typed consumers this project mandates. Adding one to the interface is **breaking**.
- **Public API symbols** are annotated `@psalm-api` to suppress unused-symbol warnings.

Expand All @@ -27,7 +27,7 @@ Facts below; the class inventory is derivable from `src/` and the **full deep di

Three directives have no guard test and therefore live here:

- Do **not** "symmetrise" columns onto a `newColumn()` factory like records — infeasible under PHPStan L9 / Psalm L1; keep the `registerColumn(ColumnInterface)` shape. (RSRMID-2899)
- Do **not** "symmetrise" columns onto a `newColumn()` factory like records — keep the `registerColumn(ColumnInterface)` shape. The original "infeasible under PHPStan L9 / Psalm L1" argument died with `CNR\Column`; re-run the analysers before reopening, and do not quote it. (RSRMID-2899, RSRMID-2942)
- Do **not** rewrite date columns in the response data. Do **not** grow `CNIC\ApiDateTime` beyond an opt-in UTC-only **parser** — accepting both `-` and `/` separators is in scope, but no `in($tz)`, no locale formatting, no `ext-intl`, no markup helpers. (RSRMID-2318; RSRMID-2926)
- Do **not** make `dateTime` fall back to `date` for date-only values — `ts` and `dateTime` are null _together_. (RSRMID-2318)

Expand Down Expand Up @@ -76,7 +76,7 @@ Rules below; harness detail (cassettes, functional tests, spies) is in [docs/age

- **Framework:** PHPUnit 12+, config `.github/phpunit.xml`. Test namespace `CNICTEST\` mirroring `CNIC\`.
- **Test classes:** always `final class` extending `\PHPUnit\Framework\TestCase`; methods `testDescriptiveName` in camelCase.
- **Mocking:** register mock API responses via `ResponseTemplateManager::addTemplate()`, or use the hand-written spies (`SpyTransport`, `SpyResponseParser`) — do **not** add Mockery or Prophecy.
- **Mocking:** register mock API responses on a `ResponseTemplateManager` **instance** and hand it to the Response — `new Response($templateId, templates: (new RTM())->addTemplate(…))` — or use the hand-written spies (`SpyTransport`, `SpyResponseParser`). Do **not** add Mockery or Prophecy, and do **not** reintroduce a static template container (RSRMID-2941).
- **Shared state:** `static` properties + `setUpBeforeClass()` for one-time client setup.
- **No real API calls in unit tests.** `request()`-path tests replay committed cassettes offline (`composer test`); re-record against OT&E only with `composer test:record` when the exercised API behaviour changes. `tests/Functional/` is the one deliberate exception — a loopback HTTP server, skipped if it cannot bind a port.
- **Direct parser tests live in `tests/<Brand>/ResponseParserTest.php`** — keep parse assertions out of `ResponseTest.php`.
Expand Down Expand Up @@ -130,14 +130,14 @@ Short reminders; full detail in the linked docs.

Class inventory is derivable from `src/` in a couple of greps and is deliberately not tabulated here (see [architecture.md](docs/agents/architecture.md)). The paths worth knowing because they are **not** guessable:

| Path | Purpose |
| ------------------------------------------------------------------------ | ------------------------------------------------------- |
| `.github/linters/{phpcs.xml,phpstan.neon,psalm.xml,rector.php}` | Linter/analyser/modernization configs |
| `.github/phpunit.xml` | PHPUnit configuration (note `stopOnDefect="true"`) |
| `tests/<Brand>/cassettes/` | Committed `request()` cassettes (replay is the default) |
| `env.example.sh` | Template for required env variables (copy to `env.sh`) |
| `src/Exception/CnicException.php` | Base of the additive `CNIC\Exception` hierarchy |
| `src/{ResponseInterface,ResponseParserInterface,TransportInterface}.php` | The seams brands and tests substitute through |
| Path | Purpose |
| ------------------------------------------------------------------------ | -------------------------------------------------------------- |
| `.github/linters/{phpcs.xml,phpstan.neon,psalm.xml,rector.php}` | Linter/analyser/modernization configs |
| `.github/phpunit.xml` | PHPUnit config (`stopOnDefect` + `failOnWarning/Notice/Risky`) |
| `tests/<Brand>/cassettes/` | Committed `request()` cassettes (replay is the default) |
| `env.example.sh` | Template for required env variables (copy to `env.sh`) |
| `src/Exception/CnicException.php` | Base of the additive `CNIC\Exception` hierarchy |
| `src/{ResponseInterface,ResponseParserInterface,TransportInterface}.php` | The seams brands and tests substitute through |

## Atlassian / JIRA

Expand Down Expand Up @@ -173,7 +173,7 @@ Opus decides, Sonnet implements: plan and review in the main thread, hand the me
- Read, display, or expose the contents of `env.sh` — it contains secrets
- Add dependencies without explicit request — this is a lightweight SDK
- Throw a bare `\Exception` or declare exception types outside `CNIC\Exception`
- Use mocking frameworks (Mockery, Prophecy) — use ResponseTemplateManager or the repo's spies
- Use mocking frameworks (Mockery, Prophecy) — use a `ResponseTemplateManager` instance or the repo's spies
- Add `@author` tags to docblocks
- Add `Co-Authored-By:` trailers to commit messages

Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Two rules follow, and they cut in opposite directions:

Then prove the guard is not vacuous: apply the mutation it is supposed to refuse, confirm the guard fails, and confirm the rest of the suite stays green — that green suite is the whole argument for the test existing. Note that `.github/phpunit.xml` sets `stopOnDefect="true"`, so a plain `composer test` halts at the first failure; set the guard aside temporarily to observe the "nothing else fails" half.

**Check the exit code, not the summary line**, and be specific about what your mutation actually produces. A guard whose subject is a PHP diagnostic rather than a wrong value needs particular care: the config now sets `failOnWarning`/`failOnNotice`/`failOnRisky` so a warning does fail the build (RSRMID-2964), but a guard that leans on that alone goes vacuous the day someone edits those attributes back out. If the thing you are refusing is a diagnostic, assert on it inside the test — install a `set_error_handler`, capture, and assert nothing was raised. [tests/ResponseTemplateRegistrySeamTest.php](tests/ResponseTemplateRegistrySeamTest.php) does this, and its docblock says why.

## Code of Conduct

### Our Pledge
Expand Down
Loading