Skip to content

Rebuild cache observability on Koriym.SemanticLogger - #178

Open
koriym wants to merge 26 commits into
bearsunday:1.xfrom
koriym:cache-observability
Open

Rebuild cache observability on Koriym.SemanticLogger#178
koriym wants to merge 26 commits into
bearsunday:1.xfrom
koriym:cache-observability

Conversation

@koriym

@koriym koriym commented Jun 2, 2026

Copy link
Copy Markdown
Member

Summary

Rebuilds cache observability on Koriym.SemanticLogger: the cache create/invalidate log becomes an open/event/close tree whose nesting is the embed/dependency structure (a parent's embedded children nest under it). The legacy flat RepositoryLogger is deprecated but stays bound for BC and receives no internal events.

Included

  • SemanticLogger-based logging at the five AOP boundaries: CacheInterceptor / AbstractDonutCacheInterceptor (GET) and CommandInterceptor / RefreshInterceptor / DonutCommandInterceptor (command).
  • SafeSemanticLogger best-effort decorator (a logging failure can never break a cache read/write), NullSemanticLogger zero-cost no-op default, bound via SafeSemanticLoggerProvider.
  • Typed AbstractContext subclasses in src/Log/Context/ with per-context JSON Schemas in docs/schemas/context/.
  • invalidate context records self-describing status words: roPool/etagPool (invalidated|failed), cdn (purged|failed), plus durationMs. The CDN purge is best-effort and no longer fails local invalidation on outage.
  • Manual (direct, non-AOP) top-level put() and invalidateTags() are rooted in manual_store / manual_invalidate scopes so their events are not dropped at flush, mirroring the existing manual_purge treatment.
  • Adds runtime dependency koriym/semantic-logger ^0.8.0; vendor/bin/stree renders the cache log as a readable tree (demo/run-dependency.php, demo/run-donut.php).

Not included (deliberate)

Verification

  • phpcs (PSR-12) clean; PHPStan (max) and Psalm (level 1) report no errors.
  • phpunit: 141 tests, 270 assertions, OK.
  • demo/run-dependency.php and demo/run-donut.php render the SemanticLogger tree (exit 0).

Commits

  1. Rebuild cache observability on Koriym.SemanticLogger
  2. Log manual (top-level) put() and invalidateTags() in dedicated scopes

github-actions and others added 4 commits January 1, 2025 03:11
Migrate the cache create/invalidate log to Koriym.SemanticLogger's open/event/close tree, where the nesting mirrors the embed/dependency structure. Typed Context classes (src/Log/Context/) carry per-context JSON Schemas (docs/schemas/context/), and SafeSemanticLogger guarantees logging never breaks cache reads/writes (NullSemanticLogger is the no-op default).

Adds koriym/semantic-logger ^0.8.0; vendor/bin/stree renders the cache log as a tree (demo/run-dependency.php, demo/run-donut.php). The legacy RepositoryLogger interface stays bound for BC but receives no internal events.

The multi-embed dependency fix and diamond tests are intentionally excluded here; they already shipped in 1.16.1 (bearsunday#177).
Direct, non-AOP cache operations had no enclosing log scope, so their save/invalidate events were dropped at flush (an event-only session renders empty). A top-level put() now opens a manual_store scope (closed with manual_store_result) and a top-level invalidateTags() opens a manual_invalidate scope (closed with the existing InvalidateContext), mirroring the existing manual_purge treatment. Nested (AOP) calls are unchanged, so the in-flow tree shape is identical.

Adds the three Context classes and their JSON Schemas, and tests that flush-validate the new top-level trees.
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d3a9ba45-ea9d-4475-b8c8-52ded8a20f86

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR migrates BEAR.QueryRepository from flat string-based repository logging to structured Koriym.SemanticLogger with 25 typed context classes. Cache components now emit observable scopes for dependencies, commands, and manual operations, validated against JSON schemas, while a failure-tolerant wrapper ensures logging failures do not disrupt cache behavior.

Changes

Semantic Logger Migration for Cache Observability

Layer / File(s) Summary
Schema and typed context contracts
docs/schemas/context/*.json, src/Log/Context/*.php
25 JSON Schemas and corresponding PHP context classes define contracts for cache hits/misses, invalidation outcomes, manual operations, command execution, and dependency resolution. Each context extends AbstractContext and declares TYPE and SCHEMA_URL constants.
Semantic logger implementations and adapter layer
src/Log/NullSemanticLogger.php, src/Log/SafeSemanticLogger.php, src/Log/SafeSemanticLoggerProvider.php, src/NullRepositoryLogger.php, src/RepositoryLoggerInterface.php, src/StructuredRepositoryLoggerInterface.php, src/RepositoryLogger.php
No-op and safe-mode logger wrappers; SafeSemanticLogger prevents delegate failures from disrupting cache; provider for DI binding; legacy RepositoryLoggerInterface marked deprecated; new StructuredRepositoryLoggerInterface exposes getLogs() and getOps() accessors for backward-compatible structured logging.
Cache interceptor instrumentation with hit/miss tracking
src/CacheInterceptor.php, src/AbstractDonutCacheInterceptor.php, src/DonutRepository.php
CacheInterceptor and AbstractDonutCacheInterceptor open a GET scope on invoke, track $hit flag when cached state succeeds, and close the scope with CacheHitContext or CacheMissContext. DonutRepository emits PutDonutContext on cache writes and CacheHitContext/CacheMissContext/RefreshDonutContext on donut refresh.
Dependency resolution and donut cache instrumentation
src/CacheDependency.php
CacheDependency emits DependsOnContext after accumulating surrogate child tags, capturing parent URI, child URI, and computed child tags array.
Top-level manual scope management for cache writes and invalidations
src/QueryRepository.php, src/ResourceStorage.php
QueryRepository wraps top-level put() and purge() in ManualStoreContext/ManualPurgeContext scopes; ResourceStorage wraps top-level invalidateTags() in ManualInvalidateContext and treats CDN purger as best-effort, recording outcome in InvalidateContext.
Command context factory and command interceptor scopes
src/CommandContextFactory.php, src/CommandInterceptor.php, src/DonutCommandInterceptor.php, src/RefreshInterceptor.php
CommandContextFactory extracts method name and command annotations; CommandInterceptor, DonutCommandInterceptor, and RefreshInterceptor open a command scope on invoke and close it with CommandResultContext derived from resource code.
Dependency injection wiring for semantic logger
src/DonutCacheModule.php
SemanticLoggerInterface bound to SafeSemanticLoggerProvider as singleton; legacy RepositoryLoggerInterface binding retained for backward compatibility but receives no internal cache events.
Test helpers and fixtures for semantic logging validation
tests/RecordingSemanticLogger.php, tests/ThrowingSemanticLogger.php, tests/FakeThrowingPurger.php, tests/SemanticLogTreeTrait.php, tests/SafeSemanticLoggerTest.php, tests/SemanticLogSchemaTest.php, tests/GracefulLoggingTest.php
RecordingSemanticLogger and ThrowingSemanticLogger test doubles; FakeThrowingPurger simulates CDN failures; SemanticLogTreeTrait provides schema validation and tree traversal helpers; SafeSemanticLoggerTest validates failure recovery; SemanticLogSchemaTest exercises dependency nesting and command causality; GracefulLoggingTest confirms cache works when logger throws.
Test updates to use semantic logger and validation
tests/DonutCacheInterceptorTest.php, tests/DonutCommandInterceptorTest.php, tests/DonutQueryInterceptorTest.php, tests/DonutQueryInterceptorPurgeTest.php, tests/DonutCommandRedisCacheTest.php, tests/QueryRepositoryTest.php, tests/ResourceRepositoryTest.php, tests/ResourceStorageTest.php, tests/RepositoryLoggerTest.php
All tests switch from RepositoryLoggerInterface to SemanticLoggerInterface; tearDown calls flushAndValidate for schema drift detection; assertions use log-tree operation types instead of JSON strings; ResourceStorageTest adds invalidateTags outcome tests.
Documentation, changelog, and demo updates
CHANGELOG.md, composer.json, tests/CACHE_DEPENDENCY_TESTS.md, demo/run-dependency.php, demo/run-donut.php
CHANGELOG documents migration and new dependency koriym/semantic-logger; CACHE_DEPENDENCY_TESTS.md expanded with observability section and schema validation details; demo scripts updated to render semantic log tree via TreeRenderer.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • bearsunday/BEAR.QueryRepository#170: Both PRs are tightly connected to the logger API/format migration: #170 changes RepositoryLoggerInterface/RepositoryLogger and updates cache call sites to emit structured JSON (log(op, context)), while the main PR continues that same logging evolution by deprecating the legacy repository logger and refactoring those same cache components (ResourceStorage, QueryRepository, DonutRepository, interceptors) to use SemanticLoggerInterface with typed context classes/scopes.

Suggested reviewers

  • NaokiTsuchiya
  • jingu
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@koriym

koriym commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (1e2700c) to head (c4da6fa).

Additional details and impacted files
@@             Coverage Diff              @@
##                 1.x      #178    +/-   ##
============================================
  Coverage     100.00%   100.00%            
- Complexity       254       332    +78     
============================================
  Files             53        80    +27     
  Lines            765       937   +172     
============================================
+ Hits             765       937   +172     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/StructuredRepositoryLoggerInterface.php (1)

7-17: 💤 Low value

Merge the two stacked docblocks into one.

Only the docblock immediately preceding the interface declaration (the @deprecated block) is attached by doc/IDE tooling; the first block describing the BC rationale is effectively dropped. Consider combining them.

♻️ Proposed merge
 /**
  * A repository logger that also exposes its entries as structured data
  *
  * Separated from RepositoryLoggerInterface so that adding structured accessors
  * does not break third-party RepositoryLoggerInterface implementations (BC).
  * Use this for structural assertions instead of substring-matching __toString().
- */
-/**
+ *
  * `@deprecated` Since the SemanticLogger migration; structured logs are now the
  *   {`@see` \Koriym\SemanticLogger\LogJson} tree returned by SemanticLogger::flush().
  */
 interface StructuredRepositoryLoggerInterface extends RepositoryLoggerInterface
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/StructuredRepositoryLoggerInterface.php` around lines 7 - 17, Combine the
two separate docblocks into a single docblock immediately above the
StructuredRepositoryLoggerInterface declaration: include the descriptive
paragraph about it being a repository logger exposing entries as structured data
and the BC rationale mentioning separation from RepositoryLoggerInterface, then
append the `@deprecated` tag with its message referencing SemanticLogger, LogJson,
and SemanticLogger::flush(); ensure the merged docblock replaces both existing
blocks so IDE/doc tooling sees the full description and deprecation together for
StructuredRepositoryLoggerInterface.
tests/SemanticLogTreeTrait.php (1)

46-58: 💤 Low value

Optional: unlink the temp file after validation.

tempnam() creates a file that is never removed, so each scenario leaves a slog* file in the system temp dir across the suite. Harmless but accumulates.

♻️ Suggested cleanup
         ob_start();
         try {
             (new SemanticLogValidator())->validate($file, $schemaDir);
         } finally {
             ob_get_clean();
+            `@unlink`($file);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/SemanticLogTreeTrait.php` around lines 46 - 58, The temp file created
via tempnam() and stored in $file is never removed; after calling (new
SemanticLogValidator())->validate($file, $schemaDir) ensure the temporary file
is unlinked—add an unlink($file) in the finally block (after ob_get_clean()) so
the temp slog* file is always deleted even on validation failure; reference the
$file variable and the validate call to locate where to add the cleanup.
src/QueryRepository.php (1)

49-60: ⚖️ Poor tradeoff

Manual-scope logging: isTopLevel() exists, and DI guarantees SafeSemanticLogger

  • put()/purge() only open manual scopes when the bound logger is SafeSemanticLogger and isTopLevel() is true; SafeSemanticLogger does define isTopLevel(): bool.
  • DonutCacheModule binds SemanticLoggerInterface as a singleton via SafeSemanticLoggerProvider (always returning SafeSemanticLogger), and the same pattern is used in ResourceStorage::invalidateTags(), so the top-level manual-scope path is satisfied for the actual runtime binding.
  • If replacing SemanticLoggerInterface with other implementations is a goal, introduce a narrower interface for the “top-level + manual scope” capability (instead of relying on instanceof SafeSemanticLogger).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/QueryRepository.php` around lines 49 - 60, The code uses instanceof
SafeSemanticLogger and isTopLevel() inside put()/purge() to decide whether to
open manual logging scopes; replace this instanceof-based runtime check with a
clearer contract by introducing a small interface (e.g.,
TopLevelManualScopeLogger with methods isTopLevel(): bool,
open(ManualStoreContext): mixed, close(ManualStoreResultContext, mixed): void),
have SafeSemanticLogger implement it, update the method signatures/DI usage to
type-hint/accept TopLevelManualScopeLogger where manual scopes are needed (or
use a safe cast via interface check), and update callers like
QueryRepository::put()/purge() and ResourceStorage::invalidateTags() to use that
interface instead of instanceof SafeSemanticLogger so other logger
implementations can opt in without relying on instanceof or concrete class
bindings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@psalm.xml`:
- Around line 23-29: psalm.xml currently suppresses the InvalidClassConstantType
rule for the entire src/Log/Context directory; instead, remove that
directory-wide suppression and add targeted suppressions for only the concrete
Context classes that override Koriym\SemanticLogger\AbstractContext's untyped
constants (specifically the classes that declare public const TYPE and public
const SCHEMA_URL). Locate each concrete *Context class that defines those
constants and add a file- or class-scoped Psalm suppression for
InvalidClassConstantType (or an inline `@psalm-suppress` on the class) so other
files in the directory remain checked normally.

In `@src/Log/NullSemanticLogger.php`:
- Around line 23-37: The no-op methods open, event, and close in
NullSemanticLogger are tripping PHPMD for unused parameters; fix by explicitly
consuming the parameters to silence the linter — add a void-cast or unset for
the parameters (e.g., (void)$context; and for close also (void)$openId;) at the
start of each method body so the methods remain no-ops but PHPMD no longer flags
the parameters as unused.

In `@src/Log/SafeSemanticLogger.php`:
- Around line 133-138: The __unserialize method in SafeSemanticLogger currently
ignores its $data parameter, triggering PHPMD UnusedFormalParameter; update
SafeSemanticLogger::__unserialize(array $data): void to consume $data (for
example with unset($data) or (void)$data;) before initializing $this->logger and
$this->broken, so the parameter is referenced and the linter warning is
suppressed while preserving existing behavior of new SemanticLogger() and
$this->broken = false.

---

Nitpick comments:
In `@src/QueryRepository.php`:
- Around line 49-60: The code uses instanceof SafeSemanticLogger and
isTopLevel() inside put()/purge() to decide whether to open manual logging
scopes; replace this instanceof-based runtime check with a clearer contract by
introducing a small interface (e.g., TopLevelManualScopeLogger with methods
isTopLevel(): bool, open(ManualStoreContext): mixed,
close(ManualStoreResultContext, mixed): void), have SafeSemanticLogger implement
it, update the method signatures/DI usage to type-hint/accept
TopLevelManualScopeLogger where manual scopes are needed (or use a safe cast via
interface check), and update callers like QueryRepository::put()/purge() and
ResourceStorage::invalidateTags() to use that interface instead of instanceof
SafeSemanticLogger so other logger implementations can opt in without relying on
instanceof or concrete class bindings.

In `@src/StructuredRepositoryLoggerInterface.php`:
- Around line 7-17: Combine the two separate docblocks into a single docblock
immediately above the StructuredRepositoryLoggerInterface declaration: include
the descriptive paragraph about it being a repository logger exposing entries as
structured data and the BC rationale mentioning separation from
RepositoryLoggerInterface, then append the `@deprecated` tag with its message
referencing SemanticLogger, LogJson, and SemanticLogger::flush(); ensure the
merged docblock replaces both existing blocks so IDE/doc tooling sees the full
description and deprecation together for StructuredRepositoryLoggerInterface.

In `@tests/SemanticLogTreeTrait.php`:
- Around line 46-58: The temp file created via tempnam() and stored in $file is
never removed; after calling (new SemanticLogValidator())->validate($file,
$schemaDir) ensure the temporary file is unlinked—add an unlink($file) in the
finally block (after ob_get_clean()) so the temp slog* file is always deleted
even on validation failure; reference the $file variable and the validate call
to locate where to add the cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 431b3a67-ada3-42a2-bbed-724f5a8f5882

📥 Commits

Reviewing files that changed from the base of the PR and between a4cd9e8 and c8744bf.

📒 Files selected for processing (81)
  • CHANGELOG.md
  • composer.json
  • demo/run-dependency.php
  • demo/run-donut.php
  • docs/schemas/context/cache_hit.json
  • docs/schemas/context/cache_miss.json
  • docs/schemas/context/command.json
  • docs/schemas/context/command_result.json
  • docs/schemas/context/depends_on.json
  • docs/schemas/context/get.json
  • docs/schemas/context/invalidate.json
  • docs/schemas/context/manual_invalidate.json
  • docs/schemas/context/manual_purge.json
  • docs/schemas/context/manual_purge_result.json
  • docs/schemas/context/manual_store.json
  • docs/schemas/context/manual_store_result.json
  • docs/schemas/context/purge.json
  • docs/schemas/context/put_donut.json
  • docs/schemas/context/refresh_donut.json
  • docs/schemas/context/save_donut.json
  • docs/schemas/context/save_donut_view.json
  • docs/schemas/context/save_etag.json
  • docs/schemas/context/save_value.json
  • docs/schemas/context/save_view.json
  • docs/schemas/repository-log.json
  • psalm.xml
  • src/AbstractDonutCacheInterceptor.php
  • src/CacheDependency.php
  • src/CacheInterceptor.php
  • src/CommandContextFactory.php
  • src/CommandInterceptor.php
  • src/DonutCacheModule.php
  • src/DonutCommandInterceptor.php
  • src/DonutRepository.php
  • src/Log/Context/CacheHitContext.php
  • src/Log/Context/CacheMissContext.php
  • src/Log/Context/CommandContext.php
  • src/Log/Context/CommandResultContext.php
  • src/Log/Context/DependsOnContext.php
  • src/Log/Context/GetContext.php
  • src/Log/Context/InvalidateContext.php
  • src/Log/Context/ManualInvalidateContext.php
  • src/Log/Context/ManualPurgeContext.php
  • src/Log/Context/ManualPurgeResultContext.php
  • src/Log/Context/ManualStoreContext.php
  • src/Log/Context/ManualStoreResultContext.php
  • src/Log/Context/PurgeContext.php
  • src/Log/Context/PutDonutContext.php
  • src/Log/Context/RefreshDonutContext.php
  • src/Log/Context/SaveDonutContext.php
  • src/Log/Context/SaveDonutViewContext.php
  • src/Log/Context/SaveEtagContext.php
  • src/Log/Context/SaveValueContext.php
  • src/Log/Context/SaveViewContext.php
  • src/Log/NullSemanticLogger.php
  • src/Log/SafeSemanticLogger.php
  • src/Log/SafeSemanticLoggerProvider.php
  • src/NullRepositoryLogger.php
  • src/QueryRepository.php
  • src/RefreshInterceptor.php
  • src/RepositoryLogger.php
  • src/RepositoryLoggerInterface.php
  • src/ResourceStorage.php
  • src/StructuredRepositoryLoggerInterface.php
  • tests/CACHE_DEPENDENCY_TESTS.md
  • tests/DonutCacheInterceptorTest.php
  • tests/DonutCommandInterceptorTest.php
  • tests/DonutCommandRedisCacheTest.php
  • tests/DonutQueryInterceptorPurgeTest.php
  • tests/DonutQueryInterceptorTest.php
  • tests/FakeThrowingPurger.php
  • tests/GracefulLoggingTest.php
  • tests/QueryRepositoryTest.php
  • tests/RecordingSemanticLogger.php
  • tests/RepositoryLoggerTest.php
  • tests/ResourceRepositoryTest.php
  • tests/ResourceStorageTest.php
  • tests/SafeSemanticLoggerTest.php
  • tests/SemanticLogSchemaTest.php
  • tests/SemanticLogTreeTrait.php
  • tests/ThrowingSemanticLogger.php
💤 Files with no reviewable changes (1)
  • docs/schemas/repository-log.json

Comment thread psalm.xml
Comment thread src/Log/NullSemanticLogger.php
Comment thread src/Log/SafeSemanticLogger.php Outdated
@koriym

koriym commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai Addressed in df8c8b6:

  • PHPMD UnusedFormalParameter: added @SuppressWarnings("PHPMD.UnusedFormalParameter") on NullSemanticLogger (class-level) and SafeSemanticLogger::__unserialize. Quoted so PHPStan's phpDoc parser accepts the dotted rule name. Preferred over scattering (void) casts through each no-op body.
  • Coverage: the no-op NullSemanticLogger methods and the best-effort catch blocks in SafeSemanticLogger::event()/close() were uncovered. Added NullSemanticLoggerTest and two SafeSemanticLogger failure tests, restoring 100% on the changed code.
  • Nitpick (StructuredRepositoryLoggerInterface): merged the two stacked docblocks into one.
  • Nitpick (SemanticLogTreeTrait): unlink() the temp file in the finally.

Not changed, with reasons:

  • instanceof SafeSemanticLogger + isTopLevel(): kept for consistency with the existing purge() path, which already uses the same check. Introducing a TopLevelManualScopeLogger interface for only the new call sites would be inconsistent; a holistic refactor across all manual-scope sites can be a separate change.
  • psalm.xml directory-wide InvalidClassConstantType suppression: every Log/Context class shares the same untyped-constant override from AbstractContext, so the directory-level suppression is intentional; per-class @psalm-suppress across ~18 classes would add more noise than it removes.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@koriym Thanks for the thorough follow-up! All the addressed items look good:

  • PHPMD suppression — class-level @SuppressWarnings on NullSemanticLogger and method-level on SafeSemanticLogger::__unserialize is cleaner than scattered (void) casts.
  • Coverage — adding NullSemanticLoggerTest and the two SafeSemanticLogger failure tests properly closes the gaps on the no-op bodies and best-effort catch blocks.
  • Docblock consolidation in StructuredRepositoryLoggerInterface and the finally-guarded unlink() in SemanticLogTreeTrait are solid improvements.

The reasoning for the two items left unchanged is sound as well:

  • instanceof SafeSemanticLogger + isTopLevel(): Keeping parity with the existing purge() path is the right call. A cross-cutting TopLevelManualScopeLogger interface is a worthwhile refactor but belongs in its own change.
  • Directory-wide Psalm suppression: With ~18 context classes all inheriting the same untyped constant from AbstractContext, a single <directory>-scoped suppression is far less noisy than per-class annotations that would all repeat the same justification.

Everything looks well-reasoned. Happy with the state of the PR! 🐇✨


🧠 Learnings used
Learnt from: CR
Repo: bearsunday/BEAR.QueryRepository PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-02T15:21:24.922Z
Learning: Applies to **/*.php : Follow PSR-12 coding standards as enforced by PHP_CodeSniffer

CI surfaced two checks the unpushed branch had never run locally:

- sa/PHPMD flagged UnusedFormalParameter on no-op / contract-driven signatures.
  Add @SuppressWarnings("PHPMD.UnusedFormalParameter") (quoted so PHPStan's
  phpDoc parser accepts the dotted rule name) on NullSemanticLogger and on
  SafeSemanticLogger::__unserialize, whose parameters are required by the
  interface / magic-method contract but intentionally unused.
- codecov (100% target) flagged the no-op NullSemanticLogger methods and the
  best-effort catch blocks in SafeSemanticLogger::event()/close() as uncovered.
  NullSemanticLogger is a pure no-op null object, so its methods are marked
  @codeCoverageIgnore; the SafeSemanticLogger catch blocks are the resilience
  guarantee, so they get real failure tests instead.

Also address review nitpicks: merge the two stacked docblocks on
StructuredRepositoryLoggerInterface, and unlink the temp file in
SemanticLogTreeTrait.
@koriym
koriym force-pushed the cache-observability branch from df8c8b6 to c5f2099 Compare June 3, 2026 14:51
@koriym

koriym commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai Follow-up (force-pushed c5f2099): for the codecov gap, NullSemanticLogger is a pure no-op null object, so its methods are now marked @codeCoverageIgnore (matching the existing /** @codeCoverageIgnore */ usage in the codebase) instead of a no-op test. The SafeSemanticLogger::event()/close() resilience paths keep their real failure tests.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@koriym That's the right call. @codeCoverageIgnore on a pure null-object is idiomatic PHP — it signals the skip is intentional rather than an oversight, avoids cluttering the test suite with trivial pass-through assertions, and aligns with the existing convention in the codebase. Keeping the real failure tests on SafeSemanticLogger::event()/close() where the resilience logic actually matters is the correct balance. 👍

🐇✨

Adversarial review flagged that the observability change had made CDN purge
silently best-effort: invalidateTags() swallowed a purger exception and still
reported success, so a write could leave stale CDN content unnoticed.

Restore the 1.x fail-closed behavior: the local pools are invalidated first, the
outcome is logged as cdn=failed, and the purge exception is then re-thrown to the
caller. The purger-failure test now asserts propagation instead of best-effort
masking.
@koriym
koriym force-pushed the cache-observability branch from 0eac6c6 to eb337fb Compare June 4, 2026 07:11
The adversarial review noted the singleton SemanticLogger session can drop a
request's log under concurrent coroutines (interleaved open/close violates LIFO,
SafeSemanticLogger marks the session broken, the flush is empty) rather than
merely interleave it. Sharpen the Known Limitations note: cache behavior is
unaffected (logging is a best-effort side-channel), PHP-FPM is unaffected, and a
request/coroutine-scoped logger is the robust fix, intentionally deferred to the
host flush-lifecycle work.
@koriym

