# M2 Cached Animation Resource Observer

## Metadata

| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-CACHED-ANIMATION-RESOURCE-OBSERVER-001` |
| Owners | Cached animated M2 eligibility, request admission and initial snapshot |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-cached-animation-resource-observer`, 2026-07-18 |
| Profiles/capabilities | Existing optional cached-GLB animated M2 path |

## Purpose

Produce the cached-animation phase of `M2BuildResourceSnapshot`: reuse an exact
animated prototype, wait for a pending request, admit the first safe GLB request,
or record a terminal static-only animation outcome without changing behavior.

## Non-goals

- Load/build native GryphonRoost animation or emit its diagnostics.
- Poll/finalize threaded Resources or instantiate/fix imported scenes.
- Own/free prototype Nodes, snapshots, pipeline entries or cache state.
- Materialize instances, consume permits or change animation policy defaults.
- Change GLB layout, cache format, batching, profiles or visible output.

## Context and boundaries

```mermaid
flowchart LR
    Loader[StreamingWorldLoader] --> Native[M2NativeAnimationResourceObserver]
    Native -->|no prototype| Observer[M2CachedAnimationResourceObserver]
    Prototype[M2PrototypeCacheState] --> Observer
    Pipeline[M2AnimationLoadPipelineState] --> Observer
    Resource[ResourceLoader and GLB JSON] --> Observer
    Observer --> Snapshot[M2BuildResourceSnapshot]
    Snapshot --> Dispatch[M2BuildDispatchPlanner]
```

## Public API

| Symbol | Kind | Purpose | Failure behavior |
|---|---|---|---|
| `observe(path, cache_dir, max_primitives, allow, deny, prototype_state, pipeline_state, debug)` | Command/query | Produce cached prototype/pending/static-only snapshot | Invalid input returns empty non-pending snapshot |
| `find_eligible_glb_cache_path(...)` | I/O query | Select first existing safe historical candidate | Returns empty String |
| `is_animation_path_allowed(path, allow, deny)` | Pure query | Apply stripped case-insensitive substring policy | Empty path/allowlist rejects; deny wins |
| `cache_resource_paths(cache_dir, path)` | Pure query | Return nested/lowercase/basename GLB candidates | Empty values yield fewer candidates |
| `glb_cache_is_safe_for_runtime_animation(path, max, debug)` | I/O query | Validate animations, primitive limit and schema | Invalid/missing GLB rejects |
| `read_glb_json(path)` | I/O query | Read version-2 GLB JSON chunk | Returns empty Dictionary |
| `glb_primitive_count(gltf)` | Pure query | Count primitives across meshes | Invalid entries contribute zero |
| `glb_animation_schema(gltf)` | Pure query | Read OpenWC schema marker | Missing marker returns empty String |

## Inputs and outputs

| Direction | Data | Producer | Consumer | Ownership/lifetime |
|---|---|---|---|---|
| Input | Normalized M2 path/cache directory | Loader | Observer | Copied Strings; one observation |
| Input | Primitive cap and allow/deny patterns | Loader exports | Observer | Copied/read-only call values |
| Input | Prototype and request states | M2 state services | Observer | Borrowed services; map/session |
| Input | Existing GLB cache file | Offline cache pipeline | Observer | Read-only file access |
| Output | Resource snapshot | Observer | Loader/dispatch/materializer | Caller-owned snapshot; one build step |
| Output | Borrowed exact prototype | Prototype state | Snapshot/materializer | No ownership transfer |
| Output | Threaded request record | Observer | Animation pipeline | Pipeline-owned copied paths |
| Output | Static-only transition | Observer | Prototype state | State-owned copied path |

## Data flow

```mermaid
flowchart TD
    Start[Observe normalized path] --> Valid{Valid composition?}
    Valid -->|no| Empty[Empty non-pending snapshot]
    Valid -->|yes| Cached{Prototype cached?}
    Cached -->|yes| Ready[Snapshot with exact prototype]
    Cached -->|no| Static{Already static-only?}
    Static -->|yes| Empty
    Static -->|no| Pending{Request exists?}
    Pending -->|yes| Wait[Pending snapshot]
    Pending -->|no| Policy{Allow and not deny?}
    Policy -->|no| Mark[Mark static-only]
    Policy -->|yes| Candidate[Historical GLB candidates]
    Candidate --> Safe{Animations, primitive cap, schema safe?}
    Safe -->|no usable| Mark
    Safe -->|yes| Request[Threaded request]
    Request -->|OK or busy| Remember[Remember and return pending snapshot]
    Request -->|error| Mark
    Mark --> Empty
```

## Lifecycle/state

```mermaid
stateDiagram-v2
    [*] --> Observing
    Observing --> Cached
    Observing --> Pending
    Observing --> Requested
    Observing --> StaticOnly
    Observing --> Rejected
    Cached --> [*]
    Pending --> [*]
    Requested --> [*]
    StaticOnly --> [*]
    Rejected --> [*]
```

## Main sequence

