Skip to content

Releases: Redrum624/aura-life

aura-life 0.3.1

Choose a tag to compare

@Redrum624 Redrum624 released this 03 Sep 18:52

aura-life 0.3.1 is on PyPI: pip install aura-life.

Fixed

  • aura_life.__version__ reported 0.1.0 in the 0.3.0 wheel while the distribution
    metadata said 0.3.0: the attribute and pyproject.toml were 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

Choose a tag to compare

@Redrum624 Redrum624 released this 03 Sep 17:47

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 scalar sanity in [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
    -- more struggles, character_defects and intrusive_thought_themes mean 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 intensity over a closed BLOW_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 closed RECOVERY_KINDS
    (rest, warmth, relief, answered, achieved), scaled by a resilience
    of 1 / 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. Entering breaking queues one event the
    host drains with drain_events(); entering broken sets 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 an rng
    is injected -- rng=None is the default, so an existing seeded consumer's
    sequence is untouched.
  • LifeService wires it like the other engines. Built from the definition
    (a new sanity_rng= keyword passes the one-draw rng through; None by
    default), exposed as LifeService.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) and export_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.
    strained or
    worse: affect carries a stressor named "sanity", cleared when sound
    again. A persona whose baseline already sits below sound (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 of 0.2 (inhibition down, the
    temptation bar for intrusive_winning down 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 on life_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
    above SANITY_STRESSED_LEVEL (0.2, the floor of affect's own stress
    description) -- not the stress.sources labels, which the service never
    resolves (struggle:*, money worries) and which would otherwise erode
    every persona with a struggle to broken in 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 recovers inhibition
    toward its baseline every call and recomputes intrusive_winning from
    scratch, so a one-shot nudge would have been a blip. The pull lowers the
    baseline the tick recovers toward and is applied to inhibition at once,
    both ways; restraint_pressure reads it back. It travels in shadow's row
    like the other baselines, because the drop is already in the stored
    inhibition and a load that re-applied it would lower restraint twice.
  • **`tests/test_sanity.p...
Read more

aura-life 0.2.0

Choose a tag to compare

@Redrum624 Redrum624 released this 27 Aug 12:34

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

  • PersonaSchedule no longer ships three hardcoded characters. The
    florence / samantha / alice dispatch and their authored weekly schedules
    are removed; PersonaSchedule(persona_id, events=None) is now a
    host-populated container with add_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 host get_config() and no
    db_path, the constructor raises ValueError instead of scattering a
    database file.
  • emotion_engine.TRAITS is renamed DEFAULT_OCEAN_TRAITS and revalued to
    a neutral 0.5-across profile (it was one specific character's OCEAN scores).
    Imports of TRAITS break; personas relying on the implicit baseline now get a
    flat one.
  • MultiPersonalityManager.current_id returns Optional[str], initially
    None (previously a hardcoded persona id nobody had registered).
  • ProfileDatabase.update_field() raises ValueError on an unknown field
    (previously sqlite3.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 from data_dir — including an
    arbitrary-read primitive via get_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 raises AttributeError, and calendar triggers — which had silently
    never fired in any deployment — now run; datastore is an optional final
    constructor argument.
  • persona_id is no longer silently discarded for relative db_path values,
    and activity emotions are never persisted under an invented fallback id.
  • Silent except Exception: pass swallows 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.