Pet architecture for v0.1.0

Document versionDateSummary
v12026-07-17Initial architecture for the headless Pet Core release
v22026-07-18Record the caller owned fixed capacity world and its storage bounds
v32026-07-19Close the time representation decision and record the world clock storage
v42026-07-19Close the autonomous transition decision and raise the world storage bound
v52026-07-19Record the explicit random source, the update order, and the bounded work limits
v62026-07-19Record the semantic action contract and the static transition tables
v72026-07-19Record the derived snapshot contract, its field policy, and its storage
v82026-07-19Record the public API failure audit and its termination evidence
v92026-07-19Record the automated boundary, header, and allocation checks and the gate structure
v102026-07-19Record the CMake and Python tooling boundary and the Linux-only check status
v112026-07-19Record the release evidence and verified toolchain, and accept the document

Status: Accepted

Product version: v0.1.0

Milestone: V0 software companion foundation

General architecture: Pet architecture

Template: arc42

Versioning: Semantic Versioning 2.0.0

1. Introduction and goals

1.1 Purpose

Version v0.1.0 is the first implementation release of Pet. It establishes a headless and portable Pet Core that can be exercised through tests and a small diagnostic host.

The release proves the Core architecture before a visual frontend is introduced. It is a technical and domain foundation rather than a public product demo.

flowchart LR
    T[Test suite]
    H[Headless host]
    C[Pet Core]
    O[Diagnostic output]

    T --> C
    H --> C
    C --> H
    H --> O

1.2 Architecture goals

The release must prove that:

  • Pet Core behaviour can be implemented in portable ISO C99
  • the Core can run without SDL or another visual frontend
  • one world and one pet have explicit ownership and lifecycle
  • time enters the Core through explicit input
  • direct user intent enters as semantic actions
  • observable state can be exposed without mutable world access
  • controlled input produces reproducible behaviour
  • invalid input does not partially corrupt active state
  • the implementation remains small enough to understand and test fully

1.3 Included capabilities

  • Pet Core library
  • one world containing one pet
  • world initialisation and destruction
  • explicit elapsed time update
  • one minimal autonomous transition
  • at least one semantic action
  • minimal presentation snapshot
  • headless diagnostic host
  • unit tests
  • development diagnostics
  • GCC and Clang hosted builds

1.4 Excluded capabilities

  • SDL and other rendering libraries
  • visual frontend
  • hardware frontend
  • physical sensor adapters
  • digital activity adapters
  • durable save files
  • plugin loading
  • audio
  • networking
  • pet transfer
  • multiple pets
  • complete mood, maturity, progression, and formative event systems
  • embedded cross compilation claims
  • embedded runtime support claims

Excluded capabilities must not leave placeholder platform dependencies inside the Core.

1.5 Stakeholders

Developer

Implements, tests, and evaluates the first public Core contracts.

Future frontend developer

Depends on the snapshot and semantic action boundaries remaining independent of the headless host.

Future embedded developer

Depends on the Core avoiding hosted platform assumptions even though no embedded target is supported by this release.

2. Architecture constraints

2.1 Product constraints

  • the release belongs to the V0 milestone
  • the release is headless
  • one world contains one pet
  • the release must not require a network connection
  • the release must not require final hardware
  • excluded product features must not be partially implemented

2.2 Technical constraints

  • Pet Core uses ISO C99
  • Core behaviour must not require compiler extensions
  • Core code must not depend on a graphical environment
  • Core code must not depend on an operating system API
  • Core code must not depend on a filesystem
  • Core code must not depend on a network stack
  • Core code must not depend on a thread runtime
  • Core code must not depend on a hardware SDK
  • public headers must not include host or test framework headers
  • time must be supplied explicitly
  • mutable world state must have one owner
  • all public boundary failures must use explicit status values

2.3 Documentation and language constraints

2.4 Verification constraints

  • GCC on Linux is required for release verification
  • Clang on Linux is required for release verification
  • strict C99 compilation is required
  • configured warnings are treated as errors in verification builds
  • tests must not depend on wall clock timing or external services

3. System scope and context

3.1 Business context

The release has no end user frontend. Its observable value is architecture evidence for the project and a stable base for v0.2.0.

flowchart TD
    D[Developer]
    H[Headless application]
    C[Pet Core library]
    T[Unit tests]

    D --> H
    H <--> C
    T <--> C

3.2 Technical context

The Pet Core receives explicit domain input and returns status values and derived snapshots. The headless host performs diagnostic output. The test suite controls every supported input.

flowchart LR
    I[Time and actions]
    C[Pet Core]
    S[Snapshot and status]
    H[Headless host]

    I --> C
    C --> S
    S --> H

3.3 External interfaces

The release has no network, sensor, storage, or plugin interface.

Its only architecture boundaries are:

  • the public C API consumed by the headless host
  • the public C API consumed by tests
  • process output produced by the headless host
  • process exit status produced by the headless host

Diagnostic output is not a stable product interface.