koriym commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai Recording the reasoning for the design points raised in review (an out-of-band adversarial pass surfaced the same three):

  1. CDN purge — now fail-closed (eb337fb). ResourceStorage::invalidateTags() previously swallowed a purger exception and still reported success, which could silently leave stale CDN content after a write. Restored the 1.x fail-closed behavior: the local pools are invalidated, the outcome is logged as cdn: failed, then the purge exception propagates. Logging stays a side-channel and never masks the failure.

  2. The logger constructor type change is not a BC break. The constructor is not part of the public contract — neither QueryRepositoryInterface nor ResourceStorageInterface declares it, and consumers use the interfaces via DI (the module binds SemanticLoggerInterface). So no compatibility bridge is added; the RepositoryLoggerInterface binding is retained as an intentional, inert deprecation.

  3. Singleton logger under concurrent runtimes — documented + deferred to Make the SemanticLogger session request/coroutine-scoped for concurrent runtimes #179. The singleton binding is pre-existing (1.x bound the logger as a singleton too). Under Swoole/RoadRunner coroutines an interleaved open/close can drop a request's log. However: cache behavior is unaffected (logging is best-effort), the default PHP-FPM deployment is unaffected, and the robust fix (request/coroutine-scoped logger) belongs with the host flush-lifecycle. Documented in tests/CACHE_DEPENDENCY_TESTS.md and tracked in Make the SemanticLogger session request/coroutine-scoped for concurrent runtimes #179; intentionally out of scope here to keep this PR focused.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@koriym Thanks for documenting these — all three are well-reasoned:

  1. Fail-closed CDN purge: Restoring the 1.x behavior is the right call. Silently swallowing a purger exception and reporting success is a correctness hazard (stale CDN content with no signal). Logging cdn: failed as a side-channel and then propagating keeps the failure observable without masking it.

  2. BC non-issue: Correct framing. The constructor is an implementation detail; the public contract lives on QueryRepositoryInterface / ResourceStorageInterface. DI consumers bind through the module and never construct these classes directly, so the logger parameter type change is purely internal.

  3. Coroutine-scope deferral: Pragmatic. The singleton pattern is inherited from 1.x, PHP-FPM (the dominant deployment) is unaffected, and the fix belongs at the request-lifecycle layer of the host framework. Documenting it and tracking in #179 keeps the scope of this PR clean.

