From 3640ca82910a963d46f5ea62ec3598edabdae776 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Thu, 30 Apr 2026 21:01:10 +0000 Subject: [PATCH] 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. --- .gitignore | 2 ++ CLAUDE.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index b4fc339..24b10a0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ vendor .phpunit.result.cache Logs.zip .scratch/ +.claude/ +.claude.local.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9dae321 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,94 @@ +# 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 +``` + +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///... e.g. src/Log/ProjectZomboid/ProjectZomboidServerLog.php +src/Pattern//Pattern.php (regex string constants; not a framework abstraction) +test/tests/Games//... +test/src/Games//fixtures/-minimal.txt (synthetic fixtures only) +``` + +Scaffolded games: `Minecraft`, `Hytale`, `SevenDaysToDie` (stubs only — empty `.gitkeep`s plus a TODO `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 `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 `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. `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//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`).