Pet architecture for v0.2.0

Document versionDateSummary
v12026-07-20Define the desktop reference app architecture
v22026-07-20Record composition and gate selection
v32026-07-20Record SDL and desktop lifecycle boundaries
v42026-07-20Record diagnostics and elapsed time
v52026-07-21Define the manifest and runtime asset layout
v62026-07-21Record manifest and asset components
v72026-07-21Record SDL asset loading
v82026-07-22Define presentation roles and animation
v92026-07-22Record SDL rendering and fallback behaviour
v102026-07-22Record layout and asset-backed rendering
v112026-07-24Accept the architecture and release evidence

Status: Accepted

Product version: v0.2.0

Milestone: V0 software companion foundation

Release: Desktop reference app

General architecture: Pet architecture

Template: arc42

Versioning: Semantic Versioning 2.0.0

1. Introduction and goals

1.1 Purpose

Version v0.2.0 introduces the first real desktop application and the first visual frontend. The release proves that the v0.1.0 Core can be presented and interacted with through a platform host, an SDL frontend, canonical visual assets, deterministic animation, semantic input, and composition-aware verification.

The release is more than a minimal visual demo. It is the reference implementation for the app, frontend, asset, and presentation boundaries that later desktop, embedded, and hardware frontends will need.

flowchart LR
    U[User]
    A[pet_desktop]
    C[Pet Core]
    S[PetSnapshot]
    R[Presentation roles]
    F[SDL frontend]
    V[Visible frame]

    U --> A
    A --> C
    C --> S
    S --> R
    R --> F
    F --> V
    F --> A

1.2 Architecture goals

The release must prove that:

  • the Core remains independent of SDL, files, images, windows, input devices, and platform clocks
  • one desktop app can compose the Core with a selected frontend
  • frontend rendering uses read only presentation state rather than mutable world storage
  • raw platform input becomes semantic product actions before it reaches the Core
  • host control events remain outside product actions
  • canonical visual assets can be loaded through a bounded manifest
  • missing or invalid external assets have a calm fallback presentation
  • animation selection is deterministic from explicit elapsed time
  • app and frontend source layout is predictable before more frontends arrive
  • gates can select supported app and frontend compositions without duplicating compatibility rules

1.3 Included capabilities

  • pet_desktop desktop host application
  • frontends/sdl SDL visual frontend component
  • pinned SDL dependency used only by the SDL frontend
  • app and frontend composition contract
  • declarative composition registry driving the build and gate workflows
  • canonical visual assets stored once in the repository
  • manifest-based asset and animation mapping
  • PNG as the canonical raster source format
  • guaranteed fallback rendering path
  • presentation role mapping from PetSnapshot
  • deterministic animation model
  • logical canvas and scaling policy
  • keyboard and pointer input mapped to supported semantic actions
  • Linux desktop verification evidence
  • continued headless Core and host verification

1.4 Excluded capabilities

  • physical hardware frontend
  • embedded runtime implementation
  • production packaging or installers
  • production quality art direction
  • audio implementation
  • local persistence and save compatibility
  • activity adapters
  • networking, synchronisation, peer discovery, and pet transfer
  • multiple pets
  • complete progression, maturity, memory, and formative event systems
  • asset editor, hot reload, sprite atlas generation, and complete conversion pipeline
  • menu and application chrome asset pipeline
  • full menu, theme, text rendering, and localisation system
  • generic host support library
  • generic frontend support library
  • runtime frontend plugin system

Excluded capabilities must not leave placeholder dependencies inside the Core.

1.5 Stakeholders

Developer

Implements and verifies the first visual product path.

Future frontend developer

Depends on the SDL frontend proving a reusable boundary without becoming the only frontend shape.

Future embedded developer

Depends on the asset and presentation contracts not assuming a desktop window, filesystem, GPU, or large runtime dependency inside the Core.

Product reviewer

Uses pet_desktop to evaluate whether the pet has the intended calm presence and direct interaction loop.

2. Architecture constraints

2.1 Product constraints

  • the release belongs to the V0 milestone
  • one world contains one active pet
  • the release must preserve the non-punitive companion principles in the specification
  • supported direct input may use only Core-supported semantic actions
  • excluded product features must not be simulated in the frontend as if they were Core behaviour
  • the first visual style may use simple art, but the asset contract is not pixel-art-only

2.2 Technical constraints

  • the Pet Core remains portable ISO C99
  • the Core does not include or link SDL
  • the Core does not know asset identifiers, image formats, file paths, animation identifiers, or renderer details
  • the Core does not read a platform clock or input device
  • pet_desktop calls only public Core APIs
  • the SDL frontend never receives a mutable PetWorld *
  • app and frontend components use explicit include/, src/, and tests/ ownership for new v0.2.0 components
  • public frontend headers expose the app-facing frontend contract, not SDL implementation internals
  • third-party headers and generated files are not treated as project code by diagnostics

2.3 Documentation and language constraints

  • code and developer documentation use British English
  • C domain identifiers use the pet prefix
  • version architecture documents follow the policy in arch README
  • important choices with alternatives are recorded in ADRs under docs/adr/
  • idea documents under docs/ideas/ are non-authoritative until their content moves here, into an ADR, or into milestone, epic, or PBI documents

2.4 Verification constraints

  • Linux desktop verification is required before release completion
  • Windows remains unverified unless a real Windows environment or runner executes the checks
  • embedded suitability remains an architecture claim until an embedded build exists
  • rendering evidence must distinguish automated checks, dummy-driver checks, and manual observation
  • the default headless composition must continue to pass

3. System scope and context

3.1 Product context

The release gives a user one local desktop application. The user can observe the pet and trigger a small direct interaction. There is no account, network service, persistence, adapter, or device transfer.

flowchart TD
    U[User]
    D[Desktop OS]
    A[pet_desktop]
    F[SDL frontend]
    C[Pet Core]
    AS[Canonical assets]

    U --> D
    D --> A
    A <--> F
    A <--> C
    F --> AS
    F --> U

3.2 Technical context

The desktop app is the imperative host. It owns process lifecycle, selected component composition, platform time acquisition, Core lifecycle, event dispatch, snapshot requests, and shutdown ordering.

The SDL frontend owns rendering resources, asset loading, layout, animation selection, raw input collection, raw input classification, and fallback presentation. It returns product actions and host controls to the app through a frontend event contract.