All three noted for future reviews. 🐇


✏️ Learnings added
Learnt from: koriym
URL: https://github.com/bearsunday/BEAR.QueryRepository/pull/178

Timestamp: 2026-06-05T06:52:10.071Z
Learning: In bearsunday/BEAR.QueryRepository, `ResourceStorage::invalidateTags()` is intentionally fail-closed for CDN purge: local pools are invalidated first, the outcome is logged as `cdn: failed` in `InvalidateContext`, and then the purger exception is allowed to propagate. This restores 1.x behavior and ensures failures are never silently swallowed.

Learnt from: koriym
URL: https://github.com/bearsunday/BEAR.QueryRepository/pull/178

Timestamp: 2026-06-05T06:52:10.071Z
Learning: In bearsunday/BEAR.QueryRepository, changing the logger constructor parameter from `RepositoryLoggerInterface` to `SemanticLoggerInterface` in `QueryRepository`, `ResourceStorage`, etc. is not a BC break. Constructors are not part of the public contract — neither `QueryRepositoryInterface` nor `ResourceStorageInterface` declares them — and all consumers obtain instances via DI using the module bindings.

Learnt from: koriym
URL: https://github.com/bearsunday/BEAR.QueryRepository/pull/178

Timestamp: 2026-06-05T06:52:10.071Z
Learning: In bearsunday/BEAR.QueryRepository, the `SemanticLoggerInterface` singleton binding under concurrent runtimes (Swoole/RoadRunner) can interleave open/close calls and drop request-scoped log entries. This is a known, pre-existing limitation (1.x also bound the logger as a singleton), cache behavior is unaffected since logging is best-effort, and the fix (request/coroutine-scoped logger) is tracked in issue `#179`. It is intentionally out of scope for PR `#178`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

