Pet architecture for v0.1.0
| Document version | Date | Summary |
|---|---|---|
| v1 | 2026-07-17 | Initial architecture for the headless Pet Core release |
| v2 | 2026-07-18 | Record the caller owned fixed capacity world and its storage bounds |
| v3 | 2026-07-19 | Close the time representation decision and record the world clock storage |
| v4 | 2026-07-19 | Close the autonomous transition decision and raise the world storage bound |
| v5 | 2026-07-19 | Record the explicit random source, the update order, and the bounded work limits |
| v6 | 2026-07-19 | Record the semantic action contract and the static transition tables |
| v7 | 2026-07-19 | Record the derived snapshot contract, its field policy, and its storage |
| v8 | 2026-07-19 | Record the public API failure audit and its termination evidence |
| v9 | 2026-07-19 | Record the automated boundary, header, and allocation checks and the gate structure |
| v10 | 2026-07-19 | Record the CMake and Python tooling boundary and the Linux-only check status |
| v11 | 2026-07-19 | Record 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
- code and developer documentation use British English
- C domain identifiers use the
petprefix - this document follows the mandatory version architecture policy
- release numbering follows Semantic Versioning 2.0.0
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
- use ISO C99 as the Core language baseline
- represent user intent as semantic actions
- inject elapsed time explicitly
- expose visible state through a derived snapshot
- validate input before observable mutation
- use one explicit owner for authoritative world state
- keep host effects outside the Core
- control every supported input in tests
- 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:
| Class | Meaning | Example in v0.1.0 |
|---|---|---|
| internal only | present for a mechanism, never exposed | activity_since_ms, expression_since_ms, initialised |
| presentable copy | exposed as an owned copy of the authoritative value | pet_name, activity, expression, elapsed_time_ms |
| presentable derived value | computed at extraction, never stored in a world | none yet |
| deferred | presentable in principle, out of scope for this release | mood, 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.cFiles 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
- the headless host depends only on the public Pet API
- tests prefer the public API
- public headers do not include platform or test framework headers
- Core implementation does not include host headers
- Core implementation uses only approved ISO C99 facilities
- 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:
- validate the world, returning
PET_ERR_NULL_ARGUMENTorPET_ERR_INVALID_STATE - reject a delta above
PET_TIME_DELTA_MAX_MS - reject a delta that would overflow the world clock
- return success without writing anything when the delta is zero
- advance
elapsed_time_ms - increment
update_count - 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:
- validate the world, returning
PET_ERR_NULL_ARGUMENTorPET_ERR_INVALID_STATE - reject an action that is not a member of
PetActionwithPET_ERR_INVALID_ARGUMENT - 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:
- reject a null world or a null destination with
PET_ERR_NULL_ARGUMENT - validate the world, returning
PET_ERR_INVALID_STATE - 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.
| Table | After ms | Trigger | Action | From | To |
|---|---|---|---|---|---|
| activity | PET_CURIOUS_AFTER_MS | time | none | idle | curious |
| activity | PET_ATTENTIVE_FOR_MS | time | none | attentive | idle |
| activity | 0 | action | greet | idle | attentive |
| activity | 0 | action | greet | curious | attentive |
| expression | 0 | action | greet | neutral | happy |
| expression | PET_HAPPY_FOR_MS | time | none | happy | neutral |
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:
- the activity table is evaluated before the expression table
- each table is scanned from the first row to the last
- the first matching row wins and the scan stops
- 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 executablesAll outputs run on the development host. There is no embedded deployment in this release.
7.2 Platform verification matrix
| Environment | Support level | Required for release |
|---|---|---|
| GCC on Linux | Build tested and runtime tested | Yes |
| Clang on Linux | Build tested and runtime tested | Yes |
| MSVC on Windows | Portable by design, configuration provided, unverified | No |
| Clang and Ninja on Windows | Analysis configuration provided, unverified | No |
| ESP-IDF | Portable by design | No |
| Raspberry Pi Pico SDK | Portable by design | No |
| Arduino host | Portable by design | No |
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:
| 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 |
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 auditgate.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/coverageBuilding 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:
| Type | Bytes | Role |
|---|---|---|
PetWorld | 88 | persistent state for one world |
PetSnapshot | 56 | caller owned derived presentation value, held outside the world |
PetEntity | 64 | pet state, stored inline in its world |
PetWorldConfig | 8 | caller owned initialisation input |
PetRandom | 8 | caller owned random source, held outside the world |
PetActivityRow | 24 | one activity transition, static and shared by every world |
PetExpressionRow | 24 | one 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:
| Operation | Maximum iterations | Bound |
|---|---|---|
pet_world_init | PET_NAME_CAPACITY name scan, then the same for the copy | compile-time capacity |
pet_world_validate | none | fixed field checks |
pet_world_update | 6 transition rows | compile-time table sizes |
pet_world_apply_action | 6 transition rows | compile-time table sizes |
pet_world_reset | none | one structure assignment |
pet_world_snapshot | PET_NAME_CAPACITY name copy | compile-time capacity |
pet_random_seed | none | one assignment |
pet_random_next | none | fixed 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 throughpet_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
| Requirement | v0.1.0 tactic | Evidence |
|---|---|---|
| SPEC-NFR-001 | ISO C99 Core with no platform dependency | GCC and Clang builds |
| SPEC-NFR-002 | Bounded world and no unbounded collections | Ownership review and tests |
| SPEC-NFR-003 | Explicit time, actions, and optional randomness | Deterministic unit tests |
| SPEC-NFR-004 | Validate before observable mutation | Negative tests |
| SPEC-NFR-005 | Small bounded transitions | Scenario execution checks |
| SPEC-NFR-006 | Core, host, and test boundaries | Dependency review |
| SPEC-NFR-010 | British English and pet prefix | Code 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.
| Check | Gate | Linux GCC | Linux Clang | Windows |
|---|---|---|---|---|
| Core symbol scan | strict | Verified | Verified | Unavailable, MSVC dumpbin output is not parsed |
| Header self-containment | strict | Verified | Verified | Unverified, the check is toolchain neutral |
| Runtime allocation | audit | Verified | Not run, the probe is measured once | Unavailable, 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.
| Operation | Null argument | Invalid enum | Invalid range | Invalid lifecycle | Rejection leaves state untouched |
|---|---|---|---|---|---|
pet_version_string | - | - | - | - | - |
pet_world_init | world, config, and pet_name | - | empty name and name filling the capacity | initialising over a valid world | destination and other worlds |
pet_world_validate | world | activity and expression | name length, clock invariants, and region timers | empty world and broken marker | world is read only |
pet_world_update | world | activity through validation | delta above the maximum and clock overflow | empty and corrupt world | world byte for byte |
pet_world_reset | world | - | - | accepts empty, corrupt, and never written storage | not applicable, reset always commits |
pet_world_apply_action | world | unknown and negative action | - | empty and corrupt world | world byte for byte |
pet_world_snapshot | world and destination | activity and expression through validation | - | empty and corrupt world | destination byte for byte |
pet_random_seed | source | - | - | - | source untouched |
pet_random_next | source 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.
| Claim | Evidence | Result |
|---|---|---|
| Core builds as strict ISO C99 | GCC and Clang builds with warnings as errors | Pass |
| Core needs no allocator | symbols.py over libpet_core.a, closed per-toolchain allowlist | Pass, GCC needs nothing external, Clang needs memcpy and memset |
| Core allocates nothing at runtime | alloc.py over pet_alloc_probe under Valgrind | Pass, 0 allocations, 0 memory errors |
| Public headers need no include order | headers.py over 8 discovered headers, GCC and Clang | Pass |
| Public failure behaviour is defined | 148 unit tests plus the audit in section 10.5 | Pass |
| Deterministic replay | unit tests and the repeated headless scenario | Pass |
| Core runs without a frontend, adapter, storage, or network | libpet_core.a builds with PET_BUILD_HEADLESS=OFF and PET_BUILD_TESTS=OFF | Pass |
| Memory model matches the declaration | measured sizes against section 8.3 | Pass |
| No memory errors in the test suite | alloc.py over pet_tests with --allow-heap | Pass, 13 framework allocations, 0 errors |
| Core coverage | Gcovr over src | 100% 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.