The Core owns authoritative state, product transitions, validation, and derived snapshots.

3.3 External interfaces

The release interfaces are:

  • process launch and exit status for pet_desktop
  • SDL window, renderer, and input facilities inside the SDL frontend
  • development runtime asset files copied from canonical source assets
  • public Core API consumed by the desktop app
  • public SDL frontend API consumed by the desktop app
  • CTest scenarios and verification scripts used by developers

No external network, storage, sensor, plugin, or audio interface is present.

4. Solution strategy

4.1 Functional core and imperative shell

pet_desktop is the imperative shell for this release. The Core remains the functional centre and receives only explicit values. The SDL frontend performs presentation effects, but it does not own product state.

flowchart LR
    E[Platform time and raw input]
    A[Desktop host]
    C[Pet Core]
    S[Snapshot]
    F[SDL frontend]
    O[Window output]

    E --> A
    A --> C
    C --> S
    A --> F
    S --> F
    F --> A
    F --> O

4.2 Main strategies

  1. keep product behaviour in the Core
  2. keep platform effects in the desktop app and SDL frontend
  3. use a declared app and frontend composition instead of hard-coded build assumptions
  4. map snapshots through the shared presentation component before selecting images to render
  5. use fallback roles and fallback rendering for unknown or missing presentation data
  6. load canonical assets outside the render loop
  7. keep asset and animation identifiers out of PetSnapshot
  8. keep semantic input as the only path from raw desktop input to Core actions
  9. verify boundaries through scripts and component-owned tests
  10. defer shared host and frontend support libraries until duplication evidence exists

4.3 Technology strategy

SDL is the selected desktop frontend technology for v0.2.0, recorded in ADR-006. SDL is a frontend dependency only. The implemented SDL frontend pins SDL 3.4.10 through FetchContent in cmake/pet_sdl.cmake. Local dependency override variables are not part of the E10 implementation; LIM-005 tracks the future project-wide override policy for fetched dependencies.

PNG is the canonical raster source format for visual assets, and a small bounded manifest maps images, animations, and presentation roles, recorded in ADR-007. Generated target formats may be introduced later, but they are build outputs rather than source of truth.

5. Building block view

5.1 Level 1 building blocks

flowchart TD
    subgraph App["apps/desktop"]
        A[pet_desktop]
    end

    subgraph Frontend["frontends/sdl"]
        F[pet_frontend_sdl]
        AN[Animation model]
        L[Layout]
        I[Input mapping]
        R[Renderer]
    end

    subgraph Shared["components"]
        M[pet::manifest]
    end

    C[pet::core]
    AS[assets/pet]
    G[composition registry]
    T[tests and gates]

    A --> C
    A --> F
    F --> M
    F --> AS
    M --> AS
    G --> A
    G --> F
    T --> A
    T --> F
    T --> M
    T --> C

5.2 Pet Core

The Core remains the authority for world state and product transitions. It provides snapshots that presentation code may read.

The Core must not:

  • include SDL headers
  • load assets
  • parse the visual manifest
  • know presentation role identifiers
  • choose animation frames
  • read the platform clock
  • receive raw SDL, keyboard, pointer, or window events
  • implement behaviour only needed to make the renderer easier

v0.2.0 should not require Core API changes. A Core change is justified only when the specification requires a new semantic action, a new presentable snapshot field, or a product rule that cannot be represented by the existing public API.

5.3 Desktop app

apps/desktop owns the pet_desktop executable target. It is a product application and the first platform host.

Responsibilities:

  • initialise one PetWorld
  • create and destroy the selected frontend
  • acquire monotonic platform time
  • compute and bound elapsed deltas
  • poll frontend events through the frontend API
  • dispatch product actions through pet_world_apply_action
  • call pet_world_update
  • request PetSnapshot
  • pass snapshots and explicit animation time to the frontend
  • map failures to process status and diagnostics
  • shut down cleanly after host control events

The app must not own renderer internals, asset decoding, animation frame selection, product behaviour, Core transition rules, or Core internal headers.

5.4 SDL frontend

frontends/sdl owns the pet_frontend_sdl library target. It presents snapshots and returns frontend events to the app.

Responsibilities:

  • create and destroy SDL presentation resources
  • consume the frontend-neutral asset set produced by shared asset components
  • decode accepted PNG paths into SDL-owned image resources at initialisation and reload points
  • consume shared presentation role and animation selection results
  • own SDL window and renderer handles privately
  • consume shared logical canvas layout and scaling results
  • render guaranteed fallback frames before asset-backed drawing exists
  • render asset-backed frames after layout and sprite drawing PBIs land
  • collect raw keyboard, pointer, window, and focus events
  • classify raw input into product actions, host controls, frontend events, or ignored input

The frontend must not own PetWorld, product transitions, persistence, app loop policy, elapsed time policy, adapters, or Core internal headers.

5.5 Canonical assets

Canonical visual assets live under assets/pet/. sprites/ is the initial pet presentation image directory, and the manifest is assets/pet/manifest.petasset. The initial source layout is:

assets/
    README.md
    pet/
        manifest.petasset
        sprites/
            fallback.png
            idle.png
            curious.png
            attentive.png
            happy.png

The exact filenames may change during asset PBIs, but the ownership rule may not: canonical assets are stored once and are not duplicated per frontend. Desktop runtime copies and future embedded binary forms are derived artefacts. assets/README.md is user-facing documentation for someone browsing the tracked asset tree, and the full manifest schema is recorded in asset manifest v1.

The set above is implemented. The sprites are small calm PNG images rather than production art, and assets/CMakeLists.txt owns the pet_runtime_assets target that copies the tree into the build output described in section 7.5. The copy is driven per file, so editing a canonical asset refreshes the runtime copy on the next build without a reconfigure.

5.6 Proposed source layout

assets/
    CMakeLists.txt
    README.md
    pet/
 