4. Solution strategy

4.1 Functional core and imperative shell

The Pet Core is the functional centre of the release. The headless host is the imperative shell.

flowchart LR
    E[Host effects]
    H[Imperative host]
    C[Functional Core]
    O[Diagnostic output]

    E --> H
    H --> C
    C --> H
    H --> O

The Core receives explicit input, performs owned state transitions, and returns explicit results. It does not perform platform input or output.

The implementation may update state in place. Mutation must remain local to Core operations and must not escape through mutable internal pointers.

4.2 Main strategies

  1. use ISO C99 as the Core language baseline
  2. represent user intent as semantic actions
  3. inject elapsed time explicitly
  4. expose visible state through a derived snapshot
  5. validate input before observable mutation
  6. use one explicit owner for authoritative world state
  7. keep host effects outside the Core
  8. control every supported input in tests
  9. avoid speculative adapter, storage, and frontend abstractions

4.3 Technology strategy

The release uses the C standard library only where the selected facility is approved for the Core. No external runtime library is required by the Core.

The build system and test framework remain open until their PBIs are accepted. Development tools may be platform specific because they are not Core runtime dependencies.

5. Building block view

5.1 Level 1 building blocks

flowchart TD
    H[Headless host]
    T[Test suite]
    API[Public Pet API]
    C[Pet Core]

    H --> API
    T --> API
    API --> C

5.2 Pet Core

The Pet Core owns all authoritative world state and domain transitions included in this release.

Responsibilities:

  • validate lifecycle input
  • establish one valid world and pet
  • apply explicit elapsed time
  • apply semantic actions
  • perform one minimal autonomous transition
  • expose a read only snapshot value
  • return explicit status values
  • preserve deterministic behaviour under controlled input

The Core must not:

  • print diagnostic output
  • read the system clock
  • access files
  • allocate platform resources
  • initialise a window
  • read physical input
  • call a network service
  • depend on the headless host

5.3 Headless host

The headless host is a developer tool and architecture demonstrator. It is not a product frontend.

Responsibilities:

  • create a world through the public Core API
  • provide a controlled sequence of elapsed time and actions
  • request presentation snapshots
  • print concise diagnostic state
  • report failures through its process exit status
  • destroy owned resources

Domain rules must not be implemented in the host.

5.4 Test suite

The test suite verifies public contracts and selected internal invariants.

Tests control:

  • initial state
  • elapsed time
  • semantic actions
  • repeated execution order
  • invalid argument cases

Tests may access internal code only when a public contract cannot verify an important invariant. Every such case requires a documented reason.

5.5 Domain model

The domain model is intentionally small.

flowchart TB
    subgraph Core["Pet Core"]
        World["PetWorld\nauthoritative world state"]
        Pet["PetEntity\nauthoritative pet state"]
        Tables["static transition tables\ninternal behaviour data"]
        Snapshot["PetSnapshot\nderived presentation value"]
    end

    Host["Host"]
    Frontend["Frontend, later"]

    World --> Pet
    Tables -.->|time and action triggers| World
    World --> Snapshot
    Host --> World
    Host --> Snapshot
    Frontend --> Host

World

The world is the authoritative state container. It owns one pet and the minimum time state required by the release.

Pet

The pet contains only the state required to demonstrate one autonomous transition and one action response. Final mood, memory, trait, maturity, and progression models are deferred.

The pet holds two independent state regions. PetActivity is what the pet is doing and is idle, curious, or attentive. PetExpression is how it looks and is neutral or happy. They are separate enumerations with separate timers, so a pet can settle its activity while still looking happy. A single combined value is deliberately avoided, because the combined set grows multiplicatively once mood, relationship, and traits arrive.

Both regions change only through the static transition tables described in section 6.2. Time and semantic actions are triggers into the same tables, so neither owns a region.

Activity is no longer a function of total elapsed time. Each region records the clock reading at which it last changed, and a time transition measures the dwell since then. This is the minimum state that lets an action and the clock compose. A greeting restarts the activity timer, so a pet greeted one second before it would have become curious stays attentive. For a world that receives no action the behaviour is unchanged: an idle pet becomes curious once the world clock reaches PET_CURIOUS_AFTER_MS, whatever way the host divided the time into updates.

Neither time nor an action removes anything from a pet, which is what SPEC-PR-001 requires. Repetition is never punished, so no interaction can become a maintenance loop.

Snapshot

The snapshot is a derived value for future frontends and current diagnostics. It exposes only information required to observe supported behaviour. It must not expose mutable Core storage.

PetSnapshot is not a mirror of PetWorld or PetEntity. No rule requires a world field to have a snapshot counterpart, and adding authoritative state does not add a snapshot field. A field appears in the snapshot because it was decided to be presentable. The reasoning is recorded in ADR-005.

Every field added to a world is classified as one of four things, and the classification is recorded where the field is added:

