Skip to content

Rename to affect-kernel, ground every constant, and evaluate the scorer - #6

Merged
kevindechang merged 7 commits into
mainfrom
feat/affect-kernel
Aug 21, 2026
Merged

Rename to affect-kernel, ground every constant, and evaluate the scorer#6
kevindechang merged 7 commits into
mainfrom
feat/affect-kernel

Conversation

@kevindechang

Copy link
Copy Markdown
Owner

Six commits taking the repository from "well-engineered but unfalsifiable" to
something a researcher can check.

What changed

Rename. anjo-coreaffect-kernel on GitHub, PyPI, and npm; module
anjo_coreaffect_kernel. The library is named for what it is rather than
for the application it came from. Nothing was ever tagged or published under the
old name. Provenance references to Anjo are deliberately kept.

Provenance. docs/foundations.md tags every constant L (literature),
P (production-tuned), or B (bounded arbitrary choice), and states the
departures instead of glossing them: appraisal is a lookup table on a
pre-classified intent rather than an appraisal process over OCC variables; mood
decays per turn, not per unit of time; retrieval multiplies where the closest
comparable sums; the mood-congruence asymmetry and every decoder control have no
citation at all; O, C, and A are validated, stored, and ignored. All 14 DOIs
verified against Crossref and 5 arXiv IDs against the arXiv API.
CITATION.cff validates against CFF schema 1.2.0.

A numeric seam. The kernel let a caller replace every word it emits while
every number stayed compiled in. AffectDynamics and RetrievalWeights close
that. Defaults reproduce the pinned contract exactly, which is the proof the
refactor preserved behavior — all 225 cross-runtime vectors pass untouched.

An evaluation that reports losses. bench/ is seeded, dependency-free, and
deterministic: five regimes, relevance assigned before any feature is drawn so
ground truth is independent of every formula under test. Regime A exists
specifically so the benchmark can embarrass the kernel.

Finding Result
Helps when its assumptions hold +0.415 MRR over similarity-only
Hurts when they don't −0.175 MRR
Additive form (Park et al.) beats ours +0.111 MRR against us
…but the weight is the cause, not the shape significance_weight 0.03→1.0 lifts MRR 0.858→0.960
Linear vs exponential and power-law recency linear wins by 0.009 / 0.044 at matched half-life
Mood congruence, in a regime built to favour it +0.012 MRR

The linear-recency result retracts a claim this branch made two commits
earlier
. bench/RESULTS.md is generated and drift-checked in CI, so no
document can quote a stale number.

Hardening. CompanionState/CompanionEngineAffectState/AffectEngine
(the last product noun in the public API; updates 8 fixture values under the
parity contract). Seeded property/fuzz suites in both runtimes over clamping,
200-turn adversarial walks, determinism, non-mutation, ranking total order,
Unicode, and serialization — no new dependency. docs/threat-model.md states
the one boundary the kernel enforces and the two ways an adapter silently undoes
it.

Test plan

  • ./scripts/check.sh green from a clean checkout
  • Python 173 → 227 tests, coverage 90% → 91%
  • TypeScript 59 → 78 tests, coverage 95.84% → 96.24%
  • All 225 shared vectors and 3 longitudinal traces pass in both runtimes
  • New tests mutation-checked: 4 deliberate mutations each failed exactly the
    test claiming to cover them
  • All 11 figures cited in bench/README.md verified against the generated
    table programmatically
  • CI green on Linux (this PR)

Not done, and marked as such

The README's headline claim — that deterministic state holds a character better
than a prompt-only persona — is still untested. Falsification items (1) and (5)
in foundations.md need affect trajectories with external ground truth this
repository does not have.

Name the library by what it is rather than by the application it was
extracted from. "anjo-core" reads as the core of one product; the code is a
general affect kernel and the game-NPC example already demonstrates that.

- Python distribution and module: anjo-core/anjo_core -> affect-kernel/affect_kernel
- npm package: @anjo-ai/core -> affect-kernel
- prose and repository URLs updated throughout

No version was ever tagged or published under the old name, so nothing
installed is affected. Provenance references to Anjo are deliberately kept.
Behavior is unchanged: identical test counts and coverage before and after
(Python 173 tests / 90%, TypeScript 160 tests / 95.84%).
The library named OCC, PAD, ALMA, and Big Five and cited nobody. A reader had
no way to tell which numbers came from published work, which were tuned against
the Anjo deployment, and which were arbitrary.

docs/foundations.md now tags every constant L (literature), P (production-tuned),
or B (bounded choice), and states the departures rather than glossing them:

- appraisal is a lookup table on a pre-classified intent, not an appraisal
  process over OCC appraisal variables the way EMA and FAtiMA are;
- mood decay is per turn, not per unit of wall-clock time;
- retrieval multiplies where the closest published comparable sums, and decays
  recency linearly where human forgetting follows a power law;
- the mood-congruence asymmetry has no citation behind it at all;
- decoder controls have no literature behind them at all;
- O, C, and A are validated, stored, and then ignored.

Also adds a "what would falsify these choices" section, so the claims are
answerable rather than decorative.

All 14 DOIs and 5 arXiv IDs verified against Crossref and the arXiv API.
CITATION.cff validates against CFF schema 1.2.0 (cffconvert).

Documents one behavior that was implemented but absent from algorithm.md: the
ambiguous-intent valence amplification (x1.10 negative, x1.04 positive above
|v| >= 0.20). Verified against the runtime, not read off the source.