components/
    assets/
        CMakeLists.txt
        include/
            pet_assets/
                assets.h
                reader.h
        src/
            assets.c
            reader_stdio.c
        tests/
            test_runner.c
            test_suites.h
            test_pet_assets_manifest.c
            test_pet_assets_reader.c
            test_pet_assets_fallback.c
            test_pet_assets_runtime.c
            doubles/
                reader_memory.c
                reader_memory.h
    manifest/
        CMakeLists.txt
        include/
            pet_manifest/
                manifest.h
        src/
            manifest.c
        tests/
            test_runner.c
            test_suites.h
            test_pet_manifest_parse.c
            test_pet_manifest_grammar.c
            test_pet_manifest_schema.c
            test_pet_manifest_limits.c
    presentation/
        CMakeLists.txt
        include/
            pet_presentation/
                presentation.h
        src/
            presentation.c
        tests/
            test_runner.c
            test_suites.h
            test_pet_presentation_roles.c
            test_pet_presentation_animation.c
    layout/
        CMakeLists.txt
        include/
            pet_layout/
                layout.h
        src/
            layout.c
        tests/
            test_runner.c
            test_suites.h
            test_pet_layout_fit.c
 
apps/
    include/
        pet_app/
            status.h
    desktop/
        CMakeLists.txt
        README.md
        include/
            pet_app_desktop/
                app.h
                clock.h
                diagnostics.h
                config.h
                status.h
        src/
            main.c
            app.c
            clock.c
            config.c
            status.c
        tests/
            test_scenario.cmake
 
frontends/
    README.md
    README.template.md
    sdl/
        CMakeLists.txt
        README.md
        include/
            pet_frontend_sdl/
                frontend.h
                types.h
        src/
            frontend.c
            backend.c
            backend.h
            assets_sdl.c
            assets_sdl.h
            input.c
            input.h
            renderer.c
            renderer.h
        tests/
            test_runner.c
            test_suites.h
            test_frontend.c
            test_assets.c
            test_renderer.c
            test_sdl_input.c
 
config/
    compositions.json
 
tools/
    compositions.py
    tests/
        test_compositions.py
    verify/
        assets.py
        dependencies.py
        includes.py

Files are added by implementation PBIs only when they have a real responsibility. The layout records ownership, not a requirement to create empty placeholders.

5.7 Composition model

The valid initial compositions are:

AppFrontendComposition nameMeaning
headlessnoneheadlessdiagnostic host without a frontend
desktopsdldesktop-sdldesktop product app with SDL frontend

Compatibility is declared by the app. The project does not generate an unrestricted Cartesian product of every app and every frontend.

The registry lives at config/compositions.json with schema version 1. It records app names, frontend names, directories, targets, default frontend selection, and valid app-owned frontend lists. tools/compositions.py validates and expands the registry and exposes import-safe helpers for the gate. CMake uses the same registry, or generated metadata derived from it, to configure only the selected directories.

A composition name is derived rather than stored. A frontend that declares no directory and no target is not a real component, so it contributes no suffix and the composition takes the app name alone. Any other frontend produces <app>-<frontend>. Deriving the name keeps one source of truth for the build directory and report names that workflows use.

A future frontend is added as one frontends entry with its directory and target, and it becomes valid only for the apps that then list it in their own frontends array. A future app is added as one apps entry naming its directory, target, supported frontends, and default frontend. The default frontend must appear in the same app’s supported list. tools/compositions.py validate rejects a dangling reference, a duplicate frontend, an unexpected key, and a partially declared frontend component. A schema change that breaks existing consumers raises schema_version.

5.8 Dependency rules

flowchart TD
    AD[apps/desktop]
    AH[apps/headless]
    FS[frontends/sdl]
    CA[components/assets]
    CM[components/manifest]
    CP[components/presentation]
    CL[components/layout]
    API[Public Pet API]
    CORE[Pet Core]
    SDL[SDL dependency]
    AS[assets]

    AD --> API
    AH --> API
    AD --> FS
    FS --> API
    FS --> SDL
    FS --> CA
    FS --> CP
    FS --> CL
    CA --> CM
    CA --> AS
    CP --> API
    CP --> CM
    API --> CORE
  1. the Core depends on no app, frontend, asset, shared component, or SDL target
  2. apps depend on the public Core API
  3. apps/desktop depends on the selected frontend target
  4. frontends/sdl depends on SDL and the public Core presentation types it consumes
  5. frontends/sdl uses shared asset records but owns every SDL image resource it creates
  6. a shared component may depend on the public Core API or another shared component when its contract consumes their public records, but it depends on no app, frontend, SDL, or Core internal header
  7. components/assets owns asset-set resolution and reader contracts, while components/manifest owns text parsing and validation
  8. components/presentation owns snapshot-to-role mapping and deterministic manifest animation selection
  9. components/layout owns frontend-neutral logical canvas fit calculations
  10. frontend implementation headers stay private to the frontend target
  11. app component headers stay local to the executable component
  12. third-party diagnostics are isolated from project diagnostics

5.9 Shared components

A shared component under components/ is a library that belongs to no single app and no single frontend. It exists when a contract is common to several consumers and would otherwise be copied into the first one that needs it. src/ and include/pet/ stay the Core alone, so a shared component cannot live there without making the Core look like the owner of work the Core must not do.

Each shared component owns its own include/, src/, and tests/, and every composition configures it. Its tests therefore run in the default headless gate rather than only in the composition of the frontend that consumes it.

components/manifest owns the pet_manifest library target, which implements the bounded manifest parser described in section 8.3. A future shared component is added the same way when either duplication evidence or an explicit architecture decision shows that the contract is frontend-neutral from its first implementation.

components/assets owns the frontend-neutral asset-set loading contract. It resolves an asset root, reads the manifest through an injected reader, calls components/manifest, and reports either accepted manifest records or a fallback-required result. The contract is reader based rather than hard-wired to desktop file I/O, so desktop builds can use a standard C runtime reader while future embedded builds can provide flash, packed-array, or generated-binary readers without changing frontend logic.

components/presentation owns the frontend-neutral presentation decision contract. It consumes a public PetSnapshot, accepted PetManifest records, and explicit animation time, then returns bounded role, animation, frame, and image indexes for a frontend to render. It contains no SDL type, no image resource, no file reader, no clock read, and no allocation. SDL consumes its output but does not own the reusable role priority or animation looping policy.

components/layout owns the frontend-neutral logical canvas layout contract. It consumes logical canvas and output sizes and returns deterministic rectangles and scale decisions for frontends to use when drawing fallback or asset-backed frames. It contains no SDL type, window, renderer, texture, surface, file reader, clock read, or allocation. SDL may adapt the returned rectangle into SDL_FRect or renderer state, but it does not own the reusable aspect-ratio and scaling policy.

