CLAUDE.md: added RedactorInterface bullet to the architecture list (after Custom Analyser subclasses, before Detectors); added ProjectZomboidRedactor entry under ProjectZomboid specifics; added src/Util/ to the game-subtrees layout code block with a prose note marking it as the sixth component directory introduced post-v0.1.0; added Pitfall 5 on mandatory pass order. README.md: new "Redaction" subsection between Quick start and Architecture — PHP snippet, replacement descriptions, three toggle methods, three documented v1 limitations. CHANGELOG.md: added [Unreleased] section (Added + Changed) above [0.1.0]. Removed the Redactor bullet from [0.1.0]'s Deferred list entirely — the historical record stays accurate (v0.1.0 shipped without it) and [Unreleased] now documents its arrival; a stub mention in Deferred would be redundant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.9 KiB
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/testspercomposer.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(writesvendor/, 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[])
Detectiveranks candidate Log subclasses by running each candidate'sgetDetectors()and picking the highest-scoring result (bool|float). It receives aLogFile, returns a constructedLogsubclass.PatternParseris regex-driven. Lines that don't match the LINE regex append to the previousEntry— this is the mechanism that handles multi-line records like Java stack traces under an ERROR header.PatternAnalyserwalks entries, runs each registered insight class's staticgetPatterns()against entry text viapreg_match_all, and emits coalesced insights (equal insights bump a counter instead of duplicating).- Custom
Analysersubclasses are the right move when analysis needs cross-entry state — pairing events, sliding-window thresholds, comparing consecutive snapshots.PatternAnalyseroperates per-entry only and can't express those. Phase B.3 (ConnectionFailureAnalyser,ItemDuplicationAnalyser,SkillProgressionAnomalyAnalyser) shows the shape: extendAnalyser, overrideanalyse(), walk$this->logonce, aggregate, then emit coalescedProblem/Informationinsights at the end. Tunable thresholds belong aspublic constconstants on the subclass with the rationale in a docblock. RedactorInterfaceis a render-time PII filter — string-in/string-out, configured per game, implemented atsrc/Util/<Game>/<Game>Redactor.php. Consumers callredact(string $content): stringon a concrete instance before rendering or exporting log content.- Detectors available out of the box:
SinglePatternDetector,WeightedSinglePatternDetector,LinePatternDetector(returns match ratio),MultiPatternDetector(AND), and the path-basedFilenameDetector(usesLogFileInterface::getPath(), returnsfalsewhen 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)
src/Util/<Game>/... e.g. src/Util/ProjectZomboid/ProjectZomboidRedactor.php
test/tests/Games/<Game>/...
test/src/Games/<Game>/fixtures/<type>-minimal.txt (synthetic fixtures only)
src/Util/ is the sixth top-level component directory, introduced post-v0.1.0-tag. Its first occupant is the Redactor; future game-agnostic utilities (tokenising redactor variants, etc.) land here too.
Scaffolded games: Minecraft, Hytale, SevenDaysToDie (stubs only — empty .gitkeeps 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, plus the analyser surface — 11 PatternAnalyser-driven Insight classes under src/Analysis/ProjectZomboid/ and 3 custom Analyser subclasses under src/Analyser/ProjectZomboid/ for cross-entry / threshold logic.
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) andProjectZomboidEventLog(marker for the ten single-line logs;ProjectZomboidServerLogextends the parent directly because it permits multi-line entries). ProjectZomboidDetective::__construct()pre-registers all 11 log classes — instantiate it and callsetLogFile(...)->detect().- Each Log subclass's
getDefaultAnalyser()returns one of:- A custom
Analysersubclass (cross-entry logic):UserLog → ConnectionFailureAnalyser,ItemLog → ItemDuplicationAnalyser,PerkLog → SkillProgressionAnomalyAnalyser. - A configured
PatternAnalyser(per-entry pattern matching):ServerLog,PvpLog,AdminLogregister their respective Insight classes. - An empty
PatternAnalyserfor logs with no analysers yet:ChatLog,ClientActionLog,CmdLog,MapLog,BurdJournalsLog. These are wiring stubs awaiting future analysis work.
- A custom
ProjectZomboidRedactoratsrc/Util/ProjectZomboid/ProjectZomboidRedactor.php— concreteRedactorInterfaceimplementation. Downstream consumers callredact(string): stringto scrub Steam IDs (zeroed placeholder), player names (<player>), and world coordinates (0,0,0) from log content. Three independent toggle methods default to on:redactSteamIds(bool),redactPlayerNames(bool),redactCoordinates(bool). Pass order (Steam ID → player name → coords) is mandatory and enforced internally — see Pitfall 5.
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
PatternParseris incompatible with named regex groups. PHP'spreg_matchreturns named groups plus their numeric duplicates in the same array;PatternParser's foreach iterates both and throws on the string-key entries. Convention:LINEregexes (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, sincePatternAnalyserhands the whole match array toInsight::setMatches.- PHPUnit 12 requires the
#[DataProvider('methodName')]attribute. The legacy@dataProviderannotation silently passes zero args and fails withArgumentCountError. Level::fromString()defaults toLevel::INFOfor unknown tokens. Project Zomboid log levels map:LOG/INFO→ INFO;WARN→ WARNING;ERROR→ ERROR.PatternParsermatches array must declare a match-type for every capture group in the regex (TIME,LEVEL, orPREFIX); otherwise the parser throws on the unmapped index. Use non-capturing groups(?:...)for fields you want to skip.ProjectZomboidRedactorpass order is mandatory.PLAYER_AFTER_STEAMID_REGEXanchors on the already-redacted Steam ID placeholder — it will not match raw Steam IDs. Do NOT swap the Steam ID and player-name passes, and do NOT stub out the Steam ID pass while leaving the player-name pass enabled.
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.zipat 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/3for Steam IDs,Player1/Player2/AdminUserfor names, generic coords (1000-1100, 2000-2200, 0).