```mermaid
sequenceDiagram
    participant L as StreamingWorldLoader
    participant O as CachedAnimationObserver
    participant C as PrototypeCacheState
    participant P as AnimationLoadPipelineState
    participant R as ResourceLoader
    participant S as M2BuildResourceSnapshot
    L->>L: attempt native candidate
    alt no native prototype
        L->>O: observe(path, cache/policy/state)
        O->>C: find animated / static-only
        O->>P: has request
        opt eligible cache candidate
            O->>R: exists + GLB JSON + threaded request
            O->>P: remember request
        end
        O-->>L: snapshot
    else native prototype
        L->>S: construct native snapshot
    end
```

## Dependency diagram

```mermaid
flowchart TB
    Observer[M2CachedAnimationResourceObserver] --> Snapshot[M2BuildResourceSnapshot]
    Observer --> Prototype[M2PrototypeCacheState]
    Observer --> Pipeline[M2AnimationLoadPipelineState]
    Observer --> ResourceLoader
    Observer --> FileAccess[GLB header and JSON]
    Observer -. no dependency .-> Native[M2 native builder/repository]
    Observer -. no dependency .-> Finalizer[M2AnimatedSceneFinalizer]
    Observer -. no dependency .-> Scheduler[RenderBudgetScheduler]
    Observer -. no dependency .-> SceneTree
```

## Ownership, threading and resources

- Observer retains no service, request, snapshot, Resource or Node reference.
- Prototype state retains accepted Nodes and static-only paths until shutdown.
- Pipeline state retains copied request paths until terminal polling/finalize.
- Snapshot borrows the exact cached prototype; observer never frees it.
- Resource existence/request and synchronous GLB JSON inspection run on the
  renderer main thread, matching the previous loader behavior.
- The sibling native observer owns the native build attempt and the animation
  resource finalizer owns terminal polling/finalize. Loader retains permits,
  material lookup, materialization and SceneTree mutation.

## Errors, cancellation and recovery

| Failure/state | Behavior | Recovery |
|---|---|---|
| Empty path/dependency | Return empty non-pending snapshot without mutation | Correct composition |
| Cached prototype | Return exact reference immediately | None |
| Existing request | Return pending without duplicate admission | Retry after terminal drain |
| Disallowed/denied path | Mark static-only | New loader session/configuration |
| Missing/invalid/unsafe GLB | Continue candidates, then mark static-only | Repair/rebake cache and restart session |
| Request error | Mark static-only | New loader session/cache repair |
| Tile cancellation | Observer retains nothing | Loader queue remains authoritative |
| Shutdown | Loader drains requests before state clear | New loader starts empty |

## Configuration and capabilities

The service consumes existing `m2_cache_dir`, `m2_animated_max_primitives`,
`m2_animated_allowlist_patterns`, `m2_animated_denylist_patterns` and debug flag.
It adds no setting and preserves runtime mutability and defaults.

## Persistence, cache and migration

No persistence or cache format changes are introduced. Existing `.glb` candidate
layout and OpenWC schema markers are read unchanged; no rebake is required.

## Diagnostics and observability

Unsupported non-empty schemas preserve the historical optional
`M2_ANIM_REJECT` debug line. Snapshot and pipeline diagnostics remain detached;
queue and hitch metrics are unchanged.

## Verification

- Dedicated verifier covers invalid/cached identity/static/pending/missing
  lifecycle, allow/deny semantics, exact path order, generated GLB header/JSON,
  animation presence, primitive cap, accepted/rejected schemas, source boundaries
  and bounded timing.
- Snapshot, dispatch, pipeline, prototype, shutdown, facade, internal-access and
  checkpoint regressions protect adjacent behavior.
- Fidelity evidence is exact policy/I/O extraction. No original-client visual or
  animation parity claim is made.

## Extension points

Native and cached observation remain sibling services. Terminal ResourceLoader
polling/finalization uses the dedicated finalizer while retaining pipeline state.

## Capability status

| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Cached animation observation/request | Implemented extraction | Lifecycle/policy/GLB/source/timing verifier | Asset-backed traversal pending |
| Native animation observation | Implemented sibling extraction | Native observer lifecycle/source verifier | Asset-backed traversal pending |
| Animated finalize/preparation | Implemented finalizer extraction | Pipeline/finalizer regressions | Asset-backed traversal pending |

## Known gaps and risks

- A successful asynchronous request is not started by the unit fixture because
  leaving it undrained leaks process work; source and pipeline tests cover admission.
- Generated GLB metadata fixtures validate policy, not Godot import fidelity.
- Private traversal, leak/descriptor pressure, p95/p99 and paired visuals remain
  unavailable without private assets.

## Source map

| Path | Responsibility |
|---|---|
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached GLB policy, request and snapshot production |
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step observation value |
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending request/finalize state |
| `src/render/m2/m2_animation_resource_finalizer.gd` | Terminal polling/load/finalize outcomes |
| `src/render/m2/m2_prototype_cache_state.gd` | Prototype and static-only outcomes |
| `src/render/m2/m2_native_animation_resource_observer.gd` | Native-first candidate/read/build/cache observation |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, fallback order, finalize and execution |
| `src/tools/verify_m2_cached_animation_resource_observer.gd` | Lifecycle/policy/GLB/boundary/timing regression |

## Related decisions and references

- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md)
- [`m2-native-animation-resource-observer.md`](m2-native-animation-resource-observer.md)
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
- [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md)
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