The contract is pet_layout_fit, which takes a logical PetLayoutSize and an output PetLayoutSize and answers with a PetLayoutFit: the destination PetLayoutRect and the whole multiple it was scaled by, or zero when the fit is not a whole multiple. The canvas keeps its aspect ratio and is centred, so an output of a different shape carries even bars. The scaled extents are floored, which lands a whole multiple exactly and leaves every other output on one rectangle rather than on a rounding choice, and an output too small for one whole unit still receives one unit. The calculation uses 64-bit intermediates, so the largest 32-bit extents answer without overflowing.

The release logical canvas is 320 by 240, chosen by apps/desktop. The frontend opens its window at that size, and it is a resizable window because the canvas is fitted into the current output surface on every frame rather than tied to the size the window opened at.

The presentation role contract is PetPresentationRole, pet_presentation_role_from_snapshot, and pet_presentation_role_id. A frontend maps a snapshot to a role and asks for the identifier of that role, so no frontend holds a role string, a role table, or a Core enum branch of its own. PET_PRESENTATION_ROLE_FALLBACK_DEFAULT is the zero role, which makes cleared storage already presentable and makes the fallback the value a consumer reaches by doing nothing rather than by handling an error. The identifiers are the ones the [roles] section of an asset manifest declares, so they resolve through an accepted manifest without a second naming scheme.

The role contract takes only a snapshot, so a frontend with no asset set, no filesystem, and no manifest can still link this component and present. Manifest animation and frame selection sits beside it rather than inside it, which is what kept the role contract unchanged when the consumer also needed the selected image.

Selection is pet_presentation_select_frame, which takes an accepted PetManifest, a role, and an animation time, and answers with a PetPresentationSelection: the role that was asked for, and the manifest role, animation, frame, and image indexes with the duration of the selected frame. Every value is an index into the manifest the caller already holds, so a renderer reaches its records without searching and without holding an identifier of its own. The frame index addresses the shared frame pool rather than counting from the start of the animation.

A role is resolved by its identifier. A role the manifest does not declare, and a role whose chain does not resolve, both select the manifest fallback role and set fallback_used. A single-frame animation selects its one frame at every time. A multi-frame animation loops over the total duration of its frames, so time zero and the total duration select the same frame, and a time equal to the first frame duration selects the second frame. Nothing reads a clock, a path, or an allocation, so equal arguments always select equal records.

An accepted manifest cannot fail this call, because its fallback role resolves through the whole chain. The rejection exists for storage no parse produced, such as a generated or badly copied manifest, which is answered with an empty selection rather than trusted.

6. Runtime view

6.1 Desktop launch and shutdown

sequenceDiagram
    participant App as pet_desktop
    participant Core as Pet Core
    participant Frontend as SDL frontend

    App->>Core: pet_world_init
    Core-->>App: status
    App->>Frontend: create frontend
    Frontend-->>App: status
    App->>Core: pet_world_snapshot
    Core-->>App: snapshot
    App->>Frontend: render snapshot
    Frontend-->>App: status
    App->>Frontend: destroy frontend
    App->>Core: pet_world_reset

Startup failure cleans up already-created resources in reverse order. Normal shutdown destroys frontend resources and resets Core storage.

6.2 Update and render loop

sequenceDiagram
    participant App
    participant Frontend
    participant Core

    App->>Frontend: poll events
    Frontend-->>App: frontend events
    App->>Core: apply semantic actions
    Core-->>App: status
    App->>App: compute bounded elapsed delta
    App->>Core: pet_world_update(delta)
    Core-->>App: status
    App->>Core: pet_world_snapshot
    Core-->>App: snapshot
    App->>Frontend: render(snapshot, animation_elapsed)
    Frontend-->>App: status

The app supplies both Core delta time and frontend animation time explicitly. The frontend does not read wall-clock time during animation selection.

6.3 Input classification

Raw SDL events are classified inside the SDL frontend.

Event categoryExampleDestination
product actionkey or pointer mapped to PET_ACTION_GREETapp dispatches to Core
host controlquit requestapp loop and process lifecycle
frontend eventresize or focus changefrontend local state
ignored inputunsupported key or buttonno effect

Unsupported desired actions are not invented in the frontend. They wait for Core support.

6.4 Asset loading and fallback

The shared asset component resolves the runtime asset root and reads the manifest during frontend initialisation or an explicit reload point. Ordinary rendering consumes already validated asset and animation records. The manifest parser remains text-only; file discovery and reader policy sit one layer above it.

pet_assets_load answers with an accepted set or with the fallback. It joins the asset root once, reads the manifest through the injected reader, parses it, and then reads the leading bytes of every image the manifest names to check that the file is reachable and begins with the PNG signature. An asset set with one image missing, unreadable, or not an image at all is not accepted, because the fallback chain has to resolve through every record the manifest declares, and half an asset set has no defined presentation. A rejected set carries the reason and an empty manifest, so a consumer cannot present records it never accepted.

The SDL frontend keeps the accepted records beside the resources it decoded from them, so presenting selects a frame without reading a file, and a released or reloaded set takes its records with it. The SDL frontend consumes accepted asset records and turns referenced PNG files into SDL-owned resources. If the manifest, a referenced file, or an SDL image load fails, the frontend uses a built-in or procedural fallback representation. The fallback is a normal calm presentation path, not an error screen.

pet_frontend_sdl_load_assets is that step. It is called at frontend initialisation and is also the reload point, so a frame never decodes a file. Resources from an earlier load are released before anything new is decoded, and an image that fails to decode releases the images the same load already took before it reports the fallback, so a set is held whole or not at all. The frontend then reports PET_FRONTEND_SDL_ASSETS_LOADED or PET_FRONTEND_SDL_ASSETS_FALLBACK, and an app reads that summary rather than any resource. A set that cannot be used is not a failed call, so a session continues on the fallback instead of ending.

pet_frontend_sdl_create opens the window and the renderer, and pet_frontend_sdl_present clears, draws, and presents one frame through them. A frontend holding an accepted set draws the image its role and animation time selected, over the placed canvas; every other frontend draws the built-in fallback, which is why a missing asset set ends no session. Both handles stay opaque in the public contract, so an app owns the lifecycle without owning a renderer. A setup step that fails releases what the steps before it took and reports PET_FRONTEND_SDL_ERR_RENDERER, which leaves storage a caller destroys like any other.