ClassMeaningExample in v0.1.0
internal onlypresent for a mechanism, never exposedactivity_since_ms, expression_since_ms, initialised
presentable copyexposed as an owned copy of the authoritative valuepet_name, activity, expression, elapsed_time_ms
presentable derived valuecomputed at extraction, never stored in a worldnone yet
deferredpresentable in principle, out of scope for this releasemood, memories, traits, progression

The default is internal only.

update_count is deliberately absent. It records how the host divided its time rather than anything the pet did, so two worlds that lived the same time in one update and in ten updates produce equal snapshots. elapsed_time_ms is present because lived world time is a fact about the pet.

The pet name is copied into snapshot-owned storage of PET_NAME_CAPACITY bytes. No const char * into a world is exposed, because such a pointer would outlive nothing safely once the storage is reset or reused.

5.6 Public contract

Exact function names and C types are finalised by implementation PBIs. The public contract must provide operations to:

  • initialise or create one valid world
  • destroy or reset the world safely
  • apply explicit elapsed time
  • supply explicit random values
  • dispatch a supported semantic action
  • produce a presentation snapshot
  • return a stable status for expected failure cases

The public contract must define:

  • ownership of every object and buffer
  • valid and invalid argument behaviour
  • valid lifecycle order
  • time units and accepted ranges
  • random value ranges and consumption rules
  • action validation
  • snapshot lifetime
  • repeated destruction or reset behaviour where supported

The API must not expose frontend, storage, adapter, or operating system types.

The fields of a public state structure are visible so that a caller can size, place, and inspect its own storage. Reading or writing those fields directly is not supported API. Callers use Core operations, which keeps domain invariants in one place and leaves the layout free to change. C cannot enforce this, so it is a contract rather than a guarantee.

5.7 Proposed source layout

The exact file list may be refined by PBIs. Architecture ownership must remain visible in the directory structure.

include/
    pet/
        pet.h
        status.h
        types.h
        random.h
        world.h
        action.h
        snapshot.h
 
src/
    random.c
    world.c
    action.c
    transition.c
    transition.h
    snapshot.c
    validation.c
 
apps/
    headless/
        main.c
 
tests/
    test_world.c
    test_action.c
    test_snapshot.c

Files should be added only when they have a clear responsibility. The release does not require every proposed file to exist if a smaller layout remains clear.

5.8 Dependency rules

flowchart TD
    HH[Headless host]
    TS[Test suite]
    API[Public Pet API]
    CORE[Core implementation]
    C99[Approved C99 library]

    HH --> API
    TS --> API
    API --> CORE
    CORE --> C99
  1. the headless host depends only on the public Pet API
  2. tests prefer the public API
  3. public headers do not include platform or test framework headers
  4. Core implementation does not include host headers
  5. Core implementation uses only approved ISO C99 facilities
  6. compiler extensions are not required for correct behaviour

6. Runtime view

6.1 Headless scenario

sequenceDiagram
    participant Host
    participant Core

    Host->>Core: initialise world
    Core-->>Host: status
    Host->>Core: apply elapsed time
    Core-->>Host: status
    Host->>Core: dispatch semantic action
    Core-->>Host: status
    Host->>Core: request snapshot
    Core-->>Host: snapshot
    Host->>Core: destroy world

6.2 State transition

The runtime path keeps input, authoritative state, internal behaviour tables, and derived output separate.

sequenceDiagram
    participant Host
    participant Core
    participant World as PetWorld
    participant Tables as Transition tables
    participant Snapshot as PetSnapshot

    Host->>Core: pet_world_update(world, elapsed_ms)
    Core->>World: validate and advance clock
    Core->>Tables: evaluate time trigger
    Tables->>World: apply matching rows

    Host->>Core: pet_world_apply_action(world, action)
    Core->>World: validate world
    Core->>Core: validate action
    Core->>Tables: evaluate action trigger
    Tables->>World: apply matching rows

    Host->>Core: pet_world_snapshot(world, out)
    Core->>World: validate and read
    Core->>Snapshot: copy name, activity, expression, and elapsed time
    Core-->>Host: status

Update input contains:

  • elapsed time
  • zero or more supported semantic actions
  • controlled random input only if required by the selected behaviour

pet_world_update performs these steps in this order. The order is part of the observable contract, because it decides which status a caller receives when more than one thing is wrong:

  1. validate the world, returning PET_ERR_NULL_ARGUMENT or PET_ERR_INVALID_STATE
  2. reject a delta above PET_TIME_DELTA_MAX_MS
  3. reject a delta that would overflow the world clock
  4. return success without writing anything when the delta is zero
  5. advance elapsed_time_ms
  6. increment update_count
  7. evaluate the transition tables with the time trigger

Steps 1 to 4 read only. Nothing is written unless every check has passed, so a rejected update leaves the world byte for byte untouched. An invalid world is reported before an invalid delta, so a corrupt world cannot be masked by an argument error.

pet_world_apply_action performs these steps in this order:

  1. validate the world, returning PET_ERR_NULL_ARGUMENT or PET_ERR_INVALID_STATE
  2. reject an action that is not a member of PetAction with PET_ERR_INVALID_ARGUMENT
  3. evaluate the transition tables with the action trigger

