Files
ik-codex/CLAUDE.md
indifferentketchup 3640ca8291
Some checks failed
Tests / Run tests on PHP v8.4 (push) Failing after 1s
Tests / Run tests on PHP v8.5 (push) Failing after 1s
Add CLAUDE.md project guidance and ignore Claude local artefacts
Captures the framework architecture, the docker-based PHP/Composer
runtime, the components-outer game subtree convention, the
PatternParser-and-named-groups gotcha, the workflow conventions that
emerged during the rename and ProjectZomboid build-out, and the
synthetic-only fixture rule for committed test data. Adds .claude/ and
.claude.local.md to .gitignore so Claude session state and personal
overrides don't ride along.
2026-04-30 21:01:10 +00:00

95 lines
7.2 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
`indifferentketchup/codex` — a generic PHP log parsing and analysis framework, plus per-game subclasses that adapt the framework to specific games' log formats. PHP `>=8.4`, MIT license. Forked from `aternos/codex`; namespace was renamed in-tree (`Aternos\Codex``IndifferentKetchup\Codex`) — only the LICENSE retains the original Aternos GmbH copyright line, which must remain byte-for-byte (MIT requires it).
## Local environment
PHP and Composer are **not** installed on the host. All Composer/PHPUnit invocations go through the official `composer:latest` Docker image (currently PHP 8.5, satisfies the `>=8.4` floor):
```
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest <subcommand>
```
Use `$(pwd)` or an absolute path — bare `$PWD` has misfired here, mounting nothing and silently no-op'ing the run.
## Common commands
- All tests: `composer test` (= `phpunit test/tests` per `composer.json`)
- One test file or method (wrap in the same docker invocation):
`docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest vendor/bin/phpunit --filter=testFooBar test/tests/path/to/SomeTest.php`
- Refresh autoloader after editing `composer.json`: `composer dump-autoload`
- After cloning: `composer install` (writes `vendor/`, gitignored)
## Framework architecture
```
LogFile (Path|String|Stream)
Log ── extends AnalysableLog ── implements DetectableLogInterface
│ │ │
│ │ └─ static getDetectors(): Detector[]
│ └─ static getDefaultAnalyser(): Analyser
├─ static getDefaultParser(): Parser
▼ Log->parse()
Entry[] of Line[] (each Entry has level, time, prefix, lines)
▼ Log->analyse()
Analysis of Insight[]
└── Information (label + value) or
Problem (with attached Solution[])
```
- **`Detective`** ranks candidate Log subclasses by running each candidate's `getDetectors()` and picking the highest-scoring result (`bool|float`). It receives a `LogFile`, returns a constructed `Log` subclass.
- **`PatternParser`** is regex-driven. Lines that don't match the LINE regex append to the previous `Entry` — this is the mechanism that handles multi-line records like Java stack traces under an ERROR header.
- **`PatternAnalyser`** walks entries, runs each registered insight class's static `getPatterns()` against entry text via `preg_match_all`, and emits coalesced insights (equal insights bump a counter instead of duplicating).
- Detectors available out of the box: `SinglePatternDetector`, `WeightedSinglePatternDetector`, `LinePatternDetector` (returns match ratio), `MultiPatternDetector` (AND), and the path-based `FilenameDetector` (uses `LogFileInterface::getPath()`, returns `false` when no path is available).
## Game subtrees
Layout is **components-outer with game suffix**, not games-outer:
```
src/<Component>/<Game>/... e.g. src/Log/ProjectZomboid/ProjectZomboidServerLog.php
src/Pattern/<Game>/<Type>Pattern.php (regex string constants; not a framework abstraction)
test/tests/Games/<Game>/...
test/src/Games/<Game>/fixtures/<type>-minimal.txt (synthetic fixtures only)
```
Scaffolded games: `Minecraft`, `Hytale`, `SevenDaysToDie` (stubs only — empty `.gitkeep`s plus a TODO `<Game>Detective` extending base `Detective`). `ProjectZomboid` is fully implemented (11 log subclasses, 11 pattern classes, detective wired with all 11, synthetic fixtures, dispatch tests).
`src/Pattern/` is **not a framework abstraction** — patterns are plain `string` class constants. Each `<Type>Pattern` typically holds a `LINE` constant for the parser plus named-group extractor constants (`FIELDS`, `COMBAT`, `MOD_LOAD`, etc.) for analysers.
### ProjectZomboid specifics
- Two abstract bases: `ProjectZomboidLog` (`TIME_FORMAT = 'd-m-y H:i:s.v'`, UTC default, `makePatternParser()` helper) and `ProjectZomboidEventLog` (marker for the ten single-line logs; `ProjectZomboidServerLog` extends the parent directly because it permits multi-line entries).
- `ProjectZomboidDetective::__construct()` pre-registers all 11 log classes — instantiate it and call `setLogFile(...)->detect()`.
### Standard test template for a Log subclass
At minimum: (1) entry count after `parse()` matches the synthetic fixture's line count, (2) one or more named-group `FIELDS` regexes from the `<Type>Pattern` class extract correctly from a representative line, (3) `Detective` handed the fixture path returns an instance of this Log class. Use `#[DataProvider]` when the same shape repeats per file.
## Pitfalls
1. **`PatternParser` is incompatible with named regex groups.** PHP's `preg_match` returns named groups *plus* their numeric duplicates in the same array; `PatternParser`'s foreach iterates both and throws on the string-key entries. Convention: `LINE` regexes (used by the parser) use **unnamed** groups with field order documented in the Pattern class's docblock. Named groups are fine inside extractor regexes invoked from analysers, since `PatternAnalyser` hands the whole match array to `Insight::setMatches`.
2. **PHPUnit 12 requires the `#[DataProvider('methodName')]` attribute.** The legacy `@dataProvider` annotation silently passes zero args and fails with `ArgumentCountError`.
3. **`Level::fromString()` defaults to `Level::INFO` for unknown tokens.** Project Zomboid log levels map: `LOG`/`INFO` → INFO; `WARN` → WARNING; `ERROR` → ERROR.
4. **`PatternParser` matches array** must declare a match-type for **every** capture group in the regex (`TIME`, `LEVEL`, or `PREFIX`); otherwise the parser throws on the unmapped index. Use non-capturing groups `(?:...)` for fields you want to skip.
## Workflow conventions
- **One commit per concrete log type** when adding game support: pattern class + log subclass + synthetic fixture + test in a single commit, run `composer test`, then move on. `<Game>Detective::__construct()` wiring goes in its own follow-up commit once all log types are present.
- **Out-of-scope cleanup goes in its own commit.** Tempting workflow/lint fixes (e.g. deprecated CI syntax, comment hygiene) noticed mid-feature should not be folded in — separate commit or follow-up PR.
- **Pre-destructive checkpoint pattern.** Before bulk renames/moves: `git commit --allow-empty -m "pre-X checkpoint"` as a revert anchor. Skip the empty slot if it produces no diff at the end of a plan.
## Privacy / fixture rules
- `Logs.zip` at the repo root contains real production server data (Steam IDs, player names, world coordinates). It is gitignored.
- Extract for reference: `unzip -q Logs.zip -d .scratch/pz/`. Real logs then live under `.scratch/pz/Logs/` (gitignored). Use only as format reference. Do not paste raw Steam IDs, player names, or coordinates into chat output, commit messages, or any committed file.
- All fixtures committed under `test/src/Games/<Game>/fixtures/` must be **synthetic**, hand-crafted from the observed format with placeholder identifiers: `76561198000000001/2/3` for Steam IDs, `Player1`/`Player2`/`AdminUser` for names, generic coords (`1000-1100, 2000-2200, 0`).