6.5 Presentation role flow

flowchart LR
    S[PetSnapshot]
    P[components/presentation mapping]
    R[Role]
    A[Manifest animation]
    FR[Frame]
    IM[Image index]
    OUT[Rendered output]

    S --> P
    P --> R
    R --> A
    A --> FR
    FR --> IM
    FR --> OUT
    IM --> OUT

components/presentation owns the mapping and selection work through the frame and image index. The renderer consumes that selection result and the resources the frontend already loaded. It does not branch directly on mutable Core world state or on Core enum values.

7. Deployment view

7.1 Logical outputs

The release produces:

Pet Core library
Headless diagnostic executable
SDL frontend library
Desktop application executable
Test executable or executables
Development runtime asset directory

There is no installer or production packaging in this release.

7.2 Platform verification matrix

EnvironmentSupport levelRequired for release
Linux x86-64 desktop, GCCBuild tested and runtime testedYes
Linux x86-64 desktop, ClangBuild tested and runtime testedYes
Linux dummy or headless video driverAllowed as additional rendering evidenceNo
MSVC on WindowsPortable by design, unverifiedNo
Clang and Ninja on WindowsPortable by design, unverifiedNo
ESP-IDFArchitecture preserved, unverifiedNo
Raspberry Pi Pico SDKArchitecture preserved, unverifiedNo
Arduino hostArchitecture preserved, unverifiedNo

The Windows and embedded rows are not support claims. They name constraints preserved for later work.

7.3 Build and gate interface

The release keeps the CMake and Python tooling boundary from v0.1.0. CMake owns targets, dependencies, include paths, compile options, link options, generated headers, selected subdirectories, and CTest registrations. Python owns workflow orchestration, composition selection, quality gates, registry validation, and report coordination.

The default gate remains headless:

./gate.py strict

Composition selection is:

./gate.py strict --app desktop
./gate.py strict --app desktop --frontend sdl
./gate.py strict --all-compositions

--all-compositions cannot be combined with --app or --frontend, and --frontend requires --app. An unknown or incompatible pair is rejected from the registry before the first configure. --dry-run prints the planned steps instead of running them.

The gate separates project steps from composition steps. Formatting, the tool tests, and Cppcheck read the sources and run once. Configure, build, test, and the build reading checks run once per selected composition. Each one configures into build/<preset>-<composition>, so no two runs share a directory. Direct preset use keeps the preset’s own build/<preset> directory.

Direct CMake use selects a composition through cache values:

cmake --preset linux-gcc-debug -DPET_APP=headless -DPET_FRONTEND=none
cmake --preset linux-gcc-debug -DPET_APP=desktop -DPET_FRONTEND=sdl

PET_APP defaults to the registry default app and an empty PET_FRONTEND selects the default frontend of the chosen app, so a plain configure stays headless. cmake/pet_compositions.cmake reads config/compositions.json directly rather than holding a second compatibility list, and fails during configuration when the pair is unknown or incompatible. Only the selected app and frontend directories are added, and PET_BUILD_APP turns the selected app off for subproject use. A selected directory that the registry declares before the component exists is reported and skipped rather than treated as an error.

7.4 Deployment constraints

  • no daemon is installed
  • no network port is opened
  • no account is required
  • no persistent application data is written
  • runtime assets are copied into the selected build output for development use
  • production packaging is deferred

7.5 Runtime asset layout

Canonical source assets stay under assets/pet/. A development build produces a runtime copy next to the selected build output rather than in the source tree. A gate composition build uses its isolated composition directory and a direct preset build uses its preset directory:

assets/pet/manifest.petasset                              canonical source
assets/pet/sprites/*.png                                  canonical source
build/<preset>-<composition>/assets/pet/manifest.petasset gate runtime copy
build/<preset>-<composition>/assets/pet/sprites/*.png     gate runtime copy
build/<preset>/assets/pet/manifest.petasset               direct preset runtime copy
build/<preset>/assets/pet/sprites/*.png                   direct preset runtime copy

The runtime copy is a generated artefact and is not canonical. Its layout is relative to the build output and must not depend on the source checkout path, so a runtime asset is resolved the same way regardless of where the repository is checked out.

The pet_runtime_assets target in assets/CMakeLists.txt produces this layout in every composition, and PET_ASSETS_PET_ROOT names the same root from the consumer side. A process therefore finds the set at assets/pet relative to its working directory, which is the build directory a development run starts from.

8. Crosscutting concepts

8.1 Time

The Core continues to receive explicit elapsed deltas. The desktop app owns platform clock reads, delta calculation, zero delta behaviour, and over-large delta policy before calling the Core.

Frontend animation receives explicit elapsed animation time from the app. Animation code must not read the wall clock directly.

The implemented policy reads CLOCK_MONOTONIC and turns two readings into one bounded delta. A stalled or backwards reading gives zero, and a gap over PET_TIME_DELTA_MAX_MS is clamped, so a suspended machine resumes instead of failing and no unbounded backlog is replayed. The bounded value is both what pet_world_update receives and what accumulates into the animation time handed to the frontend, so one decision drives Core state and presentation together.

The bounding rule is separated from the platform read so it can be tested as a pure function. A run can also take an exact step through a fixed step seam, because a run that reads a real clock cannot assert the state its own timing produced. A host without CLOCK_MONOTONIC falls back to clock(), which measures processor time and is a portability fallback rather than an equivalent source.

8.2 Presentation maintenance rule

Future Core growth should require zero or near-zero renderer maintenance. New presentable Core states map through presentation roles in components/presentation. If a presentable state is unknown to the shared mapping, the mapping selects a documented fallback role. The renderer does not gain direct Core enum branches as the normal extension mechanism.

The initial role priority is intentionally small. PET_EXPRESSION_HAPPY selects expression_happy, because a short expression should be visible over the background activity. PET_EXPRESSION_NEUTRAL does not override a known activity; PET_ACTIVITY_IDLE, PET_ACTIVITY_CURIOUS, and PET_ACTIVITY_ATTENTIVE select their corresponding activity roles. If the activity is unknown while the expression is neutral, the mapping may select expression_neutral. If neither region can produce a documented role, the mapping selects fallback_default.

pet_presentation_role_from_snapshot is that priority. It answers happy first, then lets a known activity answer, then lets a neutral expression answer, and reaches fallback_default only when no region recognised its own value. An expression it does not know therefore costs a pet nothing while its activity is still known, because only the happy expression is an override. A null snapshot selects fallback_default rather than reporting a status, so a consumer always has something calm to present and needs no error path to reach it.

This does not freeze the snapshot forever. It means a new snapshot field is added because the product needs to present a new fact, not because one frontend needs a texture name.

8.3 Visual assets and manifest

Canonical pet presentation assets are PNG files referenced by a small bounded manifest. The manifest is a project contract, not a user modding interface in this release.

The manifest is line-oriented and section-based, closer to INI than to JSON, so it is read with a small bounded grammar rather than a general data language dependency. Every manifest begins with a common header line manifest <kind> <version>. The pet presentation manifest uses kind pet, so assets/pet/manifest.petasset begins with manifest pet 1. A future UI or menu manifest may reuse the same common grammar with another kind, such as manifest ui 1, but defines its own schema. The exact pet schema, identifier rules, path rules, and limits are recorded in asset manifest v1. Pet presentation images live under assets/pet/sprites/.

The initial manifest needs to describe:

  • schema version
  • image identifiers and relative paths
  • animation identifiers
  • frame order and duration
  • role-to-animation mappings
  • fallback image, fallback animation, and fallback role

The manifest parser rejects unsupported versions, unknown directives, duplicates, invalid references, excessive lengths, excessive counts, and path traversal. It stores bounded records and does not allocate unbounded collections.

The grammar is common to future manifest kinds, so the parser is the shared component components/manifest rather than SDL frontend private code. pet_manifest_parse reads manifest text the caller already holds, which keeps the parser away from the filesystem and makes its tests deterministic. The accepted contents land in a caller-owned PetManifest whose images, animations, frames, and roles are fixed-capacity arrays, and a PetManifestStatus names the first rule a rejected manifest breaks. A rejected manifest leaves that storage empty rather than partly filled, so a consumer cannot present half of a manifest it never accepted. The parser supports kind pet version 1 alone. Another kind or version is a different contract and is rejected here rather than guessed at.

Finding and reading asset files belongs to components/assets, which is the consumer of the pure parser. The asset component exposes a reader contract so tests can provide memory fixtures, desktop builds can read the runtime copy under the build directory, and future embedded frontends can read packed or generated assets without inheriting desktop filesystem assumptions.

A PetAssetsReader carries one operation. read fills at most the capacity it is given and reports the length it delivered, so a caller that wants a whole file and a caller that wants only the leading bytes use the same operation. A buffer that was filled while more remained is reported separately from a whole file, which is how the manifest read learns that a file is over the limit while the image check learns that a file is long enough to be an image. The reader reports why a file could not be delivered rather than how the storage failed, so the layer decides between accepting a set and falling back without knowing what backs the reader. pet_assets_stdio_reader is the development desktop implementation over standard C file operations, and it is the only part of the component that touches a filesystem.

The image check reads the leading bytes of every referenced file and compares them to the PNG signature. That is a shallow check rather than decoding, and it is what separates a real image from an empty file, from a text file that was given a .png name, and from a directory a reader can open. Decoding stays with the frontend that owns image resources, so a file that passes here can still fail to decode and select the fallback there.

Decoding is where a frontend stops being portable, so the SDL frontend keeps it in one file. frontends/sdl/src/assets_sdl.c is the only place that turns a resolved path into an image resource, and the pinned SDL supplies that path today through SDL_IOFromFile and SDL_LoadPNG_IO. The decoded surface is a step rather than the resource that is kept: it becomes a renderer-owned drawable resource through SDL_CreateTextureFromSurface and is released immediately, so a frame draws without converting anything and an asset set never outlives the renderer it was loaded for. No frontend-neutral PNG decoder exists in this release and none is written speculatively. If a second frontend, or an embedded target that cannot carry SDL, later needs to decode the same canonical images, the decoder becomes a shared component such as components/png and only the implementation inside assets_sdl.c changes. The public frontend contract talks about asset state and image counts rather than image formats, so that swap does not reach an app.

pet_assets_fallback describes the calm presentation used when no asset set is available: a resting colour and one frame duration, with no text, glyph, or warning colour. It is the same value whatever failed, so a frontend needs no branch per reason and a missing asset set does not read as something the user broke. Drawing it belongs to the frontend.

This release records, but does not implement, a separate UI and menu asset layer. Menu graphics, button states, icons, panels, cursor details, and other application chrome should follow the same canonical-source principle when they are introduced. They must not be forced into the pet presentation manifest, because their state comes from app navigation, focus, pointer hover, selection, text, localisation, and layout rather than from PetSnapshot roles.

8.4 Input and host control

Raw frontend input is not a Core API. The SDL frontend classifies platform events and the app dispatches only supported PetAction values to the Core.

Closing a window, resizing a window, losing focus, or changing display state is host or frontend control. These events do not become product actions.

8.5 Memory and ownership

Core allocation rules remain unchanged. The SDL frontend may allocate or own platform resources when SDL requires it, but those resources are outside the Core and must be released on every success and failure path.

The manifest parser and presentation mapping use bounded storage. Any dynamic allocation introduced by the frontend must have explicit ownership and cleanup tests where practical.

8.6 Host and frontend support libraries

No generic host support library is implemented in this release. The desktop app records what host boundary work it owns. After more platform host evidence exists, the host support layer idea can be evaluated.

No generic frontend support library for platform boundary work is implemented in this release. The SDL frontend records what window, renderer, event, and resource work it owns. Shared frontend helpers beyond the pure layout policy wait until at least one more real frontend provides duplication evidence.

8.7 Testing and verification

Unit tests belong to the component that owns the behaviour:

  • Core behaviour stays under Core tests
  • desktop process lifecycle belongs to apps/desktop/tests
  • manifest grammar, schema, and limit tests belong to components/manifest/tests
  • asset root resolution, reader behaviour, and fallback-required decisions belong to components/assets/tests, which drive a memory reader double and never touch the host filesystem
  • logical canvas fit and scaling policy belongs to components/layout/tests
  • the runtime asset copy is checked by one separate executable in the same directory that runs from the build directory, because it is the only asset check that must read real files
  • animation and input tests belong to frontends/sdl/tests
  • SDL image resource loading and cleanup belongs to frontends/sdl/tests, which decode real files under the dummy video driver, because a drawable resource needs a renderer
  • creating a frontend opens a window, so every test that creates one runs under SDL’s dummy video driver, given by CTest rather than by the test process
  • a refused renderer needs a driver the rest of the suites must not run under, so it is a separate executable, because SDL copies the environment once per process
  • cross-component selection and boundary checks belong to tools and gates

Rendering evidence may combine pure layout tests, dummy-driver smoke tests, process scenarios, and manual observation. The evidence label must state which kind ran.

8.8 Compatibility and support claims

Linux desktop is the first verified runtime target for this release. Windows compatibility is a design requirement, not a verified claim. Embedded compatibility is preserved by Core and asset boundary choices, not proven by this release.

8.9 Language and documentation

Developer documentation uses British English. User-facing product text remains minimal in this release and does not establish a localisation contract.

8.10 Process diagnostics and exit status

An app reports where a failure came from rather than only that one happened. The category is carried by both the process exit status and the diagnostic code, so a caller can classify a failure without parsing text.

CategoryExit statusDiagnostic range
success0none
app or host11000 to 1999
Core origin22000 to 2999
frontend origin33000 to 3999

The range size is 1000. The categories, the range constants, and the mapping from a code to an exit status live in apps/include/pet_app/status.h. That header is a contract shared by apps. It is not a host support library, so it holds no reporting, no status naming, and no concrete codes.

Each app owns its own codes and its own reporting. A concrete code is an offset from a declared subgroup start, such as an app config subgroup at the app base plus 100, so a new code cannot be a loose number and cannot drift into another category. Core status values are not extended for this, because the Core does not know about processes. A frontend returns its own typed status and the app maps that status to a code and an exit status.

The desktop app writes one failure line to standard error naming the component, operation, status, diagnostic code, and exit status:

pet_desktop: error component=frontend_sdl operation=validate_config status=PET_FRONTEND_SDL_ERR_INVALID_ARGUMENT code=3101 exit=3

A constrained app may report the numeric code alone. The category meaning still holds, because only the text formatting is desktop specific.

9. Architecture decisions

9.1 Accepted decisions

  • v0.2.0 is the desktop reference app release
  • the desktop app target is pet_desktop
  • the desktop app lives under apps/desktop
  • the first visual frontend lives under frontends/sdl
  • SDL is the selected desktop frontend technology, see ADR-006
  • SDL is a frontend dependency only
  • the implemented SDL frontend pins SDL 3.4.10 through frontend-only FetchContent
  • SDL image loading uses SDL 3.4.10’s built-in PNG path through SDL_IOFromFile and SDL_LoadPNG_IO; no separate SDL_image dependency is added
  • decoding lives in frontends/sdl/src/assets_sdl.c alone, so a later shared decoder such as components/png replaces an implementation rather than a contract
  • PNG is the canonical visual asset source format, see ADR-007
  • the asset manifest is bounded, and its parser is a shared component under components/ rather than frontend private code, because the grammar is common to future manifest kinds
  • frontend-neutral asset-set loading belongs to components/assets; SDL-specific PNG decoding and resource ownership stay in frontends/sdl
  • frontend-neutral logical canvas fit and scaling policy belongs to components/layout; SDL adapts its result to renderer calls but does not own the reusable layout policy
  • an asset set is accepted whole or not at all, because a fallback chain that resolves through only part of a manifest has no defined presentation
  • a referenced image is checked by reading its leading bytes and comparing them to the PNG signature, so a file that merely opens is not mistaken for an image, and decoding still belongs to the frontend
  • the built-in fallback is one calm colour and frame duration for every failure reason, so a missing asset set never becomes a diagnostic the user has to read
  • the common manifest grammar starts with manifest <kind> <version>, and the pet presentation manifest uses kind pet version 1, recorded in asset manifest v1
  • sprites/ is the initial pet presentation image directory under assets/pet/
  • development runtime asset copies live under the selected build directory and are non-canonical
  • UI and menu assets are a related but separate future layer that may reuse the common grammar with a different kind
  • presentation roles and deterministic animation selection belong to components/presentation rather than to SDL renderer code
  • components/presentation may consume public PetSnapshot and PetManifest records, but it must not consume SDL, file I/O, image resources, wall-clock time, or allocation
  • unknown or unmapped presentable Core states use calm fallback roles
  • the composition registry is the source of truth for valid app and frontend combinations
  • the default composition remains headless
  • host support and frontend support libraries are deferred

9.2 Open decisions

The following decisions are intentionally left to implementation PBIs:

  • exact initial input bindings
  • exact fallback drawing implementation, resolved by PBI-054
  • exact logical canvas size, resolved by PBI-055 as 320 by 240

Each decision must stay within the boundaries recorded here and in the ADRs.

9.3 Architecture deviations

No deviation from the general architecture is accepted for this release.

Any later deviation must be recorded here with its reason, impact, and tracking PBI.

10. Quality requirements

10.1 Requirement mapping

Requirementv0.2.0 tacticEvidence
SPEC-PR-007SDL kept outside Core, composition registry records frontend choiceDependency and include checks
SPEC-FR-008keyboard and pointer input map to supported actionsfrontend input tests
SPEC-FR-025rendering uses PetSnapshot through presentation rolesfrontend and boundary tests
SPEC-FR-026frontend contract is separate from app and Corelayout and contract review
SPEC-FR-027pet_desktop and frontends/sdl compose into a visual appprocess and render evidence
SPEC-FR-028headless composition remains default and verifiedstrict gate
SPEC-FR-029asset and renderer data stay outside Coredependency checks
SPEC-NFR-003explicit time, deterministic animation, component testsunit and scenario tests
SPEC-NFR-006source layout, registry, and role mappingdocumentation and gate checks
SPEC-NFR-007support claims are separated by evidencerelease evidence

10.2 Required test areas

  • composition registry validation and invalid pair rejection
  • direct CMake composition selection
  • app lifecycle and failure cleanup
  • elapsed time policy before Core update
  • manifest parsing and invalid input rejection
  • missing asset fallback
  • presentation role mapping including unknown values
  • deterministic animation frame selection
  • logical canvas and scaling calculations
  • input classification and semantic action mapping
  • process scenario for desktop startup, one update/render path, and clean shutdown
  • include and dependency boundary verification

10.3 Required development checks

The strict gate continues to apply to implementation PBIs. This release adds composition-aware execution so the headless and desktop SDL compositions can both be verified.

Additional checks introduced by E16 must cover:

  • Core does not include SDL headers
  • Core does not link SDL symbols
  • app and frontend code do not include Core internal headers
  • manifest asset references resolve in the runtime asset layout
  • generated artefacts are not treated as canonical assets

10.4 Release compliance

The release complies with this architecture when:

  • all v0.2.0 exit criteria are satisfied
  • all E08 through E16 PBIs are complete
  • accepted ADRs are reflected in code and documentation
  • support claims match executed evidence
  • known limitations and deferred bugs are recorded
  • this document reflects the implemented system

Every point above holds for v0.2.0. All E08 through E16 PBIs are complete, the accepted ADRs are reflected in the code and this document, the recorded support claims match executed evidence, and the open limitations and the empty deferred bug register are recorded. The evidence is collected in section 10.5.

10.5 Release evidence

This section collects the evidence behind the v0.2.0 claims. Every gate row was produced by ./gate.py audit --all-compositions on the host recorded below, which runs the strict gate and then the audit steps over the headless and desktop-sdl compositions in turn.

ClaimEvidenceResult
Both required compositions build and test under GCC and Clangaudit gate over headless and desktop-sdlPass, headless runs 7 CTest cases and desktop-sdl runs 10 under each toolchain
Core builds as strict ISO C99 with no warningsGCC and Clang builds with project warnings as errorsPass, only third-party SDL sources emit warnings and they are outside the project warning scope
SDL stays out of the Coresymbols.py over libpet_core.a in both compositions, reading the archive and the Core compile linesPass, GCC needs nothing external, Clang needs memcpy and memset, and no Core compile line names a forbidden dependency
SDL and Core headers stay on their own side of the include boundaryboundaries.py over the scanned sourcesPass, 3 boundaries hold over 47 source files
Manifest asset references resolve in the runtime layout and no generated artefact is canonicalassets.py over each composition buildPass, 1 asset set with 5 manifest references resolving under the build output
Public headers need no include orderheaders.py over the discovered headers, GCC and ClangPass, 14 headless and 19 desktop-sdl headers compile alone
Core allocates nothing at runtimealloc.py over pet_alloc_probe under ValgrindPass, 0 allocations and 0 memory errors in both compositions
No memory errors in the test suitealloc.py over pet_tests under ValgrindPass, 13 framework allocations and 0 errors
Core coverageGcovr over src in both compositionsPass, 100% lines and 100% branches, 143/143 lines and 94/94 branches
Static analysis is cleanCppcheck and Clang-Tidy over project sourcesPass, 62 sources analysed
The desktop composition runs its process lifecycle scenariopet_desktop_scenario under the dummy driver, plus a real-driver failure pathPass, one of the 10 desktop-sdl CTest cases
The desktop app runs on a real driverpet_desktop --steps 240 --step-ms 16 against Wayland and again against X11Pass, exit 0 with the canonical set loaded and 240 frames driven
Visible rendering and semantic interactionmaintainer observation on Wayland and X11Pass as manual evidence, the drawn pet, a greet response to the space key and left mouse button, and a clean exit, recorded as manual because no automated check covers it, see LIM-011

Core coverage is measured over src only, so it reports the same 143 lines and 94 branches in both compositions. The desktop app, the SDL frontend, and the shared components are exercised by their own CTest suites rather than by the Core coverage figure.

The verification host was Linux x86_64 with these versions, printed by tools/verify/matrix.py as a release-time step:

ToolVerified version
CMake4.3.4
Ninja1.13.2
GCC15.3.0
Clang and clang-format22.1.8
Cppcheck2.21.0
Gcovr6.0
Valgrind3.27.1
GNU nm2.45.0
Python3.13.13

Windows and embedded remain unverified. The symbol scan does not parse MSVC dumpbin output and Valgrind does not run on Windows, so the boundary and allocation checks cannot execute there, and no embedded build exists. These gaps are recorded as LIM-001, LIM-002, and LIM-012 in known limitations, and they do not block the release, because neither Windows nor an embedded target is required for v0.2.0. No check required for the release failed or was skipped.

11. Risks and technical debt

RiskImpactMitigation
SDL dependency handling makes offline or deterministic builds difficultdesktop builds become fragilepin dependency, isolate third-party diagnostics, track local override policy in LIM-005
asset manifest parser grows into a general data languagefrontend complexity and attack surface growkeep schema small, bounded, and project-owned
asset loading becomes SDL-privatefuture embedded frontends have to copy desktop path and fallback ruleskeep asset-set resolution and reader policy in components/assets, with SDL only adapting accepted records to SDL resources
menu assets get folded into the pet presentation manifestrenderer and UI state models become coupledrecord UI assets as a separate future layer
renderer branches directly on Core enumsfuture Core states force renderer rewritesrequire presentation roles and fallback mappings
presentation mapping becomes SDL-privatefuture embedded frontends have to copy role priority and animation selectionkeep snapshot-to-role and manifest animation selection in components/presentation, with SDL only consuming the selected image/frame
visual evidence is overstatedrelease claims become misleadinglabel dummy-driver, automated, and manual evidence separately
app and frontend support libraries are introduced too earlyabstraction freezes before enough examples existdefer shared libraries until duplication evidence exists
PNG loading path differs across SDL versionshidden dependency appears laterecord the exact image loading path in the SDL ADR and pin accordingly
desktop assumptions leak into future embedded workasset and input contracts become expensive to reusekeep Core independent, keep canonical assets source-only, and defer generated embedded artefacts

12. Glossary

App

An executable composition and lifecycle boundary under apps/.

Frontend

A component that presents Core-derived state and classifies direct user input for one environment.

Composition

A declared app and frontend pair that build and verification workflows can select.

Canonical asset

A source asset stored once in the repository and treated as the source of truth.

Generated asset artefact

A target-specific file derived from canonical assets. It is not the source of truth.

Manifest

A bounded project file that maps images, animations, and presentation roles for a frontend.

Pet presentation asset

A visual asset selected from Core-derived presentation state.

UI asset

A visual asset selected from application navigation, focus, pointer, text, theme, or layout state.

Presentation role

A frontend-facing semantic visual role derived from a PetSnapshot before animation and rendering.

Host control

A platform or process request, such as quit or resize, that does not represent product intent.

Product action

A supported semantic action dispatched to the Core through the public API.