The world is validated before the action, so a corrupt world is not masked by an argument error, and no row is inspected until both are known good. A rejected action cannot leave one region applied and the other not.

An action moves no time. The world clock and the update count belong to pet_world_update.

pet_world_snapshot performs these steps in this order:

  1. reject a null world or a null destination with PET_ERR_NULL_ARGUMENT
  2. validate the world, returning PET_ERR_INVALID_STATE
  3. write every byte of the destination

A null destination is reported before an invalid world, because a rejected request has nowhere else to report anything to. This is the reverse of the update and action order, where the world is checked first, and it is the same order pet_world_init uses for its own null checks.

Nothing is written unless every check has passed, so a rejected request leaves the destination byte for byte untouched and a successful one leaves no field carrying a previous value. Extraction reads the world and never writes to it, so it is not a step in the simulation and repeating it changes nothing.

6.2.1 Transition tables

Every pet state change lives in one of two static tables in src/transition.c. Changing behaviour means adding or editing a row there. The tables are internal, so no frontend, adapter, or host may depend on their shape. The reasoning is recorded in ADR-004.

A row is an after_ms threshold, a trigger, an action, a source state, and a destination state. after_ms is meaningful for time rows. action is meaningful for action rows.

TableAfter msTriggerActionFromTo
activityPET_CURIOUS_AFTER_MStimenoneidlecurious
activityPET_ATTENTIVE_FOR_MStimenoneattentiveidle
activity0actiongreetidleattentive
activity0actiongreetcuriousattentive
expression0actiongreetneutralhappy
expressionPET_HAPPY_FOR_MStimenonehappyneutral

The activity table is equivalent to this state view:

stateDiagram-v2
    [*] --> Idle
    Idle --> Curious: time / PET_CURIOUS_AFTER_MS
    Idle --> Attentive: action / greet
    Curious --> Attentive: action / greet
    Attentive --> Idle: time / PET_ATTENTIVE_FOR_MS

The expression table is equivalent to this state view:

stateDiagram-v2
    [*] --> Neutral
    Neutral --> Happy: action / greet
    Happy --> Neutral: time / PET_HAPPY_FOR_MS

Evaluation order is part of the observable contract:

  1. the activity table is evaluated before the expression table
  2. each table is scanned from the first row to the last
  3. the first matching row wins and the scan stops
  4. at most one row per table applies per trigger

A row matches when its trigger is the one being evaluated, its source equals the current state of the region, its action equals the dispatched action for an action row, and the time since the region last changed is at least after_ms. Action rows use after_ms 0 and time rows use PET_ACTION_NONE.

Applying a row writes the destination and stamps the region with the current world clock, which restarts the dwell that time rows measure. A row must change its region, so a scan either produces one change or leaves the region alone.

A valid action with no matching row succeeds and changes nothing. Greeting an already attentive pet is the case that exists today.

There is no cascade. One trigger applies at most one row per table, so a large accepted delta does not replay the chain of transitions the missed time would have produced. This follows the rule in section 8.1 that an accepted delta is applied in one step.

Maximum rows inspected in one call is PET_ACTIVITY_TRANSITION_COUNT plus PET_EXPRESSION_TRANSITION_COUNT, which is six. It is derivable from the declared table sizes and depends on no input.

6.3 Rejected input

Input validation occurs before a state transition becomes externally observable.

flowchart LR
    I[Candidate input]
    V[Validate]
    T[Transition]
    C[Commit state]
    E[Return error]

    I --> V
    V --> T
    T --> C
    V --> E

When validation fails, authoritative state remains unchanged.

6.4 Failure and cleanup

Lifecycle operations must define safe behaviour after partial initialisation failure. Cleanup may be called only in the states documented by the accepted public contract.

No failure path may leave a retained platform resource inside the Core.

7. Deployment view

7.1 Logical outputs

The release produces:

Pet Core library
Headless diagnostic executable
Test executable or executables

All outputs run on the development host. There is no embedded deployment in this release.

7.2 Platform verification matrix

EnvironmentSupport levelRequired for release
GCC on LinuxBuild tested and runtime testedYes
Clang on LinuxBuild tested and runtime testedYes
MSVC on WindowsPortable by design, configuration provided, unverifiedNo
Clang and Ninja on WindowsAnalysis configuration provided, unverifiedNo
ESP-IDFPortable by designNo
Raspberry Pi Pico SDKPortable by designNo
Arduino hostPortable by designNo

The Windows entries provide CMake presets but remain unverified until executed on a Windows environment or runner. The embedded entries are design goals. They are not support claims for v0.1.0.

The two Linux rows are the only ones supported by evidence. Both were built and had their tests executed, so both are runtime tested rather than build tested alone. The verification host was Linux x86_64 with these versions:

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

