From ed920485dc9623edf5304c8fe62e44198e64e696 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Fri, 1 May 2026 12:53:32 +0000 Subject: [PATCH] docs: backfill Phase B.3 spec and plan Retroactive design + plan documentation for Phase B.3 (deferred analysers requiring custom Analyser subclasses for cross-entry and threshold logic). Records the architectural shift away from vanilla PatternAnalyser, the threshold constant rationale (event-pairing / sliding-window / consecutive-snapshot deltas), and the synthetic fixture extensions that exercise both trigger and non-trigger paths. Plan is as-built with checkboxes pre-checked and SHAs referenced. --- .../plans/2026-04-30-pz-analysers-deferred.md | 74 +++++++++++ ...2026-04-30-pz-analysers-deferred-design.md | 117 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-30-pz-analysers-deferred.md create mode 100644 docs/superpowers/specs/2026-04-30-pz-analysers-deferred-design.md diff --git a/docs/superpowers/plans/2026-04-30-pz-analysers-deferred.md b/docs/superpowers/plans/2026-04-30-pz-analysers-deferred.md new file mode 100644 index 0000000..53d7fd2 --- /dev/null +++ b/docs/superpowers/plans/2026-04-30-pz-analysers-deferred.md @@ -0,0 +1,74 @@ +# ProjectZomboid Phase B.3 Deferred Analysers — As-Built Plan + +> Retroactive: written 2026-05-01. + +This document is a historical record of how Phase B.3 (the three deferred analysers from the original Step D candidate list) was implemented. The corresponding design spec is `docs/superpowers/specs/2026-04-30-pz-analysers-deferred-design.md`. The work is complete and merged to `master`; checkboxes are pre-checked. + +**Goal:** Land three custom `Analyser` subclasses under `src/Analyser/ProjectZomboid/` (the first non-empty contents of that directory), three `Problem` subclasses under `src/Analysis/ProjectZomboid/`, threshold constants documented inline as `public const`, fixture extensions to exercise trigger and non-trigger paths, and e2e tests verifying the analysers' behaviour against the fixtures. + +**Architecture:** Custom subclasses of the framework's abstract `Analyser`. Each overrides `analyse()` to walk `$this->log` once, aggregate cross-entry state, and emit coalesced `Problem` insights at the end. This is the first deviation from Phase B.1/B.2's vanilla-`PatternAnalyser` pattern; the reasoning is recorded in the design spec and in `CLAUDE.md`. + +**Tech Stack:** PHP 8.4+, PHPUnit 12, Composer (root package: `indifferentketchup/codex`). PHP/Composer not installed on host — all command invocations wrap in `docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest …`. + +--- + +## Tasks + +### Task 0 — Pre-checkpoint + +- [x] Empty checkpoint commit: `c444e85 pre-phase-B.3 checkpoint` + +### Task 1 — `ConnectionFailureAnalyser` (UserLog) + +Pairing logic: walk the log, count `attempting to join` and `allowed to join` events per Steam ID, emit a `ConnectionFailureProblem` for any Steam ID whose attempt count exceeds its allowed count. + +- [x] Add `src/Analysis/ProjectZomboid/ConnectionFailureProblem.php` (Steam ID, player, unmatched count; `isEqual` coalesces by Steam ID) +- [x] Add `src/Analyser/ProjectZomboid/ConnectionFailureAnalyser.php` — first file in this directory; the `.gitkeep` placeholder is removed in this commit +- [x] Wire `ProjectZomboidUserLog::getDefaultAnalyser()` to return `new ConnectionFailureAnalyser()` and drop the now-unused `PatternAnalyser` import +- [x] Add `test/tests/Games/ProjectZomboid/Analyser/UserLogAnalysisTest.php` — asserts Player1 (`76561198000000001`) is flagged with `unmatchedAttempts == 1` and Player2 (`76561198000000002`) is not flagged +- [x] `composer test` green: 188 tests, 392 assertions +- [x] Commit: `73e9ca6 Add ConnectionFailureAnalyser` + +Design note inside the analyser docblock: "attempting to join used queue" rows are surfaced as failures in v1 because a long queue wait is indistinguishable from a real failure without timing context. Tunable in v2 if false positives become noisy. + +### Task 2 — `ItemDuplicationAnalyser` (ItemLog) + +Sliding-window heuristic over `(steamid, item)` groups, restricted to positive-delta events. Negative-delta rows (drops/transfers) are filtered out. + +- [x] Add `src/Analysis/ProjectZomboid/ItemDuplicationProblem.php` (Steam ID, player, item, event count; `isEqual` coalesces by `(steamid, item)`) +- [x] Add `src/Analyser/ProjectZomboid/ItemDuplicationAnalyser.php` with two threshold constants and rationale docblocks: `THRESHOLD_COUNT = 5`, `THRESHOLD_WINDOW_SECONDS = 10` +- [x] Wire `ProjectZomboidItemLog::getDefaultAnalyser()` to return `new ItemDuplicationAnalyser()`; drop unused `PatternAnalyser` import +- [x] Extend `test/src/Games/ProjectZomboid/fixtures/item-minimal.txt`: append 6 Bullets9mm events at sub-second timestamps `19:50:00.001`–`.006` for AdminUser (trigger), plus 4 Plank events scattered `20:00:00`–`20:03:00` for Player1 (sub-threshold) +- [x] Bump entry-count assertion in `ProjectZomboidItemLogTest::testParsesEachLineAsAnEntry`: 10 → 20 +- [x] Add `test/tests/Games/ProjectZomboid/Analyser/ItemLogAnalysisTest.php` — asserts one `ItemDuplicationProblem` (AdminUser + Bullets9mm + 6 events), zero for the Plank group, and the threshold constants are positive +- [x] `composer test` green: 191 tests, 400 assertions +- [x] Commit: `ba3fae8 Add ItemDuplicationAnalyser` + +Implementation note: the analyser uses a two-pointer sliding window per group, which is O(n) per group after the initial sort. `Entry::getTime()` returns integer Unix seconds (sub-second precision dropped); the burst events all collapse to the same Unix-second value so any positive window catches them. + +### Task 3 — `SkillProgressionAnomalyAnalyser` (PerkLog) + +Compare consecutive perks-snapshot rows per Steam ID; emit a problem for any single skill that gained more than `THRESHOLD_DELTA` levels between snapshots. + +- [x] Add `src/Analysis/ProjectZomboid/SkillProgressionAnomalyProblem.php` (Steam ID, player, skill, fromLevel, toLevel, delta; `isEqual` coalesces by `(steamid, skill)`) +- [x] Add `src/Analyser/ProjectZomboid/SkillProgressionAnomalyAnalyser.php` with `THRESHOLD_DELTA = 3` and a rationale docblock about PZ's slow skill leveling +- [x] Wire `ProjectZomboidPerkLog::getDefaultAnalyser()` to return `new SkillProgressionAnomalyAnalyser()`; drop unused `PatternAnalyser` import +- [x] Extend `test/src/Games/ProjectZomboid/fixtures/perk-minimal.txt`: append PlayerSuspect (Steam ID `76561198000000004`) with two snapshots — Strength 2→10 (+8 trigger), Fitness 2→8 (+6 trigger), Maintenance 0→3 (+3 boundary, does not trigger because comparison is strict `>`) +- [x] Bump entry-count assertion in `ProjectZomboidPerkLogTest::testParsesEachLineAsAnEntry`: 6 → 10 +- [x] Add `test/tests/Games/ProjectZomboid/Analyser/PerkLogAnalysisTest.php` — asserts exactly two problems for PlayerSuspect (Strength + Fitness, sorted), no problem for Maintenance, no problems for single-snapshot Player1/Player2, and the threshold constant is positive +- [x] `composer test` green: 195 tests, 412 assertions +- [x] Commit: `0c90e40 Add SkillProgressionAnomalyAnalyser` + +Filtering note: the analyser skips event-token rows (`Login`, `Logout`, `LevelUp`) by checking that the bracketed event field contains a `Skill=N` pair via `PerkPattern::PERK_PAIR`. Only true perks-snapshot rows enter the comparison. + +--- + +## Done condition (met) + +After Task 3, `composer test` reports **195 tests, 412 assertions, all green** under PHPUnit 12.5.6 / PHP 8.5.5. All eight Step D candidate analysers (Phase B.1's three ServerLog + Phase B.2's seven PvP/Admin + Phase B.3's three deferred) are operational across their respective Log subclasses. + +The directory `src/Analyser/ProjectZomboid/` now contains real code for the first time; its `.gitkeep` placeholder was removed in `73e9ca6`. + +## Deviations from the original plan + +None this phase. The 4-commit count and the per-analyser shape both match what was committed-to in chat before execution. No silent breakages, no missing closing braces. The only observation worth recording is that the planned commit count was inclusive of the pre-checkpoint, and the actual commit ordering matched the plan exactly. diff --git a/docs/superpowers/specs/2026-04-30-pz-analysers-deferred-design.md b/docs/superpowers/specs/2026-04-30-pz-analysers-deferred-design.md new file mode 100644 index 0000000..02e67ea --- /dev/null +++ b/docs/superpowers/specs/2026-04-30-pz-analysers-deferred-design.md @@ -0,0 +1,117 @@ +# ProjectZomboid analyser design (Phase B.3 — deferred analysers) + +> Retroactive: written 2026-05-01. + +## Summary + +Add the three remaining Project Zomboid analysers from the original Step D candidate list — connection failure pairing, item duplication heuristic, and skill progression anomaly detection — by introducing custom `Analyser` subclasses under `src/Analyser/ProjectZomboid/`. These are the first analysers in the tree that cannot be expressed as configured `PatternAnalyser` instances; they require cross-entry state (event pairing, sliding windows, snapshot deltas) that `PatternAnalyser` does not provide. + +This document covers Phase B.3. Phase B.1 / B.2 docs are at `2026-04-30-pz-analysers-design.md` / `2026-04-30-pz-analysers-pvp-admin-design.md`. With Phase B.3, the original eight-analyser candidate list from Step D is fully implemented. + +## Scope + +- **In scope:** `ConnectionFailureAnalyser` + `ConnectionFailureProblem` (UserLog, event pairing); `ItemDuplicationAnalyser` + `ItemDuplicationProblem` (ItemLog, sliding-window heuristic); `SkillProgressionAnomalyAnalyser` + `SkillProgressionAnomalyProblem` (PerkLog, consecutive-snapshot delta); wiring three Log subclasses' `getDefaultAnalyser()`; extending two synthetic fixtures to exercise trigger and non-trigger cases; end-to-end tests. +- **Out of scope (B.3):** the five other PZ logs whose `getDefaultAnalyser()` continues returning an empty `PatternAnalyser` stub (Chat, ClientAction, Cmd, Map, BurdJournals); the codex-side `Redactor` utility; Hytale / Minecraft / Seven Days To Die analysers; v0.1.0 release plumbing. + +## Architectural shift: custom `Analyser` subclasses + +Phases B.1 and B.2 established the convention that vanilla `PatternAnalyser` plus `Insight::isEqual()` coalescing is sufficient for per-entry pattern matching, and a custom Analyser subclass is **not** needed even for multi-line records (PatternParser's continuation-line behaviour combined with `Entry::__toString()` joins solves multi-line capture without subclassing). + +Phase B.3's three analysers genuinely require cross-entry state: + +- **ConnectionFailureAnalyser** must count `attempting to join` and `allowed to join` events per Steam ID and report unmatched attempts. PatternAnalyser dispatches each entry independently and has no mechanism to compare counts across entries. +- **ItemDuplicationAnalyser** must group positive-delta item events by `(steamid, item)` tuple and slide a fixed-second window across each group. Sliding-window logic spans multiple entries by definition. +- **SkillProgressionAnomalyAnalyser** must collect all perks-row snapshots per Steam ID, sort them by time, then compute pairwise deltas between consecutive snapshots. Pairwise comparison spans entries. + +Each subclass extends the framework's abstract `Analyser`, overrides `analyse(): AnalysisInterface`, walks `$this->log` once to aggregate state, and emits `Problem` insights at the end. The CLAUDE.md "Framework architecture" section was updated alongside Phase B.3 to document this pattern. + +## Components + +Three `Analyser` subclasses under `src/Analyser/ProjectZomboid/` (the directory's `.gitkeep` placeholder is removed in this phase): + +| Analyser | Target Log | Logic shape | Threshold constants | +|---|---|---|---| +| `ConnectionFailureAnalyser` | `ProjectZomboidUserLog` | Two-pass count of attempt vs allowed events per Steam ID; emits one Problem per Steam ID where attempts > allowed | None — strict pairing | +| `ItemDuplicationAnalyser` | `ProjectZomboidItemLog` | Sliding-window heuristic over `(steamid, item)` groups | `THRESHOLD_COUNT = 5`, `THRESHOLD_WINDOW_SECONDS = 10` | +| `SkillProgressionAnomalyAnalyser` | `ProjectZomboidPerkLog` | Consecutive-snapshot delta per `(steamid, skill)`; only positive-delta perks-row entries (Login/Logout/LevelUp event tokens are filtered out) | `THRESHOLD_DELTA = 3` | + +Three `Problem` subclasses under `src/Analysis/ProjectZomboid/`: + +| Problem | Coalescing | +|---|---| +| `ConnectionFailureProblem` | By Steam ID — one problem per player regardless of how many unmatched attempts | +| `ItemDuplicationProblem` | By `(steamid, item)` tuple — one problem per suspicious group | +| `SkillProgressionAnomalyProblem` | By `(steamid, skill)` — one problem per skill exceeding the delta threshold | + +## Threshold rationale (recorded as docblocks) + +The constants are first-pass heuristics expected to be tuned once production logs flow through codex. Each is documented inline in its analyser class: + +- **`ItemDuplicationAnalyser::THRESHOLD_COUNT = 5`**: Five identical item gains in a fixed window. Legitimate gameplay rarely produces five identical items quickly — crafting has animation delays, looting is one-at-a-time, zombie drops are similarly serial. A burst of five suggests admin-spawn or exploit. Tune downward if false negatives appear. +- **`ItemDuplicationAnalyser::THRESHOLD_WINDOW_SECONDS = 10`**: Ten seconds covers a realistic burst-loot scenario (e.g. a crate full of identical items) without collapsing onto unrelated events. Combined with `THRESHOLD_COUNT` this means an effective rate of 0.5 same-item events per second. +- **`SkillProgressionAnomalyAnalyser::THRESHOLD_DELTA = 3`**: PZ skills require thousands of XP per level; even active grinding rarely produces four-or-more level jumps in a single session bridge. Set to 3 as baseline; modded XP servers may need to raise this via subclass override. + +## Patterns + +No new pattern constants. Existing constants from Phase A are reused inside the per-entry walks: + +- `UserPattern::PLAYER_EVENT` — decode `[time] "" ` lines +- `ItemPattern::FIELDS` — decode `[time] "" []` lines +- `PerkPattern::FIELDS` — decode the bracket-heavy perks log line +- `PerkPattern::PERK_PAIR` — extract individual `Skill=N` pairs from the perks-row event field + +`Entry::getTime()` returns integer Unix seconds (sub-second precision is dropped by `DateTime::getTimestamp()`). For `ItemDuplicationAnalyser` this means events within the same second collapse to time-diff zero, which is acceptable for v1. + +## Wiring + +Three `getDefaultAnalyser()` overrides (each was previously `return new PatternAnalyser();`): + +```php +// ProjectZomboidUserLog +return new ConnectionFailureAnalyser(); + +// ProjectZomboidItemLog +return new ItemDuplicationAnalyser(); + +// ProjectZomboidPerkLog +return new SkillProgressionAnomalyAnalyser(); +``` + +The unused `PatternAnalyser` import is removed from each Log subclass. + +## Test plan + +End-to-end tests under `test/tests/Games/ProjectZomboid/Analyser/`, one per Log: + +- **`UserLogAnalysisTest`** — drives `user-minimal.txt`. Asserts exactly one `ConnectionFailureProblem` for Player1 (Steam ID `76561198000000001`) with `unmatchedAttempts == 1` (Player1 has two `attempting to join` events, one of which is `attempting to join used queue`, and one `allowed to join`). Asserts that Player2 (matched 1+1) is not flagged. +- **`ItemLogAnalysisTest`** — drives the extended `item-minimal.txt`. Asserts one `ItemDuplicationProblem` for AdminUser + Base.Bullets9mm with `eventCount == 6`, and verifies the four-event Base.Plank group does not trigger. Also asserts the threshold constants are positive and documented. +- **`PerkLogAnalysisTest`** — drives the extended `perk-minimal.txt`. Asserts exactly two `SkillProgressionAnomalyProblem` insights for PlayerSuspect (Steam ID `76561198000000004`), one for Strength (delta +8) and one for Fitness (delta +6). Verifies that Maintenance (delta exactly +3) does not trigger because the comparison is strict `>`. Verifies that single-snapshot players (Player1, Player2) are not flagged. Asserts the threshold constant is positive and documented. + +## Fixture changes + +Two synthetic fixtures extended (no new files, no real-log content): + +- **`item-minimal.txt`** — appended 10 lines: a 6-event Bullets9mm burst by AdminUser at sub-second timestamps `19:50:00.001`–`.006` (triggers the dupe heuristic), and a 4-event Plank group by Player1 scattered across 4 minutes (`20:00:00`–`20:03:00`, sub-threshold). The Phase A entry-count assertion in `ProjectZomboidItemLogTest` was bumped from 10 → 20. +- **`perk-minimal.txt`** — appended 4 lines: PlayerSuspect (Steam ID `76561198000000004`) with two perks snapshots — a low-stat baseline at `18:30:00.000` and an inflated set at `22:00:00.000` showing Strength 2→10, Fitness 2→8, and Maintenance 0→3 (boundary case). The Phase A entry-count assertion in `ProjectZomboidPerkLogTest` was bumped from 6 → 10. + +All identifiers are placeholder per the Privacy / Fixture Rules in CLAUDE.md (`76561198000000001`–`76561198000000004` for Steam IDs, `Player1`/`Player2`/`AdminUser`/`PlayerSuspect` for names, coords in the `1000-1100, 2000-2200, 0` range). + +## Commits (as-built, in order) + +1. `c444e85` — `pre-phase-B.3 checkpoint` (`--allow-empty`) +2. `73e9ca6` — `Add ConnectionFailureAnalyser` +3. `ba3fae8` — `Add ItemDuplicationAnalyser` +4. `0c90e40` — `Add SkillProgressionAnomalyAnalyser` + +4 commits total. Each non-checkpoint commit ships an Analyser + Problem + (optional) fixture extension + updated count assertion + e2e test in one logical unit, per the per-analyser commit shape requested up front. + +## Open issues + +None blocking. All three threshold constants are heuristic guesses pending production data calibration; tuning is expected once iblogs starts feeding real logs through codex. The values are tunable via subclass override and the rationale is in the source docblocks. + +## Pointers + +- Phase B.1 (foundation, ServerLog analysers): `2026-04-30-pz-analysers-design.md` and `2026-04-30-pz-analysers.md`. +- Phase B.2 (vanilla PatternAnalyser PvP/Admin coverage): `2026-04-30-pz-analysers-pvp-admin-design.md` and `2026-04-30-pz-analysers-pvp-admin.md`. +- Workflow conventions and architecture overview: `CLAUDE.md`. +- The Phase B.3 commit set begins at `c444e85` (pre-checkpoint) and ends at `0c90e40` (the third analyser).