No behavior change: 173 Python tests / 90%, 160 TypeScript tests / 95.84%.
foundations.md pointed at analysis/ and bench/ directories that do not exist
yet. Replaced each with either a measured number or an explicit statement that
the work has not been done, so the document makes no promise the repository
does not keep.

The mood half-life is 2.25 turns at the default personality (phi 0.735),
computed from the runtime rather than estimated.
The kernel already let a domain replace every word it emits — stage names,
expectation cues, turn-shape rules, presence labels — while every number stayed
compiled in. A game, a tutor, or a support agent could rename the rungs but not
disagree with the inertia curve without forking.

AffectDynamics and RetrievalWeights close that gap in both runtimes: inertia
base and trait terms and clamp, the resting-dominance coefficient, the baseline
blend, per-emotion carry decay and its fallback and floor, the ambiguity
amplification, the recency horizon and floor and parse fallback, significance
and rehearsal weights, the episode bonus, and the mood-congruence threshold and
its negative/positive asymmetry.

Defaults reproduce the pinned contract exactly, which is the proof the refactor
preserved behavior: all 225 cross-runtime vectors and 3 longitudinal traces pass
untouched in both runtimes. The baseline blend keeps two independent fields
rather than a retention plus its complement, because 1 - 0.98 is not exactly
0.02 in binary floating point.

36 Python and 12 TypeScript tests cover the new seam, including that the
defaults are indistinguishable from omitting the argument. Three deliberate
mutations (dropping the caller's weights at the ranking entry point, pinning
carry decay to the module default, ignoring the carry floor) were confirmed to
fail exactly the tests that claim to cover them.

Python 173 -> 209 tests at 90% coverage; TypeScript 160 -> 196 at 96.06%.
foundations.md labelled most retrieval constants "production-tuned, never
ablated" and listed five results that would falsify them. Three needed no model
and are now run.

bench/ is seeded, dependency-free, and deterministic. Five regimes x 400 queries
x 20 candidates, with relevance assigned before any feature is drawn so that
ground truth is independent of every formula under test. Regime A exists
specifically so the benchmark can embarrass the kernel; regime D is the only one
where all its assumptions hold at once.

Findings, including the ones against us:

- the machinery is not free: -0.175 MRR against plain similarity when its
  assumptions are violated, +0.415 when they hold;
- Park et al.'s additive form beats the multiplicative form by 0.111 MRR in the
  fairest regime;
- but the composition is not the cause. Significance enters this scorer at 0.03
  and Park's at 1.0; raising that one parameter lifts MRR 0.858 -> 0.960 against
  additive's 0.968, with the multiplicative form untouched. The salience term is
  underpowered, not misshapen;
- mood congruence is worth +0.012 MRR in a regime built to favour it;
- linear recency is *not* the weak point. This retracts a claim this repository
  made two commits ago in foundations.md and in the recency_weight docstring:
  at a matched 30-day half-life linear beats exponential by 0.009 and power-law
  by 0.044. The claim is retracted in place rather than quietly softened.

bench/RESULTS.md is generated, and check.sh and CI fail on drift, so no document
can quote a stale number. All 11 figures cited in bench/README.md were verified
against the generated table programmatically.

Limitations are stated at the same volume as the results: synthetic corpora,
machine-assigned ground truth, drawn rather than embedded similarity, one gold
per query, no language model anywhere. The README's larger claim — deterministic
state beats a prompt-only persona — remains untested and is marked as such.
…fuzzing

Two hardening items the roadmap had been carrying.

CompanionState -> AffectState and CompanionEngine -> AffectEngine, in both
runtimes, with the compound names (createAffectState, AffectStateInput,
ResolvedAffectState, AffectEngineOptions) following. A library called
affect-kernel whose central type made a game NPC instantiate a "companion" was
not coherent. The presence vector's `source` field changes from
"companion_state" to "affect_state", updating 8 expected values in
shared/golden/kernel_golden.json — a reviewed fixture change under the parity
contract, safe because nothing was ever published.

Seeded property and fuzz suites in both runtimes, using a fixed-seed PRNG rather
than a property-testing dependency so a failure is re-runnable from the seed
alone and the zero-dependency claim is untouched. They cover: PAD, baseline, and
carry staying in domain across 200-turn adversarial walks; determinism under
repeated identical input; appraisal never mutating the caller's state; ranking
as a total order that dedupes, sorts, respects its limit, and ignores input
order; the length factor never growing a budget; the decoding envelope; Unicode
handling over combining marks, zero-width and RTL controls, NEL, BOM, and astral
codepoints; and pickle/deepcopy round trips. A mutation that let the length
factor grow a budget was confirmed to fail the test that claims to forbid it.

Adds docs/threat-model.md, which states the one boundary the kernel actually
enforces (untrusted evidence is a separate type, structurally excluded from the
system prompt, bounded at 2k/8k characters), the two ways an adapter silently
undoes it — flattening the channel on the wire, and returning a no-op
transaction — and an explicit list of what the kernel does not defend against,
including the affect-derived side channel in response length and temperature.

Python 173 -> 227 tests at 91% coverage; TypeScript 59 -> 78 at 96.24%.
A single-maintainer project that does not say so reads as either abandoned or
unresponsive when a contribution sits for a week. Says the response time out
loud, and orders what gets looked at first — with evidence that contradicts the
repository's own claims ranked second, since the linear-recency claim has
already been retracted on exactly that basis.
@kevindechang
kevindechang merged commit 5c0c681 into main Aug 21, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant