Releases: Redrum624/aura-life
Release list
aura-life 0.3.1
aura-life 0.3.1 is on PyPI: pip install aura-life.
Fixed
aura_life.__version__reported0.1.0in the 0.3.0 wheel while the distribution
metadata said 0.3.0: the attribute andpyproject.tomlwere maintained separately and
only one of them was bumped. The version now lives in the attribute alone and
setuptools reads it (dynamic = ["version"]), so the two cannot drift; a smoke test
asserts they agree.
aura-life 0.3.0
aura-life 0.3.0 is on PyPI: pip install aura-life (or "aura-life[scheduler]").
personas are no longer all women, the world clock is injectable, and the generator's output finally fits its consumer
_generic_concept returned a hardcoded "gender": "female" from three literal
sites, and that was the small part of the problem. All seven appearance_templates
opened "{age}-year-old woman ...", the module carried 209 she/her occurrences
and 15 woman/daughter/mother/sister, and every one of the seven name pools was
female. Deleting the three literals without touching the prose would have produced
a persona the library calls male and then describes as "She has sat with ten
thousand people" — a worse defect than the one being fixed, because it would look
fixed. So this release rewrites the corpus, not the constant.
It is a minor bump rather than a patch, and it does reach existing callers.
Nothing public was removed and no signature changed incompatibly — gender was
appended to both builders, so every v0.2.0 call form still binds — but the default
gender is now drawn instead of always being "female", and the rng stream gained
one draw at the front. A caller replaying a run off a stored seed gets a different
persona than it did on 0.2.0. Both consequences are spelled out under Changed.
The release carries a second, unrelated change: the world and the energy engine
no longer hard-code the host wall clock. EnergySystem read datetime.now()
for hours_awake, which is right for a companion app that lives as long as the
person using it and wrong for anything that simulates time — the engine could
not be replayed, stepped, or unit-tested by any consumer without freezing global
state. It is additive and byte-identical by default: now defaults to
datetime.now at every new seam, so a caller that passes nothing gets exactly
what it got before. Details under Added and Fixed.
The third change closes a gap between two halves of this same package.
build_genre_concept() emits a 26-key dict; LifeService(definition=...) wants
a PersonalityDefinition. Seventeen of those keys are fields of that dataclass
and nine are not (age, archetype, description, gender, genre, goal,
intensity, style_theme, tone_directive), so PersonalityDefinition(**concept)
raises TypeError — and no converter existed anywhere in the package. The path
of least resistance, handing the raw dict over as definition, is worse than
the error: it works. Every read of the definition in life_service.py is
getattr(definition, name, default), a dict answers none of them, and every
engine starts from its empty default with no exception and no log line. That is
a defect a consumer meets the first time it wires the generator to the service,
which is why the fix is a method on the library's own dataclass and not an
adapter in someone's app. Two smaller seams ship alongside it, on the same
argument: a supported way to register a genre, and a way to generate a cast
whose names do not collide. All three are additive.
The fourth change is a new engine, and it is the one the library was missing.
Eight engines carry interior -- mood, stress and loneliness in affect; unease,
felt safety, doubt, intrusive thought, concealment and masking in shadow; focus
in cognitive -- and none of them integrates those into a trajectory that can
break. A persona can be stressed, lonely, afraid and lying all at once,
indefinitely, and be the same persona tomorrow. SanitySystem is the seam that
ends: one scalar the host's blows push down, its recoveries push up, and world
time erodes or mends between, read through a closed graded vocabulary. Details
under Added.
The fifth change is the one the fourth exposed: the energy tick now takes an
rng. Every draw on LifeService._on_energy_tick -- identity's struggle,
defect and tendency rolls, desire's arousal drift, cognitive's monologue,
drive's avoidance roll, career, finance, errands, memory-time's nostalgia, the
life-event title, and the service's own coin between a surfaced struggle and a
rumination -- came from the module-level random. That is invisible to a
companion app, which never replays anything, and fatal to a consumer that does:
a host that seeds one random.Random per persona and drives two loops from the
same seed still watches them diverge, because the library keeps drawing from a
stream the host does not own, and nothing the host can seed reaches it short of
reseeding the global before every tick. Before SanitySystem nothing a host
decided on read those numbers, so the drift stayed in the digest text. Now
the sanity tick reads affect's stress level as stressed, a struggle surfacing
on one loop and not the other is a different state word from that hour on, and
the run is not reproducible by any consumer. A library tick that draws from
module random cannot be replayed, stepped or unit-tested against a fixed
outcome by anyone but the library itself. LifeService(rng=...) closes it,
additively and byte-identically by default; details under Added.
Added
SanitySystem-- the one interior number that integrates and can break.
aura_life.sanity(exported from the facade beside the other engines) holds
one scalarsanityin [0, 1] and the state word a consumer couples to:
STATES = ("sound", "strained", "fraying", "breaking", "broken"), with the
thresholds living in exactly one place (state_for) so a retune here cannot
ripple outward. It starts at a per-persona baseline read from the definition
-- morestruggles,character_defectsandintrusive_thought_themesmean a
lower start and a higher intensity -- rather than at 1.0, so fragility is a
lean the persona already carries and not a rule a wizard wrote. Severity is
the blow's, intensity is the person's:on_blow(kind, severity)costs
severity x BLOW_WEIGHT[kind] x intensityover a closedBLOW_KINDS
(grief,witnessed,did_harm,broke_value,rejected,neglect,
concealment); what a "grief" is stays the host's business.
on_recovery(kind, amount)is the way up over a closedRECOVERY_KINDS
(rest,warmth,relief,answered,achieved), scaled by a resilience
of1 / intensity-- the same burdens that make a blow land harder make rest
count for less.tick(hours, *, stressed, concealment_load)erodes while a
stressor is live or a front is being kept up (scaled by shadow's
concealment_load, so the mask costs something without the host writing a
rule) and mends toward the baseline otherwise; the engine reads no clock and
is told how much world time passed. Enteringbreakingqueues one event the
host drains withdrain_events(); enteringbrokensets a terminal flag
that only a reported recovery lifts. Replayable by construction: the single
random draw is a baseline jitter at construction, taken only when anrng
is injected --rng=Noneis the default, so an existing seeded consumer's
sequence is untouched.LifeServicewires it like the other engines. Built from the definition
(a newsanity_rng=keyword passes the one-draw rng through;Noneby
default), exposed asLifeService.sanity, ticked from the energy tick with
hours measured on the world clock the way energy measures them, reported in
get_status()["sanity"](number and word) andexport_inner_state(),
persisted in its own row (life_sanity_state) so a restarted host resumes the
same number, word, flag and pending events.on_sanity_blow()and
on_sanity_recovery()report through the service so the couplings apply at
once; a host that calls the engine directly is coupled at the next tick.- Three couplings from the word to the library, applied by
LifeService
glue and never inside the engine, on a change of the word.strainedor
worse: affect carries a stressor named"sanity", cleared whensound
again. A persona whose baseline already sits belowsound(most shipped
genre personas do) is not coupled for merely existing: the couplings fire
when the word changes, so a persona that is never hit is byte-identical to
one built before this engine existed.fraying
or worse: shadow holds a restraint pull of0.2(inhibition down, the
temptation bar forintrusive_winningdown by the same), released when the
word climbs back above.breaking: affect's regulation capacity collapses
once, on the way in. The amounts are module constants onlife_service
(SANITY_STRESS_SOURCE,SANITY_FRAYING_RESTRAINT_PRESSURE,
SANITY_BREAKING_REGULATION_COLLAPSE,SANITY_STRESSED_LEVEL); the words
are the contract. "Stressed", for the tick, is affect's stress level at or
aboveSANITY_STRESSED_LEVEL(0.2, the floor of affect's own stress
description) -- not thestress.sourceslabels, which the service never
resolves (struggle:*,money worries) and which would otherwise erode
every persona with a struggle tobrokenin days without a single blow.
The +0.05 the"sanity"stressor itself adds sits under that floor, so the
state that opens it is not the state that keeps eroding. The word the
couplings were last applied for travels in the sanity row, so a reload
re-holds shadow's pull without firing the entry event twice. ShadowSystem.set_restraint_pressure(amount), the smallest public seam
that made the fraying coupling hold: shadow's tick recoversinhibition
toward its baseline every call and recomputesintrusive_winningfrom
scratch, so a one-shot nudge would have been a blip. The pull lowers the
baseline the tick recovers toward and is applied toinhibitionat once,
both ways;restraint_pressurereads it back. It travels in shadow's row
like the other baselines, because the drop is already in the stored
inhibitionand a load that re-applied it would lower restraint twice.- **`tests/test_sanity.p...
aura-life 0.2.0
aura-life v0.2.0 — pre-publication hardening
First public release. Everything here landed after the v0.1.0 tag (which does
not contain these changes) and before the repository was made public: 0.1.0's
"behaviour identical to the origin, defects recorded rather than fixed" contract
ended, a five-dimension audit ran (privacy, security, memory leaks, mock data,
docs accuracy), and the approved findings were fixed test-first.
This release contains breaking changes. If you consumed v0.1.0, read the
list below before upgrading.
Breaking changes
PersonaScheduleno longer ships three hardcoded characters. The
florence/samantha/alicedispatch and their authored weekly schedules
are removed;PersonaSchedule(persona_id, events=None)is now a
host-populated container withadd_event(), and empty is the documented
default for every id.LifeService(db_path=...)is effectively required. The default is no
longer the CWD-relative"life.db"; with no hostget_config()and no
db_path, the constructor raisesValueErrorinstead of scattering a
database file.emotion_engine.TRAITSis renamedDEFAULT_OCEAN_TRAITSand revalued to
a neutral 0.5-across profile (it was one specific character's OCEAN scores).
Imports ofTRAITSbreak; personas relying on the implicit baseline now get a
flat one.MultiPersonalityManager.current_idreturnsOptional[str], initially
None(previously a hardcoded persona id nobody had registered).ProfileDatabase.update_field()raisesValueErroron an unknown field
(previouslysqlite3.OperationalError), and field names are now validated
against the table schema — it no longer builds SQL from caller input.GoalEngine.to_dict()["completed_count"]/["abandoned_count"]are
retained-history counts, not lifetime totals, a consequence of capping goal
history.- Persona-id case is uniform (lowercased) at every id → path boundary, and
ids must match[a-z0-9_-]{1,64}— invalid ids are rejected with
ValueError, never repaired.
Security
- Persona ids are validated at every id → path boundary (
safe_persona_id()/
safe_join()); the..path-traversal escape fromdata_dir— including an
arbitrary-read primitive viaget_owner_device_id()— is closed. ProfileDatabase.update_field()SQL-injection sink closed (schema whitelist,
quoted identifiers).- The place kill switch fails closed: an unreadable host config now means
"off", not "on". save_device_location()enforces its documented contract: coordinates are
range-checked and rounded to 2 decimals before storage.
Fixed
- Public calendar API (
add_calendar_entry/get_upcoming_calendar_entries)
no longer raisesAttributeError, and calendar triggers — which had silently
never fired in any deployment — now run;datastoreis an optional final
constructor argument. persona_idis no longer silently discarded for relativedb_pathvalues,
and activity emotions are never persisted under an invented fallback id.- Silent
except Exception: passswallows narrowed and logged; broken host
providers now degrade loudly instead of invisibly. - Eleven unbounded collections/tables capped (applied on write and on load),
and teardown paths fixed:LifeScheduler.stop()waits and releases,
LifeService.stop()joins its background threads before saving state.
Added
clear_persona_schedule()joins the public facade (__all__: 116 → 117
names);clear_emotion_persistence()and
MultiPersonalityManager.remove_personality()add teardown for the remaining
process-global caches.EmotionEngine(ocean_traits=...)and
personality_config.set_default_languages(...)replace two origin-app
hardcodes with inputs.- Six new test modules; the suite grows from 129 tests across 13 modules to 298
across 19, with the original 129 passing unchanged.
Full detail: CHANGELOG.md ([0.2.0]) and DEFERRED.md. License: Apache-2.0 —
see LICENSE and NOTICE.