koriym and others added 2 commits June 30, 2026 01:43
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
#	tests/ResourceStorageTest.php
@koriym

koriym commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai The status is still "request," but is it okay to mark them all as "approved"?

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@koriym Yes — all the items raised in this review round have been addressed or reasoned through satisfactorily (PHPMD suppression, coverage, docblock cleanup, CDN fail-closed behavior, the BC non-issue, and the singleton-logger deferral to #179). There's nothing outstanding blocking approval from my side.

The lingering "changes requested" status on GitHub is just a stale review state from an earlier round — it won't clear on its own from a discussion reply. To flip it to "approved," please post a top-level comment on the PR with exactly:

`@coderabbitai` approve

That will trigger the explicit approval action and update the review status accordingly.

(ノ・∇・)ノ

Resolve conflicts with the RFC 9110 ETag work (bearsunday#182, bearsunday#183):
- ResourceStorage::saveEtag() keeps the SemanticLogger event and stores
  the bare opaque-tag (trim quotes) as the pool key
- ResourceStorageTest keeps both the invalidation-outcome tests and the
  If-None-Match tokenization tests
koriym added 3 commits August 1, 2026 04:00
The log is the evidence an agent uses to verify the TTL-less,
event-driven cache; three gaps made that evidence misleading:

- SaveDonutContext/SaveDonutViewContext called the entry TTL "sMaxAge",
  though callers never pass a CDN s-maxage. Renamed to "ttl" (the public
  ResourceStorage::saveDonut() parameter keeps its name for BC, with a
  comment noting it carries the donut entry TTL).
- A saver that returned false still logged a successful-looking save.
  All five save contexts now carry the pool's "saved" outcome; the
  schemas mark it required and explain that false means NOT cached.
- A cache-server outage was indistinguishable from a cold miss. The
  interceptors now emit a cache_error event (uri + throwable message)
  before the warning, so a cache_miss after it reads as degradation.

Also clamp negative TTLs to 0 at the QueryRepository/ResourceStorage
boundary (past expiryAt, negative expirySecond or ttl argument), matching
the "minimum": 0 the schemas declare, and reword invalidate.json's "cdn"
description to the fail-closed semantics the code implements.
- Top-level purge() roots in a manual_purge scope; with a throwing
  purger the exception still propagates and the flushed tree closes
  with result "failed" and a nested cdn "failed" invalidate event.
- The command scope test now also pins the #[Purge] annotation and the
  purge / command_result entries.
- Second GET on a #[Cacheable] resource closes the get scope with
  cache_hit layer=resource (only the donut layers were pinned).
- The schema negative control passes an empty OBJECT context so the
  rejection provably comes from the JSON-schema layer, and unlinks its
  temp file in a finally.
- SafeSemanticLogger: LIFO violation against a real SemanticLogger
  delegate breaks the session, flushes empty, then recovers.
- Cache-down GET logs cache_error (uri + message) while the scope still
  closes cache_miss, so an outage is distinguishable from a cold miss;
  FakeErrorCache now throws on getItems with a "cache server down"
  message (TagAwareAdapter::getItem delegates to getItems).
- A pool that rejects on commit yields saved=false in save_value, and a
  negative ttl / past expiryAt is pinned to clamp to 0.
- llms.txt/llms-full.txt: replace the removed flat op-string "Repository
  Logger" section (and the deleted repository-log.json link) with the
  open/event/close tree, the per-context schemas, and a short guide to
  verifying the event-driven cache from logs (ttl null lives until an
  invalidate with a matching tag; saved/cdn outcome fields; cache_error
  means degraded, not cold).
- CHANGELOG: move repository-log.json to Removed, add manual_purge to
  the manual-scope entry, correct the SafeSemanticLogger default
  description, and add entries for cache_error, saved, the ttl renames
  and the TTL clamping.
- CACHE_DEPENDENCY_TESTS.md: CommandContextFactory is shared by three
  interceptors; the CDN purge is fail-closed, not best-effort; point to
  the real testInvalidateTagsFailsClosedWhenPurgerFails.
- CLAUDE.md: link the per-context schema directory.
koriym added 12 commits August 1, 2026 08:27
- save_view/save_donut now log the required tags written with the entry;
  save_etag/save_donut_view rename surrogateKeys to tags for consistency
- Add put_skipped context (uri + reason: etag-present | error-code) emitted
  when a cacheable result is intentionally not stored
- Command interceptors always open a command scope, skip commands on
  code >= 400, and always close with command_result; CommandContext gains
  a required source field (interceptor basename)
- Remove assert($saved) in ResourceStorage::saveDonut so failed donut
  writes surface as warnings instead of relying on zend.assertions
- Bound command_result code to 100-599 and fix put_donut /
  manual_store_result / command / invalidate schema descriptions;
  ttl descriptions now state 0 or null means no expiry is set
- Assert put_skipped is logged with reason etag-present (new SelfEtag
  fake page) and error-code, and that nothing is stored in either case
- Assert command scopes carry the interceptor source and that a failed
  (4xx) command still opens and closes a scope without running commands
- Pin tags on save_view/save_donut/save_etag/save_donut_view log records
- GracefulLoggingTest records E_USER_WARNING and asserts an Age-header
  cache hit; drop RecordingSemanticLogger::types() helper
- Rewrite the log-verification guides in llms.txt / llms-full.txt:
  five rules covering truthful TTLs (0 or null means no expiry),
  put_skipped reasons, command_result on 4xx, and pre-write invalidate
- Fix the CACHE_DEPENDENCY_TESTS.md table, flow example and PHP-FPM
  paragraph to match actual log output
- Note the semantic-log example file in the schema links and reword the
  demo comment as conforming output validated in the test suite
- CHANGELOG entries for the round-2 log-verifiability changes
- run-dependency.php scenario 3 is now a PUT on LevelThree, whose new
  onPut carries #[Purge]: the log shows a command scope (method,
  annotations, source) driving the surrogate-key cascade to level-two
  and level-one. Scenario 6 stays a manual purge, so both entry kinds
  (command scope vs top-level manual_purge scope) appear in one run
- run.php prints the semantic log (tree + pretty JSON) after the
  HTTP-level output; its AppModule now binds ArrayAdapter pools because
  the QueryRepositoryModule default NullAdapter made every GET miss,
  so the entry demo could never show a cache hit
- run-donut.php prints the same pretty JSON as run-dependency.php,
  reusing the flush taken for the tree
- All three demos validate the flushed log offline against
  docs/schemas/context via the shared demo/validate.php helper
  (SemanticLogValidator, schemaUrl basenames mapped to local files)
  and print a one-line verdict, exiting non-zero on any violation
- CacheDependencyTest::testWriteToGrandChildCascadesInvalidation mirrors
  testDestroyByGrandChild but drives the cascade with a write command
  (PUT on level-three) instead of a manual purge
- CACHE_DEPENDENCY_TESTS.md references the new test, notes the #[Purge]
  on LevelThree in the fake table, and ties the command-scope paragraph
  to demo scenario 3 vs the manual_purge entry kind in scenario 6
- CHANGELOG Added entries for the self-verifying demos, the
  command-driven scenario and the run.php pool fix
- invalidate cdn is now tri-state: purged (a configured purger ran),
  failed (it threw), skipped (the bound purger is NullPurger, no CDN
  configured) — a no-op purger no longer reads as a successful purge
- cache_error gains a required operation field (read|write) naming the
  failing side; both read interceptors and the write path set it
- put_skipped gains an optional code (the actual response status when
  reason is error-code), is now also emitted by CacheInterceptor on a
  non-200 GET (before the purge), and gains a not-cacheable reason
  emitted by DonutRepository when a refreshed donut page has no
  page-level entry to save; its description is reworded to state facts
  without judging whether skipping was correct
- save_etag gains the ttl field it previously dropped, and all save_*
  ttl descriptions state the 31536000 never-expiry convention so a
  blind reader does not read the log as a TTL-driven cache
- refresh_donut description corrected (template hit + re-render, not a
  cache-miss rebuild); cache_hit close description now states it
  reports only the final layer's outcome
- SafeSemanticLogger no longer silently wipes a broken session: the
  recovery flush returns a log_session_broken sentinel scope carrying
  the cause, falling back to the empty log only if the sentinel itself
  fails (never-throw guarantee stands)
- The invalidate schema's pre-write-cleanup rule is redefined as a
  machine-applicable predicate: a later same-scope save_* event whose
  tags include the invalidate's tags — regardless of scope type, with
  depends_on possibly in between; donut scopes match against
  save_etag/save_donut_view
- testInvalidateTagsWithNullPurgerLogsCdnSkipped (renamed from
  testInvalidateTagsRecordsSuccessfulOutcome) expects cdn=skipped with
  the default NullPurger; new testInvalidateTagsLogsCdnPurgedWithConfiguredPurger
  uses a minimal recording purger and expects purged; the fail-closed
  test still expects failed
- GracefulLoggingTest asserts cache_error carries operation=read
- New testNon200GetLogsPutSkippedWithActualCode: a 203 GET records
  put_skipped{error-code, code:203} plus a purge event
- DonutCacheInterceptorTest::testCached asserts the refresh of a
  not-entire-content-cacheable donut records put_skipped{not-cacheable}
- The two flush-failure SafeSemanticLogger tests now expect a
  log_session_broken sentinel (with reason) instead of a silent empty
  flush, and still prove the next session recovers
- New testSaveDonutLogsSavedFalseWhenPoolRejectsEntry mirrors the
  saveValue case, pinning that a rejected donut store is observable
- llms.txt / llms-full.txt: the pre-write-cleanup rule is the new
  tag-correlation predicate; the non-200 rule now reads
  put_skipped{error-code, code} + purge + invalidate; outcome fields
  cover the cdn tri-state, cache_error operation and the
  log_session_broken sentinel; new bullet states the ordering semantics
  (events time-ordered within a scope only; use nesting plus the next
  GET's hit/miss as ground truth)
- CACHE_DEPENDENCY_TESTS.md: context table gains log_session_broken and
  the new put_skipped/cache_error/invalidate shapes, the flow example
  gets the cleanup predicate, the test tables reference the renamed and
  new tests, and the Known Limitations reflect the sentinel and the
  not-cacheable skip
- CHANGELOG: Added entries for the cdn tri-state, cache_error
  operation, put_skipped code/not-cacheable, save_etag ttl and the
  log_session_broken sentinel; Changed entry for the redefined
  pre-write-cleanup rule
- CacheInterceptor: restructure the non-200 branch as an early return
  (ElseExpression sniff)
- ResourceStorage: resolve the no-CDN status by a branch-free lookup
  (CDN_OK_STATUS indexed by NullPurger-ness) so the class stays under
  the PHPMD complexity ceiling; a thrown purge is still "failed"
- SafeSemanticLogger: exclude the defensive sentinel-failure fallback
  from coverage (@codeCoverageIgnoreStart/End); codecov targets 100%
…arkers

php-code-coverage only recognizes bare // @codeCoverageIgnoreStart/End
tokens; the previous marker carried trailing prose and was silently
ignored, leaving the defensive never-throw fallback in flush() reported
as an uncovered line. Put the exact markers on the catch and return
lines, matching the established pattern in AbstractDonutCacheInterceptor.
The rationale stays in the class docblock's recovery paragraph.
@koriym

koriym commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai approve

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Comments resolved and changes approved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant