Pet architecture for v0.2.0
| Document version | Date | Summary |
|---|---|---|
| v1 | 2026-07-20 | Define the desktop reference app architecture |
| v2 | 2026-07-20 | Record composition and gate selection |
| v3 | 2026-07-20 | Record SDL and desktop lifecycle boundaries |
| v4 | 2026-07-20 | Record diagnostics and elapsed time |
| v5 | 2026-07-21 | Define the manifest and runtime asset layout |
| v6 | 2026-07-21 | Record manifest and asset components |
| v7 | 2026-07-21 | Record SDL asset loading |
| v8 | 2026-07-22 | Define presentation roles and animation |
| v9 | 2026-07-22 | Record SDL rendering and fallback behaviour |
| v10 | 2026-07-22 | Record layout and asset-backed rendering |
| v11 | 2026-07-24 | Accept 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_desktopdesktop host applicationfrontends/sdlSDL 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_desktopcalls only public Core APIs- the SDL frontend never receives a mutable
PetWorld * - app and frontend components use explicit
include/,src/, andtests/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
petprefix - 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
- keep product behaviour in the Core
- keep platform effects in the desktop app and SDL frontend
- use a declared app and frontend composition instead of hard-coded build assumptions
- map snapshots through the shared presentation component before selecting images to render
- use fallback roles and fallback rendering for unknown or missing presentation data
- load canonical assets outside the render loop
- keep asset and animation identifiers out of
PetSnapshot - keep semantic input as the only path from raw desktop input to Core actions
- verify boundaries through scripts and component-owned tests
- 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.pngThe 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.pyFiles 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:
| App | Frontend | Composition name | Meaning |
|---|---|---|---|
headless | none | headless | diagnostic host without a frontend |
desktop | sdl | desktop-sdl | desktop 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
- the Core depends on no app, frontend, asset, shared component, or SDL target
- apps depend on the public Core API
apps/desktopdepends on the selected frontend targetfrontends/sdldepends on SDL and the public Core presentation types it consumesfrontends/sdluses shared asset records but owns every SDL image resource it creates- 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
components/assetsowns asset-set resolution and reader contracts, whilecomponents/manifestowns text parsing and validationcomponents/presentationowns snapshot-to-role mapping and deterministic manifest animation selectioncomponents/layoutowns frontend-neutral logical canvas fit calculations- frontend implementation headers stay private to the frontend target
- app component headers stay local to the executable component
- 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 category | Example | Destination |
|---|---|---|
| product action | key or pointer mapped to PET_ACTION_GREET | app dispatches to Core |
| host control | quit request | app loop and process lifecycle |
| frontend event | resize or focus change | frontend local state |
| ignored input | unsupported key or button | no 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 directoryThere is no installer or production packaging in this release.
7.2 Platform verification matrix
| Environment | Support level | Required for release |
|---|---|---|
| Linux x86-64 desktop, GCC | Build tested and runtime tested | Yes |
| Linux x86-64 desktop, Clang | Build tested and runtime tested | Yes |
| Linux dummy or headless video driver | Allowed as additional rendering evidence | No |
| MSVC on Windows | Portable by design, unverified | No |
| Clang and Ninja on Windows | Portable by design, unverified | No |
| ESP-IDF | Architecture preserved, unverified | No |
| Raspberry Pi Pico SDK | Architecture preserved, unverified | No |
| Arduino host | Architecture preserved, unverified | No |
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 strictComposition 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=sdlPET_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 copyThe 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.
| Category | Exit status | Diagnostic range |
|---|---|---|
| success | 0 | none |
| app or host | 1 | 1000 to 1999 |
| Core origin | 2 | 2000 to 2999 |
| frontend origin | 3 | 3000 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=3A 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.0is 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_IOFromFileandSDL_LoadPNG_IO; no separate SDL_image dependency is added - decoding lives in
frontends/sdl/src/assets_sdl.calone, so a later shared decoder such ascomponents/pngreplaces 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 infrontends/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 kindpetversion1, recorded in asset manifest v1 sprites/is the initial pet presentation image directory underassets/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/presentationrather than to SDL renderer code components/presentationmay consume publicPetSnapshotandPetManifestrecords, 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
| Requirement | v0.2.0 tactic | Evidence |
|---|---|---|
| SPEC-PR-007 | SDL kept outside Core, composition registry records frontend choice | Dependency and include checks |
| SPEC-FR-008 | keyboard and pointer input map to supported actions | frontend input tests |
| SPEC-FR-025 | rendering uses PetSnapshot through presentation roles | frontend and boundary tests |
| SPEC-FR-026 | frontend contract is separate from app and Core | layout and contract review |
| SPEC-FR-027 | pet_desktop and frontends/sdl compose into a visual app | process and render evidence |
| SPEC-FR-028 | headless composition remains default and verified | strict gate |
| SPEC-FR-029 | asset and renderer data stay outside Core | dependency checks |
| SPEC-NFR-003 | explicit time, deterministic animation, component tests | unit and scenario tests |
| SPEC-NFR-006 | source layout, registry, and role mapping | documentation and gate checks |
| SPEC-NFR-007 | support claims are separated by evidence | release 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.
| Claim | Evidence | Result |
|---|---|---|
| Both required compositions build and test under GCC and Clang | audit gate over headless and desktop-sdl | Pass, headless runs 7 CTest cases and desktop-sdl runs 10 under each toolchain |
| Core builds as strict ISO C99 with no warnings | GCC and Clang builds with project warnings as errors | Pass, only third-party SDL sources emit warnings and they are outside the project warning scope |
| SDL stays out of the Core | symbols.py over libpet_core.a in both compositions, reading the archive and the Core compile lines | Pass, 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 boundary | boundaries.py over the scanned sources | Pass, 3 boundaries hold over 47 source files |
| Manifest asset references resolve in the runtime layout and no generated artefact is canonical | assets.py over each composition build | Pass, 1 asset set with 5 manifest references resolving under the build output |
| Public headers need no include order | headers.py over the discovered headers, GCC and Clang | Pass, 14 headless and 19 desktop-sdl headers compile alone |
| Core allocates nothing at runtime | alloc.py over pet_alloc_probe under Valgrind | Pass, 0 allocations and 0 memory errors in both compositions |
| No memory errors in the test suite | alloc.py over pet_tests under Valgrind | Pass, 13 framework allocations and 0 errors |
| Core coverage | Gcovr over src in both compositions | Pass, 100% lines and 100% branches, 143/143 lines and 94/94 branches |
| Static analysis is clean | Cppcheck and Clang-Tidy over project sources | Pass, 62 sources analysed |
| The desktop composition runs its process lifecycle scenario | pet_desktop_scenario under the dummy driver, plus a real-driver failure path | Pass, one of the 10 desktop-sdl CTest cases |
| The desktop app runs on a real driver | pet_desktop --steps 240 --step-ms 16 against Wayland and again against X11 | Pass, exit 0 with the canonical set loaded and 240 frames driven |
| Visible rendering and semantic interaction | maintainer observation on Wayland and X11 | Pass 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:
| Tool | Verified version |
|---|---|
| CMake | 4.3.4 |
| Ninja | 1.13.2 |
| GCC | 15.3.0 |
| Clang and clang-format | 22.1.8 |
| Cppcheck | 2.21.0 |
| Gcovr | 6.0 |
| Valgrind | 3.27.1 |
| GNU nm | 2.45.0 |
| Python | 3.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
| Risk | Impact | Mitigation |
|---|---|---|
| SDL dependency handling makes offline or deterministic builds difficult | desktop builds become fragile | pin dependency, isolate third-party diagnostics, track local override policy in LIM-005 |
| asset manifest parser grows into a general data language | frontend complexity and attack surface grow | keep schema small, bounded, and project-owned |
| asset loading becomes SDL-private | future embedded frontends have to copy desktop path and fallback rules | keep 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 manifest | renderer and UI state models become coupled | record UI assets as a separate future layer |
| renderer branches directly on Core enums | future Core states force renderer rewrites | require presentation roles and fallback mappings |
| presentation mapping becomes SDL-private | future embedded frontends have to copy role priority and animation selection | keep snapshot-to-role and manifest animation selection in components/presentation, with SDL only consuming the selected image/frame |
| visual evidence is overstated | release claims become misleading | label dummy-driver, automated, and manual evidence separately |
| app and frontend support libraries are introduced too early | abstraction freezes before enough examples exist | defer shared libraries until duplication evidence exists |
| PNG loading path differs across SDL versions | hidden dependency appears late | record the exact image loading path in the SDL ADR and pin accordingly |
| desktop assumptions leak into future embedded work | asset and input contracts become expensive to reuse | keep 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.