@adam0white/human-framework 2.19.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export const FRAMEWORK_VERSION = '2.19.0';
2
+ export const FRAMEWORK_VERSION = '2.21.0';
3
3
  export { AFFECT_DEFAULTS, actionTendencies, advanceAffect, appraise, breakAllows, breakBehaviour, breakHazard, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, enableBreaks, feel, inBreak, readAffect, regulate, release as releaseEmotion, skipCrisis, strain, tendencyEmotions, } from './affect/index.js';
4
4
  // `agenda.release` collides with `affect.release`; the agenda one is re-exported under a clearer name.
5
5
  export { AGENDA_DEFAULTS, abandonGoal, adoptGoal, advanceAgenda, agendaTerms, applyExemptions, CARE_DEFAULTS, calendarRetimer, careDuty, commitmentPressure, createAgenda, DEFAULT_PRAYER_TIMES, EID_PRAYER_IMPORTANCE, EXEMPTION_DEFAULTS, eidPrayer, eidWindow, fastWindow, iftarWindow, inMakruhTime, isContinuingCommitment, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, retirePaidMakeUps, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
@@ -25,6 +25,7 @@ export { clearMethodContext, demonstrateMethod, METHOD_DEFAULTS, methodEvidence,
25
25
  export { MIGRATIONS, migratableVersions, migrate } from './migrate.js';
26
26
  export * from './narrate/index.js';
27
27
  export * from './needs/index.js';
28
+ export { confirmObservedReputation } from './observed-reputation.js';
28
29
  export * from './partnering/index.js';
29
30
  export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistence/index.js';
30
31
  export * from './person.js';
@@ -34,6 +35,7 @@ export * from './presentation.js';
34
35
  export { adoptProjectTask, considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal, } from './projects/index.js';
35
36
  export * from './promptings/index.js';
36
37
  export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.js';
38
+ export { qalbReading } from './qalb.js';
37
39
  export { fingerprint, restore, snapshot } from './restore.js';
38
40
  export * from './review/index.js';
39
41
  export * from './scenarios/index.js';
@@ -1,3 +1,29 @@
1
+ /**
2
+ * SCOPE: episodic memory and learned action expectations. Episodes carry a salience that decays
3
+ * exponentially (Ebbinghaus-style forgetting curve, modelled as a single exponential per episode) with a
4
+ * half-life that lengthens for emotionally strong episodes (emotional-enhancement of memory), and recall
5
+ * slightly raises the salience of what is retrieved (testing effect). Expectations are cached action
6
+ * values learned by a delta rule (prediction-error learning, Rescorla-Wagner shape) with a minimum
7
+ * learning rate and a clamped step, blended with the host's advertisement by sample-based confidence.
8
+ * It does NOT model reconsolidation, false memory, interference between similar episodes, sleep-dependent
9
+ * consolidation, semantic abstraction, or model-based planning; parameters are engineering defaults for
10
+ * game time scales, not calibrated estimates.
11
+ *
12
+ * SCOPE (lasting gists, 1.8.0, opt-in per person with `enableGists`): episodic detail does not last a life, but the
13
+ * gist of what mattered does. An episode that leaves the episode list (evicted beyond the bound, or older than
14
+ * `GIST_DEFAULTS.horizon` at a `foldGists` call, which the composite makes at each day's close) is folded into the
15
+ * gist with the same kind, action, people and place when it was emotional enough (|valence| ≥ `minValence`, or a
16
+ * loss); weaker ones are simply forgotten. A gist keeps the encoding-weighted mean valence, a count and the summary of
17
+ * its strongest episode, and fades with half-lives of years (`halfLifeNeutral`..`halfLifeEmotional` by |valence|).
18
+ * Gists answer `recall` and `recallByCue` like episodes (so a remembered fear or loss still comes back at its place,
19
+ * person or anniversary), and `expectedEffect` reports the gists about an option's action or place, which cognition
20
+ * weighs as a `memory` term that fades as fresh experience of the option accumulates. So a fear learned at 20 still
21
+ * shapes choices at 40 after its episodes are gone, and enough good later visits outweigh it. Named shapes: the
22
+ * fading of episodic detail while gist persists (fuzzy-trace theory, Brainerd & Reyna) and the long retention of
23
+ * emotional autobiographical memories; the horizon, thresholds, half-lives and the bound of 64 are engineering
24
+ * choices. It does not model reconstruction errors in gists, deliberate rehearsal beyond retelling (`recall`
25
+ * boosts), or semantic knowledge abstracted from gists.
26
+ */
1
27
  import type { Affordance, AppraisalEvent, EntityId, Episode, Gist, MemoryState, Minute, NeedId, Outcome, Person, Signed, Unit } from '../types.ts';
2
28
  export declare const MEMORY_DEFAULTS: {
3
29
  maxEpisodes: number;
@@ -25,6 +25,7 @@
25
25
  * boosts), or semantic knowledge abstracted from gists.
26
26
  */
27
27
  import { clamp, clamp01, clampSigned, decay, hourOf, isNum, isObj, lerp, runningMean, } from '../core/index.js';
28
+ import { keysOf } from '../core/keys.js';
28
29
  import { DAYS_PER_YEAR, MINUTES_PER_DAY, MINUTES_PER_YEAR } from '../types.js';
29
30
  export const MEMORY_DEFAULTS = {
30
31
  maxEpisodes: 200,
@@ -511,10 +512,7 @@ export function expectedEffect(p, aff) {
511
512
  const samples = learned?.samples ?? 0;
512
513
  const confidence = samples / (samples + MEMORY_DEFAULTS.confidenceK);
513
514
  const needs = {};
514
- const keys = new Set([
515
- ...Object.keys(aff.advertises),
516
- ...(learned ? Object.keys(learned.needs) : []),
517
- ]);
515
+ const keys = new Set([...keysOf(aff.advertises), ...(learned ? keysOf(learned.needs) : [])]);
518
516
  for (const k of [...keys].sort()) {
519
517
  const adv = aff.advertises[k] ?? 0;
520
518
  const lv = learned?.needs[k];
@@ -563,11 +561,7 @@ function updateExpectation(p, key, aff, success, realized, felt) {
563
561
  }
564
562
  x.samples += 1;
565
563
  const n = x.samples;
566
- const keys = new Set([
567
- ...Object.keys(x.needs),
568
- ...Object.keys(aff.advertises),
569
- ...Object.keys(realized),
570
- ]);
564
+ const keys = new Set([...keysOf(x.needs), ...keysOf(aff.advertises), ...keysOf(realized)]);
571
565
  for (const k of keys) {
572
566
  const prior = x.needs[k] ?? aff.advertises[k] ?? 0;
573
567
  x.needs[k] = step(prior, realized[k] ?? 0, n);
@@ -582,7 +576,8 @@ function updateExpectation(p, key, aff, success, realized, felt) {
582
576
  export function learnOutcome(p, aff, outcome, realized, felt) {
583
577
  if (outcome.status === 'interrupted')
584
578
  return;
585
- const success = outcome.status === 'completed';
579
+ // A declared voiding purpose succeeds only when the actual outcome confirms it.
580
+ const success = outcome.status === 'completed' && (aff.voidsBladder !== true || outcome.voidedBladder === true);
586
581
  const touched = new Set([expectationKey({ action: aff.action })]);
587
582
  if (aff.targetId !== undefined)
588
583
  touched.add(expectationKey(aff));
package/dist/migrate.js CHANGED
@@ -41,6 +41,11 @@ function renameAmbient(json) {
41
41
  }
42
42
  /** The chain, keyed by the version each step upgrades from. */
43
43
  export const MIGRATIONS = {
44
+ '2.17.0': {
45
+ to: '2.21.0',
46
+ note: 'doze and bladder state start absent; old callers retain their continuation',
47
+ apply: stamp,
48
+ },
44
49
  '1.4.0': {
45
50
  to: '1.5.0',
46
51
  note: 'person shape unchanged; standing advice is heard for the running activity from now on',
@@ -23,6 +23,7 @@ export const EN_LINES = {
23
23
  ],
24
24
  'need:rest': ['I need to sit down for a while.', 'A rest first, then the rest.'],
25
25
  'need:relief': ['This pain needs seeing to.', "I can't think past the pain."],
26
+ 'need:bladder': ['I need to use the toilet.'],
26
27
  'need:belonging': ["I've been alone too long. I want company.", 'Let me find someone to talk to.'],
27
28
  'need:leisure': ['I need a break from all this.', 'Time for something easier.'],
28
29
  'need:meaning': ['I want to do something that matters.', 'Something worthwhile, for once.'],
@@ -102,6 +103,7 @@ export const EN_LINES = {
102
103
  'intention:sleep': ['to sleep'],
103
104
  'intention:rest': ['to rest'],
104
105
  'intention:relief': ['to ease the pain'],
106
+ 'intention:bladder': ['to use the toilet'],
105
107
  'intention:belonging': ['to be with people'],
106
108
  'intention:leisure': ['to enjoy myself'],
107
109
  'intention:meaning': ['to do something that matters'],
@@ -22,6 +22,7 @@ export const PLAIN_EN_LINES = {
22
22
  'I need to sleep before I do anything else.',
23
23
  ],
24
24
  'need:rest': ['I need to sit down for a while.', 'I will rest before I do anything else.'],
25
+ 'need:bladder': ['I need to use the toilet.'],
25
26
  'need:relief': [
26
27
  'I need to do something about this pain.',
27
28
  'The pain makes it hard to think about anything else.',
@@ -116,6 +117,7 @@ export const PLAIN_EN_LINES = {
116
117
  'intention:water': ['to drink'],
117
118
  'intention:sleep': ['to sleep'],
118
119
  'intention:rest': ['to rest'],
120
+ 'intention:bladder': ['to use the toilet'],
119
121
  'intention:relief': ['to ease the pain'],
120
122
  'intention:belonging': ['to be with people'],
121
123
  'intention:leisure': ['to enjoy myself'],
@@ -0,0 +1,4 @@
1
+ import { type TraitEffect } from './social/social.ts';
2
+ import type { EntityId, Minute, Person } from './types.ts';
3
+ /** Returns the same detached, sorted misled-source ids as confirm; no relationship alias escapes. */
4
+ export declare function confirmObservedReputation(p: Person, prop: string, truth: boolean, now: Minute, table?: Record<string, TraitEffect>): EntityId[];
@@ -0,0 +1,16 @@
1
+ /**
2
+ * SCOPE: opt-in composition of beliefs.confirm and social.applyReputationBelief, in that order.
3
+ * Owns no state or formula. This is the Hearsay 2 wrapper: direct observation corrects the belief,
4
+ * recalibrates its sources and applies the credence change to the subject's relationship. It does
5
+ * not infer opposite traits, accuse a source of lying, run attention or record a percept. Hosts
6
+ * supply only observations their person has access to. Existing callers remain unchanged.
7
+ */
8
+ import { confirm, credence } from './beliefs/beliefs.js';
9
+ import { applyReputationBelief } from './social/social.js';
10
+ /** Returns the same detached, sorted misled-source ids as confirm; no relationship alias escapes. */
11
+ export function confirmObservedReputation(p, prop, truth, now, table) {
12
+ const before = credence(p, prop);
13
+ const misled = confirm(p, prop, truth, now);
14
+ applyReputationBelief(p, prop, before, credence(p, prop), now, table);
15
+ return misled;
16
+ }
package/dist/person.d.ts CHANGED
@@ -50,6 +50,11 @@ export declare const PERSON_DEFAULTS: {
50
50
  nightFrom: number;
51
51
  nightTo: number;
52
52
  };
53
+ /**
54
+ * SCOPE (host setup): optional habits and expectations copy authored records, including nested cues and
55
+ * need deltas, in source order. Invalid numbers, domains or collection bounds reject with RangeError;
56
+ * setup never clamps, evicts, learns, advances time or consumes RNG. Omitted options keep legacy state.
57
+ */
53
58
  export declare function createPerson(spec: PersonSpec): Person;
54
59
  export interface PersonReadout {
55
60
  body: BodyReadout;
package/dist/person.js CHANGED
@@ -22,18 +22,20 @@
22
22
  import { actionTendencies, advanceAffect, appraise, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, feel, readAffect, regulate, release, skipAffect, skipCrisis, strain, tendencyEmotions, } from './affect/index.js';
23
23
  import { advanceAgenda, agendaTermsWithGoalEstimates, createAgenda, onFinished, promise, proposeGoals, revisePurposes, } from './agenda/index.js';
24
24
  import { advanceBeliefs, applyContradiction, attend, believe, confirm, credence, noteProvenance, trustOf, } from './beliefs/index.js';
25
+ import { enableBladder, voidBladder } from './body/bladder.js';
26
+ import { checkDoze, DOZE_DEFAULTS, enableDoze, enterVoluntarySleep, wakeDoze } from './body/doze.js';
25
27
  import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, enableFatigueStore, enableFullness, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
26
28
  import { ageCharacter, enableCharacterChange, noteCharacterDay, noteCharacterSocial, } from './character/index.js';
27
29
  import { closeDay, enableYearbook, endDay, noteAnswer, noteCommitments, noteDecision, noteMood, noteOutcome, trimChronicle, } from './chronicle/index.js';
28
30
  import { decide as cognitionDecide, desperationOf, prospectiveRoute, scoreAll } from './cognition/index.js';
29
31
  import { createConscience, guiltLoad, heldNorms, normVeto, recordDeed, recordRepair, repent, } from './conscience/index.js';
30
- import { clamp01, clampSigned, createRng, dayOf, isObj, minuteOfDay, round } from './core/index.js';
32
+ import { clamp01, clampSigned, createRng, dayOf, isObj, keysOf, minuteOfDay, round } from './core/index.js';
31
33
  import { enableCuriosity, noteCuriosity } from './curiosity/index.js';
32
34
  import { ambientBodyParams, ambientModifiers, ambientMood, ambientNeeds } from './environment/index.js';
33
35
  import { aptitudeOf, createFamily, pregnancyModifiers } from './family/index.js';
34
- import { advanceHabits, reinforce, withholdCued } from './habits/index.js';
36
+ import { advanceHabits, HABIT_DEFAULTS, reinforce, withholdCued } from './habits/index.js';
35
37
  import { ageYears, learningMultiplier, lifeModifiers } from './lifecourse/index.js';
36
- import { advanceMemory, createMemory, enableGists, foldGists, learnOutcome, recallByCue, remember, } from './memory/index.js';
38
+ import { advanceMemory, createMemory, enableGists, foldGists, learnOutcome, MEMORY_DEFAULTS, recallByCue, remember, } from './memory/index.js';
37
39
  import { intentionFor, narrateDecision, voiceLine } from './narrate/index.js';
38
40
  import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisfy, urgencyOf, } from './needs/index.js';
39
41
  import { widow } from './partnering/index.js';
@@ -114,7 +116,56 @@ function fill(keys, given, def) {
114
116
  }
115
117
  return out;
116
118
  }
119
+ function validateHostSetup(spec) {
120
+ const finite = (n) => Number.isFinite(n);
121
+ const unit = (n) => finite(n) && n >= 0 && n <= 1;
122
+ const count = (n) => Number.isSafeInteger(n) && n >= 0;
123
+ const optional = (n, valid) => n === undefined || valid(n);
124
+ if (spec.habits !== undefined) {
125
+ if (!Array.isArray(spec.habits) || spec.habits.length > HABIT_DEFAULTS.maxHabits)
126
+ throw new RangeError('Invalid habits setup bound');
127
+ for (const h of spec.habits) {
128
+ if (!h ||
129
+ !isObj(h.cue) ||
130
+ typeof h.action !== 'string' ||
131
+ h.action.length === 0 ||
132
+ !unit(h.strength) ||
133
+ !count(h.repetitions) ||
134
+ !finite(h.lastAt) ||
135
+ !optional(h.withheld, count) ||
136
+ !optional(h.lastWithheldAt, finite) ||
137
+ !optional(h.craving, unit) ||
138
+ !optional(h.cue.hour, (n) => finite(n) && n >= 0 && n < 24) ||
139
+ (h.cue.placeId !== undefined && typeof h.cue.placeId !== 'string') ||
140
+ (h.cue.after !== undefined && typeof h.cue.after !== 'string'))
141
+ throw new RangeError('Invalid habit setup record');
142
+ }
143
+ }
144
+ if (spec.expectations !== undefined) {
145
+ if (!Array.isArray(spec.expectations) || spec.expectations.length > MEMORY_DEFAULTS.maxExpectations)
146
+ throw new RangeError('Invalid expectations setup bound');
147
+ for (const e of spec.expectations) {
148
+ if (!e ||
149
+ typeof e.key !== 'string' ||
150
+ e.key.length === 0 ||
151
+ !isObj(e.needs) ||
152
+ !unit(e.successRate) ||
153
+ !count(e.samples) ||
154
+ !finite(e.valence) ||
155
+ Math.abs(e.valence) > 1 ||
156
+ Object.entries(e.needs).some(([key, n]) => ![...PHYSIOLOGICAL_NEEDS, ...PSYCHOLOGICAL_NEEDS].some((need) => need === key) ||
157
+ (n !== undefined && !finite(n))))
158
+ throw new RangeError('Invalid expectation setup record');
159
+ }
160
+ }
161
+ }
162
+ /**
163
+ * SCOPE (host setup): optional habits and expectations copy authored records, including nested cues and
164
+ * need deltas, in source order. Invalid numbers, domains or collection bounds reject with RangeError;
165
+ * setup never clamps, evicts, learns, advances time or consumes RNG. Omitted options keep legacy state.
166
+ */
117
167
  export function createPerson(spec) {
168
+ validateHostSetup(spec);
118
169
  const now = spec.now ?? 0;
119
170
  const traits = fill(TRAIT_KEYS, spec.traits, 0.5);
120
171
  const values = fill(VALUE_KEYS, spec.values, 0.5);
@@ -143,6 +194,38 @@ export function createPerson(spec) {
143
194
  trace: [],
144
195
  nextDecision: 0,
145
196
  };
197
+ if (spec.habits)
198
+ p.habits = spec.habits.map((h) => {
199
+ const cue = {};
200
+ if (h.cue.hour !== undefined)
201
+ cue.hour = h.cue.hour;
202
+ if (h.cue.placeId !== undefined)
203
+ cue.placeId = h.cue.placeId;
204
+ if (h.cue.after !== undefined)
205
+ cue.after = h.cue.after;
206
+ const out = {
207
+ cue,
208
+ action: h.action,
209
+ strength: h.strength,
210
+ repetitions: h.repetitions,
211
+ lastAt: h.lastAt,
212
+ };
213
+ if (h.craving !== undefined)
214
+ out.craving = h.craving;
215
+ if (h.withheld !== undefined)
216
+ out.withheld = h.withheld;
217
+ if (h.lastWithheldAt !== undefined)
218
+ out.lastWithheldAt = h.lastWithheldAt;
219
+ return out;
220
+ });
221
+ if (spec.expectations)
222
+ p.memory.expectations = spec.expectations.map((e) => ({
223
+ key: e.key,
224
+ needs: Object.fromEntries(Object.entries(e.needs)),
225
+ successRate: e.successRate,
226
+ samples: e.samples,
227
+ valence: e.valence,
228
+ }));
146
229
  if (spec.lexicon)
147
230
  p.lexicon = structuredClone(spec.lexicon);
148
231
  if (spec.retention)
@@ -177,6 +260,10 @@ export function createPerson(spec) {
177
260
  enablePromptings(p, on.promptings);
178
261
  if (on?.review)
179
262
  enableReview(p, on.review);
263
+ if (on?.doze)
264
+ enableDoze(p, on.doze);
265
+ if (on?.bladder)
266
+ enableBladder(p);
180
267
  return p;
181
268
  }
182
269
  /**
@@ -392,10 +479,10 @@ export function tick(p, now) {
392
479
  const seg = PERSON_DEFAULTS.maxSegment;
393
480
  while (p.now < now) {
394
481
  const act = p.activity;
395
- const active = act !== null && p.now < act.endsAt;
482
+ const active = act !== null && act.dozedAt === undefined && p.body.doze?.since === undefined && p.now < act.endsAt;
396
483
  // Segment boundaries sit on an absolute grid, so how a host chunks its calls does not change the result.
397
484
  const grid = (Math.floor(p.now / seg) + 1) * seg;
398
- const end = Math.min(active ? Math.min(now, act.endsAt) : now, grid);
485
+ const end = Math.min(active ? Math.min(now, act.endsAt) : now, grid, p.body.doze ? Math.floor(p.now) + 1 : Number.POSITIVE_INFINITY);
399
486
  const load = active && act ? { effort: act.effort, focus: act.focus, mode: act.mode } : PERSON_DEFAULTS.idleLoad;
400
487
  // A minute so large that the grid step rounds away cannot advance (security review H2): stop, never spin.
401
488
  if (!(end > p.now)) {
@@ -407,6 +494,12 @@ export function tick(p, now) {
407
494
  }
408
495
  advanceSegment(p, end - p.now, load, active && act ? act.affordance : undefined);
409
496
  p.now = end;
497
+ if (Number.isInteger(end) && p.body.doze && !(act && act.endsAt === end) && checkDoze(p, end)) {
498
+ if (act)
499
+ act.dozedAt ??= end;
500
+ // Physical onset must not erase a host/percept reason awaiting resolution.
501
+ interrupt(p, end, act?.interrupt?.reason ?? 'doze');
502
+ }
410
503
  pruneConsequences(p);
411
504
  if (p.pursuit)
412
505
  releasePursuit(p);
@@ -771,6 +864,18 @@ function judgeDeed(p, actor, pc, at) {
771
864
  export function perceive(p, percepts, options = {}) {
772
865
  if (percepts.length === 0 || !p.body.alive)
773
866
  return [];
867
+ if (p.body.doze?.since !== undefined) {
868
+ const wakes = percepts.some((pc) => Number.isFinite(pc.at) &&
869
+ pc.at === p.now &&
870
+ Number.isFinite(pc.salience) &&
871
+ pc.salience >= DOZE_DEFAULTS.wakeSalience &&
872
+ pc.near !== false &&
873
+ (pc.targetId === p.id || pc.near === true) &&
874
+ (pc.channel === 'heard' || pc.channel === 'felt' || pc.channel === 'social'));
875
+ if (!wakes)
876
+ return [];
877
+ wakeDoze(p, p.now);
878
+ }
774
879
  const body = readBodyOf(p);
775
880
  const attended = attend(p, [...percepts], {
776
881
  focus: p.activity?.focus ?? 0,
@@ -1068,6 +1173,8 @@ function thresholdFor(p, load, review) {
1068
1173
  export function begin(p, aff, record, opts = {}) {
1069
1174
  if (!p.body.alive)
1070
1175
  return null;
1176
+ if (p.body.doze?.since !== undefined && aff.mode !== 'sleep')
1177
+ return null;
1071
1178
  const now = p.now;
1072
1179
  if (p.pursuit) {
1073
1180
  const considered = record.considered.find((c) => c.affordanceId === aff.id);
@@ -1143,6 +1250,8 @@ export function begin(p, aff, record, opts = {}) {
1143
1250
  activity.commitmentId = c.id;
1144
1251
  }
1145
1252
  p.activity = activity;
1253
+ if (load.mode === 'sleep')
1254
+ enterVoluntarySleep(p);
1146
1255
  advanceBody(p, 0, load, mods); // apply the sleep/wake switch now
1147
1256
  const review = load.mode === 'sleep' ? PERSON_DEFAULTS.sleepReviewInterval : PERSON_DEFAULTS.reviewInterval;
1148
1257
  const thresholdAt = thresholdFor(p, load, review);
@@ -1367,7 +1476,7 @@ export function finish(p, outcome, opts = {}) {
1367
1476
  const now = p.now;
1368
1477
  const aff = act.affordance;
1369
1478
  const completed = outcome.status === 'completed';
1370
- const minutes = Math.max(0, now - act.startedAt);
1479
+ const minutes = Math.max(0, Math.min(now, act.dozedAt ?? now) - act.startedAt);
1371
1480
  const mods = lifeModifiers(p);
1372
1481
  const D = PERSON_DEFAULTS;
1373
1482
  const before = readNeeds(p, readBodyOf(p));
@@ -1380,6 +1489,8 @@ export function finish(p, outcome, opts = {}) {
1380
1489
  if (given.relief)
1381
1490
  bodyDeltas.relief = given.relief;
1382
1491
  consume(p, bodyDeltas);
1492
+ if (completed && outcome.voidedBladder === true)
1493
+ voidBladder(p);
1383
1494
  const psych = {};
1384
1495
  for (const id of PSYCHOLOGICAL_NEEDS)
1385
1496
  if (given[id] !== undefined)
@@ -1399,7 +1510,7 @@ export function finish(p, outcome, opts = {}) {
1399
1510
  const realized = realizedNeedDeltas(act, given, before, after);
1400
1511
  // Felt valence.
1401
1512
  let felt = 0;
1402
- for (const id of Object.keys(realized)) {
1513
+ for (const id of keysOf(realized)) {
1403
1514
  felt += D.feltGain * (D.feltBase + urgencyOf(before, id)) * (realized[id] ?? 0);
1404
1515
  }
1405
1516
  if (outcome.status === 'failed')
package/dist/persona.js CHANGED
@@ -4,8 +4,8 @@
4
4
  * identity, age, family and health compatibility before returning a new Person. Neither input is mutated.
5
5
  * This is not a world migration, identity registry, transport, hash, authentication or general calendar mapper.
6
6
  * v1 admits zero-duration travel and healthy compatible bodies only; family/bonds remain host-authored and must
7
- * match source provenance. Portable v1 accepts its published 2.14.1 stamp and the current engine;
8
- * the 2.14.1 to 2.17.0 migration changes no portable field semantics. Pregnancy and nonempty temporal bonds are unsupported in v1. All other world-dependent stores stay with the source and are disclosed as omitted.
7
+ * match source provenance. Portable v1 accepts its published 2.14.1 and 2.17.0 stamps and the current engine;
8
+ * the bodily opt-ins change no portable field semantics. Pregnancy and nonempty temporal bonds are unsupported in v1. All other world-dependent stores stay with the source and are disclosed as omitted.
9
9
  */
10
10
  import { hasUnresolvedMakeUps, sameChain } from './agenda/agenda.js';
11
11
  import { createPerson } from './person.js';
@@ -186,8 +186,10 @@ export function validatePersonaProjection(input) {
186
186
  text(input.mappingVersion);
187
187
  text(input.name);
188
188
  keys(input.source, ['schema', 'engine', 'id', 'now']);
189
- // Both published passport releases used 2.14.1; the next engine upgrade is stamp-only here.
190
- const compatibleEngine = input.source.engine === ENGINE_VERSION || input.source.engine === '2.14.1';
189
+ // The 2.14.1 and 2.17.0 portable fields retain their semantics across the bodily opt-ins.
190
+ const compatibleEngine = input.source.engine === ENGINE_VERSION ||
191
+ input.source.engine === '2.17.0' ||
192
+ input.source.engine === '2.14.1';
191
193
  if (input.source.schema !== PERSON_SCHEMA || !compatibleEngine)
192
194
  fail('unsupported source schema/engine; migrate the source before projection');
193
195
  text(input.source.id);
package/dist/qalb.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * SCOPE: a read-only narration lens over retained attention evidence, held norms and conviction,
3
+ * mood and the three distinct trust stores. Owns no state and supplies no decision input. It does
4
+ * not infer spiritual health, rank people, judge norms, measure the ruh or compute divine acceptance.
5
+ * Retained memories are evidence available for narration, not a complete attention history.
6
+ *
7
+ * Provenance: research/reconciliation.md §2–4, §7 (Q7: Qur'an 22:46; H9: Bukhari 52 /
8
+ * Muslim 1599a); research/islamic-foundations.md §2 (qalb as an overlapping interpretive view);
9
+ * research/decisions.md, “Qalb, nafs, fitra, ʿaql as state”. These support the lens, not a
10
+ * diagnostic mapping. Sign/zero categories below are engineering descriptions of stored values,
11
+ * not religious categories. Mood and interpersonal trust imply nothing about faith.
12
+ */
13
+ import type { Person } from './types.ts';
14
+ export interface QalbDescription {
15
+ facet: 'attention' | 'held-norm' | 'mood' | 'trust';
16
+ category: string;
17
+ /** Every description is attributed to this simulated person's records. */
18
+ personId: string;
19
+ text: string;
20
+ /** Detached identifiers and state paths, never mutable state objects or spiritual scores. */
21
+ evidence: string[];
22
+ sources: string[];
23
+ }
24
+ /**
25
+ * Plain English descriptions; no scalar summary, comparison or hidden update.
26
+ * Hosts must not present these records as spiritual health or a religious assessment.
27
+ */
28
+ export declare function qalbReading(p: Person): QalbDescription[];
package/dist/qalb.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Plain English descriptions; no scalar summary, comparison or hidden update.
3
+ * Hosts must not present these records as spiritual health or a religious assessment.
4
+ */
5
+ export function qalbReading(p) {
6
+ const sources = [
7
+ 'research/reconciliation.md §2–4, §7; Q7 (22:46); H9 (Bukhari 52; Muslim 1599a)',
8
+ 'research/islamic-foundations.md §2: Qalb',
9
+ 'research/decisions.md: Qalb, nafs, fitra, ʿaql as state (2026-10-18)',
10
+ ];
11
+ const rows = [];
12
+ const add = (facet, category, text, evidence) => {
13
+ rows.push({ facet, category, personId: p.id, text, evidence, sources: [...sources] });
14
+ };
15
+ const memories = [...p.memory.episodes, ...(p.memory.gists ?? [])];
16
+ add('attention', memories.length ? 'retained-evidence' : 'no-retained-evidence', memories.length
17
+ ? 'Their memories retain some experiences. These do not show everything they noticed.'
18
+ : 'Their records contain no retained experiences. This does not show what they noticed.', memories.map((e) => `memory:${e.id}:${e.kind}`));
19
+ if (!p.conscience.norms.length)
20
+ add('held-norm', 'unrecorded', 'Their records contain no held rules.', ['conscience.norms']);
21
+ for (const n of p.conscience.norms) {
22
+ add('held-norm', n.conviction === 0 ? 'no-conviction' : 'held-understanding', n.conviction === 0
23
+ ? 'They have a recorded rule with no stated conviction.'
24
+ : 'They hold an understanding of this rule. This is their view.', [`norm:${n.normId}`, `standing:${n.standing}`, 'conscience.norms.conviction']);
25
+ }
26
+ add('mood', p.affect.mood.valence < 0 ? 'unpleasant' : p.affect.mood.valence > 0 ? 'pleasant' : 'neutral', p.affect.mood.valence < 0
27
+ ? 'Their recorded mood feels unpleasant.'
28
+ : p.affect.mood.valence > 0
29
+ ? 'Their recorded mood feels pleasant.'
30
+ : 'Their recorded mood feels neutral.', ['affect.mood.valence']);
31
+ add('mood', p.affect.mood.arousal === 0 ? 'no-arousal' : 'some-arousal', p.affect.mood.arousal === 0
32
+ ? 'Their recorded mood has no arousal.'
33
+ : 'Their recorded mood has some arousal.', ['affect.mood.arousal']);
34
+ // Keep stores distinct. Do not substitute relationship trust for source or advising-voice trust.
35
+ const ties = p.social.relationships.map((r) => ({
36
+ store: 'social.relationships',
37
+ id: r.otherId,
38
+ trust: r.trust,
39
+ }));
40
+ const voices = p.will.voices.map((v) => ({ store: 'will.voices', id: v.voiceId, trust: v.trust }));
41
+ const sourcesTrust = Object.entries(p.memory.sourceTrust).map(([id, trust]) => ({
42
+ store: 'memory.sourceTrust',
43
+ id,
44
+ trust,
45
+ }));
46
+ const trusts = [...ties, ...voices, ...sourcesTrust];
47
+ if (!trusts.length)
48
+ add('trust', 'unrecorded', 'Their records contain no learned trust.', [
49
+ 'social.relationships',
50
+ 'will.voices',
51
+ 'memory.sourceTrust',
52
+ ]);
53
+ for (const t of trusts) {
54
+ // 0.5 is the numeric range midpoint, not a faith threshold or personal baseline.
55
+ const subject = t.store === 'social.relationships'
56
+ ? 'this person'
57
+ : t.store === 'will.voices'
58
+ ? 'this advising voice'
59
+ : 'this information source';
60
+ add('trust', t.trust < 0.5 ? 'below-midpoint' : t.trust > 0.5 ? 'above-midpoint' : 'at-midpoint', t.trust < 0.5
61
+ ? `Their trust in ${subject} is below the middle of its range.`
62
+ : t.trust > 0.5
63
+ ? `Their trust in ${subject} is above the middle of its range.`
64
+ : `Their trust in ${subject} is at the middle of its range.`, [`${t.store}:${t.id}`]);
65
+ }
66
+ return rows;
67
+ }
package/dist/sim/sim.d.ts CHANGED
@@ -245,7 +245,7 @@ export declare function birth(c: Community, a: Person, b: Person, spec: ChildSpe
245
245
  export declare function tellDeath(c: Community, dead: Person, at: Minute, opts?: {
246
246
  late?: 'throw' | 'now';
247
247
  }): void;
248
- /** Next minute at which this person needs attention. */
248
+ /** Pure scheduling query, including passive bodily checkpoints during joint waiting. */
249
249
  export declare function nextEventAt(c: Community, p: Person): Minute;
250
250
  /**
251
251
  * How this person would answer `suggestion` now, without changing state (UI telegraph; wraps `predict`). Pass the