tools/verify/matrix.py prints this set from the environment it runs in. It is a release-time step rather than a gate step, so the table is refreshed when a release is verified rather than on every audit run.

7.3 Build interface

The build system is CMake with presets and Ninja, and requires CMake 3.25 or later. It exposes documented commands for:

  • normal build
  • test build and execution
  • strict warning build
  • development diagnostics

The fast, strict, and audit workflows each run through a single command:

./gate.py fast
./gate.py strict
./gate.py audit

gate.py is a dependency-free Python script that composes the named CMake presets and the checks under tools/verify. Each gate is defined as the list of the gate before it plus its own steps, so containment is a property of the code rather than a convention: strict runs every fast step and audit runs every strict step.

This release applies the tooling boundary in code rules. CMake defines the libraries, the executables, the include paths, the compile and link options, the generated version.h, and the CTest registrations. It defines no quality check target. Every check lives in tools/verify and runs on its own:

python3 tools/verify/format.py [--fix]
python3 tools/verify/cppcheck.py
python3 tools/verify/tidy.py --build-dir build/<preset>
python3 tools/verify/symbols.py build/<preset>/src/libpet_core.a --toolchain <gcc|clang>
python3 tools/verify/headers.py --cc <compiler> --include include --include build/<preset>/include
python3 tools/verify/alloc.py <binary> [--allow-heap]
python3 tools/verify/coverage.py --build-dir build/<preset> --report _reports/coverage

Building and running tests stays with CMake and CTest, so cmake --preset, cmake --build --preset, and ctest --preset remain the direct commands for a normal build.

Strict is the required completion gate for every implementation PBI. Audit adds coverage, Valgrind, and clean rebuilds from empty directories, and its reports are written beneath _reports. The Windows gates are separate: a native MSVC gate covering formatting, MSVC warnings, build, tests, and Cppcheck, and an optional Clang and Ninja gate adding Clang-Tidy and supported sanitisers. Valgrind is not required on Windows, and neither the symbol scan nor the allocation check is available there.

7.4 Deployment constraints

  • no installation service is required
  • no daemon is required
  • no network port is opened
  • no user account is required
  • no graphical environment is required
  • no persistent application data is required

8. Crosscutting concepts

8.1 Time

The host supplies elapsed time explicitly. The Core does not read the wall clock.

The public API defines one time unit. PetTimeMs is a uint64_t duration in milliseconds, recorded in ADR-002.

The representation must:

  • have documented range and overflow behaviour
  • reject or safely handle invalid input
  • produce deterministic transitions
  • avoid reliance on frame rate
  • remain practical for embedded targets

The host passes an elapsed delta rather than an absolute clock value, so the Core interprets no epoch, calendar, or time zone. pet_world_update applies one delta to one world and keeps elapsed_time_ms and update_count as the observable time state.

A zero delta succeeds and changes nothing. PET_TIME_DELTA_MAX_MS is one day, and a larger delta or one that would overflow the world clock is rejected with PET_ERR_INVALID_ARGUMENT. An accepted delta is applied in one step, so update work is constant and no missed time is replayed. The world is validated before the delta and everything is validated before anything is written, so a rejected update leaves the world untouched.

Missed session time and calendar behaviour are outside this release.

8.2 Randomness

Random values enter explicitly. PetRandom is a caller-declared source holding one uint64_t of state, seeded by pet_random_seed and drawn from by pet_random_next. The Core owns no generator, reads no platform entropy source, and calls no standard library random function. The representation and its trade-offs are recorded in ADR-003.

Required rules:

  • random state or values enter explicitly
  • tests provide a fixed seed or controlled sequence
  • the Core does not use an implicit process global generator
  • the selected behaviour remains reproducible

The source is standalone. It is not part of PetWorld, it is not an argument of pet_world_update, and no behaviour in this release consumes a value from it. The autonomous transition is a threshold on elapsed time and needs no randomness, so world updates stay reproducible from the world and the delta alone. A later behaviour that needs variation takes a source as an explicit argument.

A bounded draw performs constant work. It uses no modulo and no retry loop, so its cost does not depend on the bound or on the value drawn. The mapping onto a bound that is not a power of two is uneven by at most the bound divided by 2

Avoiding unnecessary randomness in behaviour remains preferred for this release.

8.3 Memory and ownership

The release uses explicit ownership and bounded state.

Required rules:

  • no hidden global mutable world
  • no unbounded collection growth
  • no allocation by a getter or snapshot function without an explicit contract
  • no frontend or host pointer retained inside domain state
  • no mutable internal pointer exposed through the snapshot
  • cleanup remains safe after partial initialisation failure

The accepted strategy is a caller owned fixed capacity world, recorded in ADR-001. PetWorld is a complete public structure that the caller declares and places. Pet Core never allocates a world, never frees one, and retains no pointer to one, so independently owned worlds do not interact.

Every bounded collection has a capacity macro, so the size of a world is fixed at compile time. The measured storage on Linux x86-64 is:

TypeBytesRole
PetWorld88persistent state for one world
PetSnapshot56caller owned derived presentation value, held outside the world
PetEntity64pet state, stored inline in its world
PetWorldConfig8caller owned initialisation input
PetRandom8caller owned random source, held outside the world
PetActivityRow24one activity transition, static and shared by every world
PetExpressionRow24one expression transition, static and shared by every world

The world clock and update count added 16 bytes to PetWorld. The pet activity added a further 8, which is 4 bytes for the enumeration and 4 of padding, because size_t name_length already aligns PetEntity to 8 bytes. No ordering of the pet fields avoids that padding.

The semantic action added 16 more. The expression enumeration filled the padding the activity left, so the cost is the two region timers that let an action and the clock compose. There is no cheaper representation that keeps the regions independent.

The transition tables are static constants outside any world. They are 96 and 48 bytes and are shared by every world in the process. A row carries no padding because its dwell leads.

A snapshot is caller owned in the same way a world is. The caller declares and places the storage, Pet Core writes it and retains no pointer to it, and nothing needs to be released. A snapshot holds no pointer of its own, so it cannot reach the world it came from and its lifetime is bounded only by the storage the caller declared. It records the world as it was, so a later update or action is observable only through a new request.

A snapshot carries no padding. Byte equality is how the tests assert that equal worlds produce equal snapshots, and that claim is only meaningful while every byte belongs to a field. A test asserts the absence of padding so that a later field addition cannot quietly break it.

Core operations require no temporary storage beyond their parameters and local variables.

Work per operation is bounded by declared capacities rather than by input magnitude:

OperationMaximum iterationsBound
pet_world_initPET_NAME_CAPACITY name scan, then the same for the copycompile-time capacity
pet_world_validatenonefixed field checks
pet_world_update6 transition rowscompile-time table sizes
pet_world_apply_action6 transition rowscompile-time table sizes
pet_world_resetnoneone structure assignment
pet_world_snapshotPET_NAME_CAPACITY name copycompile-time capacity
pet_random_seednoneone assignment
pet_random_nextnonefixed arithmetic, no retry loop

The only loops in the Core are the bounded name scan, the snapshot name copy, and the two transition table scans. No operation iterates over elapsed time, over a delta, or over a value drawn from a random source, so a large accepted input costs exactly what a small one costs. Nothing is allocated, resized, freed, or deferred to a first call.

A unit test asserts that PetWorld stays within 96 bytes. The exact size depends on the target because padding and size_t width differ, so the bound is a regression guard rather than a portable guarantee. Its purpose is to make world growth a deliberate decision. A capacity change that exceeds the bound requires the new size and its justification to be recorded here.

The bound was raised from 64 to 96 bytes when the pet activity was added. Keeping 64 would have meant narrowing name_length below size_t, which contradicts the object size rule in code rules section 5.1, so the bound moved instead of the contract.

The semantic action took PetWorld from 72 to 88 bytes and left 8 bytes of headroom under that bound. The next field is likely to exceed it, which makes the next world growth a recorded decision rather than an accident. That is the purpose of the bound.

Large state copies must not be introduced only to imitate immutability.

8.4 Validation and error handling

Expected failures use explicit status values. The status set remains small and describes public contract failures rather than internal implementation details.

Validation covers:

  • null pointers where relevant
  • invalid lifecycle order
  • invalid enum values
  • unsupported actions
  • invalid time values
  • corrupt internal invariants detected at a public boundary

Assertions may support development diagnostics. They must not replace public contract validation.

8.5 Persistence

Durable persistence does not apply to v0.1.0. The release contains no file format, storage interface, compatibility promise, or migration behaviour.

World lifecycle design must avoid preventing a later persistence boundary.

8.6 Logging and diagnostics

The Core does not log directly. The headless host may print snapshot and status information for development use.

Diagnostic text is not a stable API and is not persisted.

8.7 Functional design

Core code prefers:

  • deterministic functions
  • immutable configuration
  • explicit state ownership
  • small transformations with narrow responsibilities
  • separation between decisions and effects
  • derived data instead of duplicated mutable data
  • exhaustive handling of domain variants

Core code does not need to avoid useful local mutation or allocation when the accepted ownership contract requires them.

8.8 Language and documentation

Code and developer documentation use British English. Public domain identifiers use the pet prefix. Detailed comment, naming, and formatting rules belong in code rules when that document is created.

9. Architecture decisions

9.1 Accepted decisions

  • the release is headless
  • Pet Core uses ISO C99
  • one world owns one pet
  • the Core owns domain behaviour
  • the host forms the imperative shell
  • time is explicit
  • input uses semantic actions
  • output uses a read only snapshot
  • the world is caller owned with fixed capacity, see ADR-001
  • simulation time is an explicit millisecond delta, see ADR-002
  • the minimal autonomous transition is an elapsed time threshold that turns an idle pet curious
  • pet state changes live in static internal transition tables, see ADR-004
  • activity and expression are separate state regions rather than one combined value
  • the snapshot is a derived contract rather than a mirror of the world, see ADR-005
  • a snapshot is a caller owned value that copies the pet name and holds no pointer
  • the first semantic action is PET_ACTION_GREET, dispatched through pet_world_apply_action
  • no adapter interface is required
  • no durable persistence format is required
  • no visual frontend is required
  • embedded platforms are portability goals rather than supported targets

9.2 Open decisions

The following decisions must be resolved before implementation completion:

  • public status values
  • build system
  • test framework
  • naming and final source layout

Choices with meaningful alternatives receive an Architecture Decision Record.

9.3 Architecture deviations

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

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

10. Quality requirements

10.1 Requirement mapping

Requirementv0.1.0 tacticEvidence
SPEC-NFR-001ISO C99 Core with no platform dependencyGCC and Clang builds
SPEC-NFR-002Bounded world and no unbounded collectionsOwnership review and tests
SPEC-NFR-003Explicit time, actions, and optional randomnessDeterministic unit tests
SPEC-NFR-004Validate before observable mutationNegative tests
SPEC-NFR-005Small bounded transitionsScenario execution checks
SPEC-NFR-006Core, host, and test boundariesDependency review
SPEC-NFR-010British English and pet prefixCode and documentation review

10.2 Required test areas

  • valid world lifecycle
  • invalid lifecycle input
  • explicit time update
  • semantic action validation
  • autonomous transition
  • deterministic replay of a controlled scenario
  • snapshot derivation
  • snapshot independence from mutable state
  • unchanged state after rejected input
  • cleanup after failure where applicable

10.3 Required development checks

  • strict ISO C99 compilation
  • high compiler warning level
  • warnings treated as errors in the verification build
  • GCC build and test execution
  • Clang build and test execution
  • AddressSanitizer where supported
  • UndefinedBehaviorSanitizer where supported
  • static analysis selected by the implementation plan
  • Core boundary verification by compiled symbol scan
  • public header self-containment compilation
  • runtime allocation verification under a memory checker

Exact commands belong in build and testing documentation.

10.3.1 Automated boundary verification

Three checks turn architecture claims into gate failures rather than review findings.

The symbol scan reads the compiled Core archive and fails when it needs an external symbol that is not explicitly allowed. It reads symbols rather than source text, so a comment or string literal naming malloc cannot fail it, and a reference that a review would have missed cannot pass. The allowlist is closed and per toolchain, because what a compiler emits for a loop idiom is a property of that compiler rather than of Pet Core. GCC is allowed nothing at the verified settings. Clang is allowed memcpy and memset in at most two object files each, which is what structure assignment in snapshot.c and world.c currently emits. The cap is the count Core needs today, so a new call site fails the gate and has to be looked at rather than absorbed silently.

Header self-containment compiles each public header as a translation unit that includes only that header, under strict C99 with pedantic errors and warnings as errors. Headers are discovered rather than listed, so a new public header is covered without editing the check.

Runtime allocation verification runs a probe that links Pet Core and no test framework, exercises every public operation including its rejecting paths, and runs under Valgrind requiring zero heap usage. The probe writes no diagnostic output, because a stdio buffer is itself a heap allocation. The symbol scan shows that Core does not reference an allocator and this shows that Core does not allocate while running. Neither statement implies the other.

CheckGateLinux GCCLinux ClangWindows
Core symbol scanstrictVerifiedVerifiedUnavailable, MSVC dumpbin output is not parsed
Header self-containmentstrictVerifiedVerifiedUnverified, the check is toolchain neutral
Runtime allocationauditVerifiedNot run, the probe is measured onceUnavailable, Valgrind does not run on Windows

The Windows entries are recorded as unavailable rather than passing. A check that never executed is not evidence.

10.4 Release compliance

The release complies with this architecture when:

  • all release exit criteria are satisfied
  • implemented component boundaries match this document
  • accepted architecture decisions are reflected in code
  • unresolved limitations are recorded in known limitations
  • verified platform support is not overstated
  • build and test evidence is recorded
  • this document reflects the released architecture

10.5 Public API failure audit

Every v0.1.0 public operation was audited against the validation rules in section 8.4 and the rejected input flow in section 6.3. A dash marks a case the operation cannot reach.

OperationNull argumentInvalid enumInvalid rangeInvalid lifecycleRejection leaves state untouched
pet_version_string-----
pet_world_initworld, config, and pet_name-empty name and name filling the capacityinitialising over a valid worlddestination and other worlds
pet_world_validateworldactivity and expressionname length, clock invariants, and region timersempty world and broken markerworld is read only
pet_world_updateworldactivity through validationdelta above the maximum and clock overflowempty and corrupt worldworld byte for byte
pet_world_resetworld--accepts empty, corrupt, and never written storagenot applicable, reset always commits
pet_world_apply_actionworldunknown and negative action-empty and corrupt worldworld byte for byte
pet_world_snapshotworld and destinationactivity and expression through validation-empty and corrupt worlddestination byte for byte
pet_random_seedsource---source untouched
pet_random_nextsource and output-zero bound-source and output untouched

The audit confirmed four properties.

Validation always precedes mutation. Each operation that writes runs its whole check set before its first store, so a rejected call leaves the world byte for byte unchanged. The tests compare full structures rather than selected fields.

Rejection ordering is defined where two faults are present at once. A null argument is reported before an invalid state, and an invalid state is reported before an invalid argument. This keeps a status meaningful when a caller supplies more than one bad input.

Output initialisation is explicit. pet_world_snapshot writes every byte of its destination on success and none of it on failure, so a caller never reads a partly written snapshot. pet_world_init and pet_random_seed never read their destination, which makes indeterminate storage a valid argument.

No failure path terminates the process. The Core includes only stddef.h and stdint.h, both of which supply types rather than functions. It contains no assertion, abort, exit, raise, signal, or longjmp. Reading the undefined symbols of libpet_core.a from the GCC and Clang release-shaped builds returns only the internal Core symbols, plus the memcpy and memset that Clang emits for loop idioms. The __ubsan_*_abort symbols appear only in the sanitiser configuration, which is a diagnostic build and not a release output.

pet_version_string is the one public operation with no failure case. It reads no world, so it has no lifecycle precondition and returns no status. Its return was undocumented before this audit and the header now states that it is a null-terminated string and never null. The string has static storage and the caller neither owns nor frees it, which a test pins by comparing the pointer across calls.

The audit added no product behaviour and changed no public API shape.

10.6 Release evidence

This section collects the evidence behind the v0.1.0 claims. Every row was produced by the audit gate on the host recorded in section 7.2.

ClaimEvidenceResult
Core builds as strict ISO C99GCC and Clang builds with warnings as errorsPass
Core needs no allocatorsymbols.py over libpet_core.a, closed per-toolchain allowlistPass, GCC needs nothing external, Clang needs memcpy and memset
Core allocates nothing at runtimealloc.py over pet_alloc_probe under ValgrindPass, 0 allocations, 0 memory errors
Public headers need no include orderheaders.py over 8 discovered headers, GCC and ClangPass
Public failure behaviour is defined148 unit tests plus the audit in section 10.5Pass
Deterministic replayunit tests and the repeated headless scenarioPass
Core runs without a frontend, adapter, storage, or networklibpet_core.a builds with PET_BUILD_HEADLESS=OFF and PET_BUILD_TESTS=OFFPass
Memory model matches the declarationmeasured sizes against section 8.3Pass
No memory errors in the test suitealloc.py over pet_tests with --allow-heapPass, 13 framework allocations, 0 errors
Core coverageGcovr over src100% lines, functions, and branches, 97.0% decisions

The measured storage matches every figure declared in section 8.3 under both compilers: PetWorld 88, PetSnapshot 56, PetEntity 64, PetWorldConfig 8, PetRandom 8, and both transition row types 24, giving 96 and 48 byte tables. PetWorld remains under its 96 byte regression bound with 8 bytes of headroom.

Two checks are unavailable outside Linux and neither is reported as passing anywhere else. The symbol scan does not parse MSVC dumpbin output, and Valgrind does not run on Windows, so the runtime allocation check cannot execute there. The header check is toolchain neutral but has not been executed on Windows. These gaps are recorded as LIM-002 in known limitations and they do not block the release, because Windows is not a required environment for v0.1.0.

No check required for the release failed or was skipped.

11. Risks and technical debt

11.1 Current risks

  • designing public types before the minimum domain model is understood
  • making the snapshot specific to console diagnostic output
  • claiming embedded support without cross compilation
  • introducing allocation patterns that are difficult to replace
  • adding random behaviour before deterministic transitions are stable
  • implementing future mood or progression systems inside the minimal model
  • allowing the headless host to accumulate product rules
  • creating too many source files for the small release scope

11.2 Accepted limitations

  • no visual product experience
  • no durable persistence
  • no adapter boundary implementation
  • no embedded build verification
  • no compatibility promise beyond the release contracts
  • no complete long term pet development model

Accepted limitations are part of release scope. They do not require entries in known limitations unless implementation creates an additional limitation not stated here.

11.3 Technical debt

No technical debt is accepted before implementation begins.

Any deliberate shortcut discovered during implementation must be linked to a PBI and recorded in the appropriate project tracking document.

12. Glossary

12.1 Terms

Pet Core: The ISO C99 domain library that owns authoritative pet and world behaviour.

World: The authoritative state container that owns one pet.

Headless host: A developer executable that drives the Core without a visual frontend.

Semantic action: A platform independent representation of user intent.

Autonomous transition: A state change selected by Core rules without direct user input.

Snapshot: A read only derived value containing observable pet state.

Functional core: The deterministic domain area that transforms explicit input and owned state.

Imperative shell: The host area that performs platform effects and supplies their results to the Core.

Portable by design: A design claim that known platform dependencies are avoided. It is not a tested platform support claim.

12.2 References