@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/README.md CHANGED
@@ -222,7 +222,7 @@ reference host: `createVillage`.
222
222
 
223
223
  There are two version numbers, and they move independently.
224
224
 
225
- - **The release version** is the package version (`package.json`, now `2.14.1`) and `FRAMEWORK_VERSION`, the same
225
+ - **The release version** is the package version (`package.json`, currently `2.21.0`) and `FRAMEWORK_VERSION`, the same
226
226
  string compiled into the build; a test keeps them equal. It versions the public API under
227
227
  [semver](https://semver.org/): a breaking change to an exported name or signature bumps the major, a new
228
228
  faculty or export the minor, a fix the patch. Each release is a tag `vX.Y.Z` and a GitHub release whose notes
@@ -236,6 +236,16 @@ There are two version numbers, and they move independently.
236
236
  The root `package.json` version is the site's version (stamped into the site's `release.json`) and is not part of
237
237
  this policy.
238
238
 
239
+ ## Optional felt body (2.21 source candidate)
240
+
241
+ `PersonSpec.enable.doze` accepts `{ posture: 'upright' | 'seated' | 'reclined', stimulation: number }`; `enableDoze` and `setDozeContext` are the explicit host calls. Doze onset is checked at integer-minute boundaries while awake. `readDoze` reports the current hazard; `nextDozeCheck` tells a manual host when to tick next. The opt-in costs one check per elapsed minute, including when onset gates are closed. It changes integration granularity, so enabled runs require a new replay pin. A dozed activity records `dozedAt`; the host resolves interrupted work. Doze does not wake spontaneously. `perceive` wakes only for a percept delivered at `p.now`, with `salience >= 0.8`, a heard/felt/social channel, detectable proximity (`near !== false`) and either `targetId === p.id` or `near === true`. The host must supply those alarms. These hazard and salience values are engineering constants.
242
+
243
+ `estimateQuantity(p, { truth, difficulty })` is an explicit observation, not a display reader. Finite nonnegative truth and difficulty in `[0,1]` are required. A nonzero request consumes one draw from the person's saved seeded RNG; zero truth or zero difficulty returns truth with confidence 1 and consumes none. The result does not store a belief. Objective sleepiness widens its uniform error; perceived sleepiness determines confidence. Coefficients are engineering choices, not calibrated uncertainty intervals.
244
+
245
+ `PersonSpec.enable.bladder` or `enableBladder(p)` starts a normalized fill store. `readBladder` returns true and perceived urgency, equal in this first slice. Water intake adds delayed fill and a small base rate fills it over time. A host marks a toilet `Affordance` with `voidsBladder: true` to add urgency-based choice value. Actual successful urination is separate: a completed `Outcome` with `voidedBladder: true` empties it; `voidBladder(p)` is the direct host operation. An interrupted or failed outcome does not empty it. For an offer that declares voiding, expectation learning treats completion without actual voiding as failure of that purpose. Do not use `needs.relief` as a substitute: that field reduces pain. Filling rates, threshold and choice weight are engineering constants. The model has no continence, automatic toilet search or automatic waking.
246
+
247
+ With none of these options enabled or calls made, person state and seeded RNG continuation match HF 2.20.0 after normalizing the engine stamp only. HF 2.21.0 is library source here; the three site games still declare HF 2.20.0. Watch's adoption of `gestureVoiceTrust` and `addTiredness` uses the 2.20.0 helpers.
248
+
239
249
  ## Long runs (2.0)
240
250
 
241
251
  HF 2.0 adds what a life over decades needs. Each faculty is opt-in or absent until used, so a host that uses none
@@ -529,3 +539,14 @@ wording, the generic fact sentence still includes that id so the claim is not hi
529
539
  Stock village outcomes, birth/marriage memories, routine fallbacks and generated
530
540
  community log details use the same per-person option. World-supplied outcome
531
541
  summaries and routine summaries remain exact.
542
+
543
+
544
+ ### Authored setup and explicit host effects
545
+
546
+ Hosts may pass `habits: Habit[]` and `expectations: ActionExpectation[]` to `createPerson`. The framework copies records and their nested objects in order. Invalid or over-bound setup throws `RangeError`; omission preserves prior state. Duplicates remain authored records, with the existing matcher semantics.
547
+
548
+ Use `gestureVoiceTrust(p, voiceId, amount, { reason, at?, action? })` for an explicit signed gesture toward an existing advising voice. It records the realized delta in bounded history and returns `undefined` for a missing voice. Reasons must be nonempty strings; optional action must be a string. Use `addTiredness(p, { sleepPressure?, exertion? })` for positive host-caused costs. Neither advances time or draws randomness. Host magnitudes are engineering premises.
549
+
550
+ `confirmObservedReputation(p, prop, truth, now, table?)` replaces the manual `credence → confirm → applyReputationBelief` sequence. Supply only a fact the person could observe. It does not run attention or invent a moral judgement.
551
+
552
+ `qalbReading(p)` is a read-only, sourced narration lens over existing records. Its detached categorical rows are this person's recorded understanding and evidence. They are never a spiritual health score, a ruling, a rank, or a decision input. Do not present mood, low social trust or missing memories as evidence of poor faith.
@@ -0,0 +1,21 @@
1
+ import type { Affordance, Person } from '../types.ts';
2
+ export declare const BLADDER_DEFAULTS: Readonly<{
3
+ intakeScale: 0.5;
4
+ maxPending: 4;
5
+ halfLife: 45;
6
+ basePerMinute: 0.001;
7
+ /** Frozen engineering default, not a clinical threshold or fitted choice result. */
8
+ choiceThreshold: 0.35;
9
+ /** Frozen engineering weight on perceived urgency above the threshold. */
10
+ choiceScale: 4;
11
+ }>;
12
+ export declare function enableBladder(p: Person): void;
13
+ /** Pure host readout; equal perceived urgency is a first-slice assumption. */
14
+ export declare function readBladder(p: Person): {
15
+ urgency: number;
16
+ perceived: number;
17
+ } | undefined;
18
+ /** Pure opt-in choice contribution; a declared offer says nothing about actual success. */
19
+ export declare function bladderMotivation(p: Person, offer: Affordance, believedSuccess: number): number;
20
+ /** Host calls only after successful urination. Pending intake remains. */
21
+ export declare function voidBladder(p: Person): boolean;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * SCOPE: opt-in normalized storage and delayed water intake; hosts resolve actual voiding.
3
+ * NIDDK, The Urinary Tract & How It Works supports production/storage/urination, not these
4
+ * engineering rates. Base filling is independent of hydration, including dehydration.
5
+ * Urgency and perceived urgency are equal in this slice; hosts supply toilet choices. It models no renal calibration, continence, injury, search for a toilet or automatic waking.
6
+ */
7
+ import { clamp01, dexp, isNum, isObj } from '../core/index.js';
8
+ export const BLADDER_DEFAULTS = Object.freeze({
9
+ intakeScale: 0.5,
10
+ maxPending: 4,
11
+ halfLife: 45,
12
+ basePerMinute: 0.001,
13
+ /** Frozen engineering default, not a clinical threshold or fitted choice result. */
14
+ choiceThreshold: 0.35,
15
+ /** Frozen engineering weight on perceived urgency above the threshold. */
16
+ choiceScale: 4,
17
+ });
18
+ export function enableBladder(p) {
19
+ p.body.bladder ??= { fill: 0, pending: 0 };
20
+ }
21
+ /** Pure host readout; equal perceived urgency is a first-slice assumption. */
22
+ export function readBladder(p) {
23
+ const b = p.body.bladder;
24
+ return b ? { urgency: b.fill, perceived: b.fill } : undefined;
25
+ }
26
+ /** Pure opt-in choice contribution; a declared offer says nothing about actual success. */
27
+ export function bladderMotivation(p, offer, believedSuccess) {
28
+ if (offer.voidsBladder !== true)
29
+ return 0;
30
+ const perceived = readBladder(p)?.perceived;
31
+ if (perceived === undefined)
32
+ return 0;
33
+ return (BLADDER_DEFAULTS.choiceScale *
34
+ clamp01((perceived - BLADDER_DEFAULTS.choiceThreshold) / (1 - BLADDER_DEFAULTS.choiceThreshold)) *
35
+ clamp01(believedSuccess));
36
+ }
37
+ /** Host calls only after successful urination. Pending intake remains. */
38
+ export function voidBladder(p) {
39
+ if (!p.body.bladder || !p.body.alive || p.body.asleep)
40
+ return false;
41
+ p.body.bladder.fill = 0;
42
+ return true;
43
+ }
44
+ /** @internal */
45
+ export function intakeBladder(p, water) {
46
+ const b = p.body.bladder;
47
+ if (b && isNum(water) && water > 0)
48
+ b.pending = Math.min(BLADDER_DEFAULTS.maxPending, b.pending + BLADDER_DEFAULTS.intakeScale * clamp01(water));
49
+ }
50
+ /** @internal */
51
+ export function advanceBladder(b, dt) {
52
+ const remaining = b.pending * dexp((-Math.LN2 * dt) / BLADDER_DEFAULTS.halfLife);
53
+ b.fill = clamp01(b.fill + BLADDER_DEFAULTS.basePerMinute * dt + b.pending - remaining);
54
+ b.pending = remaining;
55
+ }
56
+ /** @internal */
57
+ export function sanitizeBladder(raw) {
58
+ if (!isObj(raw) || !isNum(raw.fill) || !isNum(raw.pending))
59
+ return undefined;
60
+ return {
61
+ fill: clamp01(raw.fill),
62
+ pending: Math.min(BLADDER_DEFAULTS.maxPending, Math.max(0, raw.pending)),
63
+ };
64
+ }
@@ -154,6 +154,16 @@ export declare function fastingDamping(b: Pick<BodyState, 'satiety' | 'hydration
154
154
  * fast: perceived hunger is damped (see SCOPE, N2b); true values, capacity and health are unchanged.
155
155
  */
156
156
  export declare function readBody(p: Person, params?: BodyParams, ctx?: BodyPerceptionContext): BodyReadout;
157
+ /**
158
+ * SCOPE (host-caused tiredness): add nonnegative sleep-pressure and exertion costs, clamped to 0..1.
159
+ * The host supplies magnitudes; these are not calibrated claims. Absent, negative and non-finite costs
160
+ * do nothing. A dead body is unchanged, as with consume. Writes only these two body fields: no ticking,
161
+ * sleep transition, depletion, memory, RNG or other effects. Recovery remains with the existing body APIs.
162
+ */
163
+ export declare function addTiredness(p: Person, costs: {
164
+ sleepPressure?: number;
165
+ exertion?: number;
166
+ }): void;
157
167
  /** Eat, drink or take pain relief. `relief` lowers current pain; it then relaxes back toward its sources. */
158
168
  export declare function consume(p: Person, deltas: {
159
169
  food?: number;
package/dist/body/body.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * and is under-perceived (cumulative-cost-of-wakefulness pattern, Van Dongen et al. 2003). Satiety and
9
9
  * hydration fall linearly with metabolic rate and effort. Fitness relaxes exponentially toward a training
10
10
  * target or a detraining floor. All rates are engineering defaults; none is a calibrated physiological
11
- * prediction. Not modelled: bladder, temperature, specific nutrients, endocrine and immune detail,
11
+ * prediction. Not modelled: temperature, specific nutrients, endocrine and immune detail,
12
12
  * stress physiology, and masking of hunger or pain by emotion or focus (that belongs to other faculties).
13
13
  * `advanceBody` integrates in deterministic sub-steps of at most 15 minutes, using the exact solution
14
14
  * within each sub-step where the dynamics are linear, so one long step matches many short ones closely.
@@ -44,6 +44,8 @@
44
44
  */
45
45
  import { chance, clamp, clamp01, dcos, decay, dexp, dlog, hasNumbers, isNum, isObj, minuteOfDay, smoothstep, } from '../core/index.js';
46
46
  import { MINUTES_PER_DAY, MINUTES_PER_YEAR } from '../types.js';
47
+ import { advanceBladder, intakeBladder, sanitizeBladder } from './bladder.js';
48
+ import { sanitizeDoze } from './doze.js';
47
49
  import { bleedStep, INJURY_DEFAULTS } from './injury.js';
48
50
  export const BODY_DEFAULTS = {
49
51
  maxSubStep: 15,
@@ -218,6 +220,20 @@ export function sanitizeExposures(input, now) {
218
220
  * downing are checked by `sanitizeInjuries`. A save the engine wrote is unchanged. @internal
219
221
  */
220
222
  export function sanitizeBody(b, now) {
223
+ if (b.doze !== undefined) {
224
+ const d = sanitizeDoze(b.doze, b, now);
225
+ if (d)
226
+ b.doze = d;
227
+ else
228
+ delete b.doze;
229
+ }
230
+ if (b.bladder !== undefined) {
231
+ const bladder = sanitizeBladder(b.bladder);
232
+ if (bladder)
233
+ b.bladder = bladder;
234
+ else
235
+ delete b.bladder;
236
+ }
221
237
  if (b.rates !== undefined) {
222
238
  const rates = sanitizeRates(b.rates);
223
239
  if (rates)
@@ -304,6 +320,8 @@ export function createBody(init, now) {
304
320
  illnesses: [],
305
321
  nextId: 0,
306
322
  };
323
+ delete b.doze;
324
+ delete b.bladder;
307
325
  b.satiety = clamp01(b.satiety);
308
326
  b.hydration = clamp01(b.hydration);
309
327
  b.sleepPressure = clamp01(b.sleepPressure);
@@ -486,7 +504,7 @@ export function advanceBody(p, dt, load, mods, params = BODY_DEFAULTS) {
486
504
  const b = p.body;
487
505
  if (!b.alive)
488
506
  return;
489
- const wantAsleep = load.mode === 'sleep';
507
+ const wantAsleep = load.mode === 'sleep' || b.doze?.since !== undefined;
490
508
  if (wantAsleep !== b.asleep) {
491
509
  if (b.asleep)
492
510
  b.lastSleep = { from: b.since, to: p.now };
@@ -497,8 +515,15 @@ export function advanceBody(p, dt, load, mods, params = BODY_DEFAULTS) {
497
515
  return;
498
516
  const n = Math.ceil(dt / params.maxSubStep);
499
517
  const h = dt / n;
500
- for (let i = 0; i < n && b.alive; i++)
518
+ for (let i = 0; i < n && b.alive; i++) {
519
+ if (b.bladder)
520
+ advanceBladder(b.bladder, h);
501
521
  subStep(b, h, load, mods, params);
522
+ }
523
+ if (!b.alive && b.doze?.since !== undefined) {
524
+ delete b.doze.since;
525
+ b.asleep = false;
526
+ }
502
527
  }
503
528
  /** Circadian alertness 0..1 (Process C): cosine peaking at `circadianPeak`, trough 12 h earlier, post-lunch dip. */
504
529
  export function circadianAlertness(now, circadianPeak) {
@@ -578,11 +603,27 @@ export function readBody(p, params = BODY_DEFAULTS, ctx) {
578
603
  },
579
604
  };
580
605
  }
606
+ /**
607
+ * SCOPE (host-caused tiredness): add nonnegative sleep-pressure and exertion costs, clamped to 0..1.
608
+ * The host supplies magnitudes; these are not calibrated claims. Absent, negative and non-finite costs
609
+ * do nothing. A dead body is unchanged, as with consume. Writes only these two body fields: no ticking,
610
+ * sleep transition, depletion, memory, RNG or other effects. Recovery remains with the existing body APIs.
611
+ */
612
+ export function addTiredness(p, costs) {
613
+ if (!p.body.alive)
614
+ return;
615
+ for (const key of ['sleepPressure', 'exertion']) {
616
+ const amount = costs[key];
617
+ if (amount !== undefined && Number.isFinite(amount) && amount > 0)
618
+ p.body[key] = clamp01(p.body[key] + amount);
619
+ }
620
+ }
581
621
  /** Eat, drink or take pain relief. `relief` lowers current pain; it then relaxes back toward its sources. */
582
622
  export function consume(p, deltas) {
583
623
  const b = p.body;
584
624
  if (!b.alive)
585
625
  return;
626
+ intakeBladder(p, deltas.water);
586
627
  if (deltas.food)
587
628
  b.satiety = clamp01(b.satiety + deltas.food);
588
629
  if (deltas.water)
@@ -602,6 +643,8 @@ function freshId(b, prefix, now) {
602
643
  */
603
644
  export function skipBody(p, to) {
604
645
  p.body.since = to;
646
+ if (p.body.doze?.since !== undefined)
647
+ p.body.doze.since = to;
605
648
  }
606
649
  /**
607
650
  * The body-owned death setter for host-rolled deaths (`lifecourse.mortalityEvent`): health 0, not alive, awake.
@@ -614,6 +657,8 @@ export function die(p) {
614
657
  b.alive = false;
615
658
  b.health = 0;
616
659
  b.asleep = false;
660
+ if (b.doze)
661
+ delete b.doze.since;
617
662
  b.since = p.now;
618
663
  }
619
664
  export function injure(p, injury) {
@@ -746,6 +791,10 @@ const cloneBody = (b) => {
746
791
  ex[k] = { ...e };
747
792
  out.exposures = ex;
748
793
  }
794
+ if (b.doze)
795
+ out.doze = { ...b.doze, context: { ...b.doze.context } };
796
+ if (b.bladder)
797
+ out.bladder = { ...b.bladder };
749
798
  if (b.rates)
750
799
  out.rates = { ...b.rates };
751
800
  if (b.downed)
@@ -778,6 +827,8 @@ export function nextBodyThreshold(p, load, mods, thresholds = { hunger: 0.7, thi
778
827
  return watch.some((k) => r[k] >= thresholds[k]);
779
828
  };
780
829
  const sim = { ...p, body: cloneBody(p.body) };
830
+ if (sim.body.doze)
831
+ delete sim.body.doze.since;
781
832
  advanceBody(sim, 0, load, mods, params); // apply mode change
782
833
  const coarse = 30;
783
834
  let t = 0;
@@ -808,6 +859,8 @@ export function nextBodyThreshold(p, load, mods, thresholds = { hunger: 0.7, thi
808
859
  export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30, perception) {
809
860
  const out = [];
810
861
  const sim = { ...p, body: cloneBody(p.body) };
862
+ if (sim.body.doze)
863
+ delete sim.body.doze.since;
811
864
  for (const s of spans) {
812
865
  const span = { label: s.label, endsAt: sim.now, worst: 0, worstOf: 'fatigue' };
813
866
  // Hard internal cap (a week) so a hostile span cannot stall a decision.
@@ -0,0 +1,28 @@
1
+ import type { DozeContext, Person } from '../types.ts';
2
+ export declare const DOZE_DEFAULTS: Readonly<{
3
+ sleepiness: 0.75;
4
+ stimulation: 0.5;
5
+ hazard: 0.08;
6
+ seatedWeight: 0.5;
7
+ wakeSalience: 0.8;
8
+ }>;
9
+ /**
10
+ * Enable involuntary doze without drawing RNG. Idempotent; off when absent.
11
+ * Hosts must deliver current, detectable heard/felt/social alarms with salience >= 0.8
12
+ * to this person (targetId, or near: true). There is no spontaneous wake: without such
13
+ * percepts sleep continues. The community driver checks percepts on its ordinary idle cadence.
14
+ * Enabled tick integration uses one-minute boundaries even with closed gates or during sleep;
15
+ * cost is O(elapsed minutes) and continuous state can differ from the absent opt-in arm.
16
+ * Closed gates draw no onset RNG; passive community checkpoints do not call choice.
17
+ * Activity.dozedAt pins stopped work until the host resolves it, even after an alarm;
18
+ * the pending interrupt reason may retain an earlier host/percept cause.
19
+ */
20
+ export declare function enableDoze(p: Person, input: DozeContext): void;
21
+ export declare function setDozeContext(p: Person, input: DozeContext): void;
22
+ /** Pure; closed onset gates still require the next checkpoint. */
23
+ export declare function nextDozeCheck(p: Person): number;
24
+ /** Pure truth/debug reader, not a first-person lens. */
25
+ export declare function readDoze(p: Person): {
26
+ asleep: boolean;
27
+ hazardPerMinute: number;
28
+ } | undefined;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * SCOPE: opt-in minute-grid involuntary onset and current urgent-percept waking. Cole 1989
3
+ * (PMID 2606843) supports posture context; auditory arousal studies (PMIDs 8776786,
4
+ * 8153254) distinguish arousal from attention. None calibrates these engineering gates,
5
+ * hazard weights or salience. No stages, inertia, spontaneous wake or clinical prediction.
6
+ * Manual hosts schedule nextDozeCheck and resolve activities stopped at Activity.dozedAt.
7
+ * A prior pending interrupt reason is retained; physical doze still stops work.
8
+ */
9
+ import { clamp01, dexp, isNum, isObj, random, smoothstep } from '../core/index.js';
10
+ import { readBody } from './body.js';
11
+ export const DOZE_DEFAULTS = Object.freeze({
12
+ sleepiness: 0.75,
13
+ stimulation: 0.5,
14
+ hazard: 0.08,
15
+ seatedWeight: 0.5,
16
+ wakeSalience: 0.8,
17
+ });
18
+ function context(raw) {
19
+ if (!isObj(raw) ||
20
+ (raw.posture !== 'upright' && raw.posture !== 'seated' && raw.posture !== 'reclined') ||
21
+ !isNum(raw.stimulation))
22
+ return undefined;
23
+ return { posture: raw.posture, stimulation: clamp01(raw.stimulation) };
24
+ }
25
+ /**
26
+ * Enable involuntary doze without drawing RNG. Idempotent; off when absent.
27
+ * Hosts must deliver current, detectable heard/felt/social alarms with salience >= 0.8
28
+ * to this person (targetId, or near: true). There is no spontaneous wake: without such
29
+ * percepts sleep continues. The community driver checks percepts on its ordinary idle cadence.
30
+ * Enabled tick integration uses one-minute boundaries even with closed gates or during sleep;
31
+ * cost is O(elapsed minutes) and continuous state can differ from the absent opt-in arm.
32
+ * Closed gates draw no onset RNG; passive community checkpoints do not call choice.
33
+ * Activity.dozedAt pins stopped work until the host resolves it, even after an alarm;
34
+ * the pending interrupt reason may retain an earlier host/percept cause.
35
+ */
36
+ export function enableDoze(p, input) {
37
+ if (p.body.doze)
38
+ return;
39
+ const ctx = context(input);
40
+ if (!ctx)
41
+ throw new RangeError('Invalid doze context');
42
+ p.body.doze = { context: ctx };
43
+ }
44
+ export function setDozeContext(p, input) {
45
+ if (!p.body.doze)
46
+ return;
47
+ const ctx = context(input);
48
+ if (!ctx)
49
+ throw new RangeError('Invalid doze context');
50
+ p.body.doze.context = ctx;
51
+ }
52
+ /** Pure; closed onset gates still require the next checkpoint. */
53
+ export function nextDozeCheck(p) {
54
+ return p.body.doze && p.body.alive && !p.body.asleep ? Math.floor(p.now) + 1 : Number.POSITIVE_INFINITY;
55
+ }
56
+ /** Pure truth/debug reader, not a first-person lens. */
57
+ export function readDoze(p) {
58
+ const d = p.body.doze;
59
+ if (!d)
60
+ return undefined;
61
+ const s = readBody(p).sleepiness;
62
+ const D = DOZE_DEFAULTS;
63
+ const eligible = p.body.alive &&
64
+ !p.body.downed &&
65
+ !p.body.asleep &&
66
+ s > D.sleepiness &&
67
+ d.context.posture !== 'upright' &&
68
+ d.context.stimulation < D.stimulation;
69
+ const lambda = eligible
70
+ ? D.hazard *
71
+ smoothstep(D.sleepiness, 1, s) *
72
+ (d.context.posture === 'seated' ? D.seatedWeight : 1) *
73
+ (1 - d.context.stimulation / D.stimulation)
74
+ : 0;
75
+ return { asleep: d.since !== undefined, hazardPerMinute: 1 - dexp(-lambda) };
76
+ }
77
+ /** @internal Composite dispatch only, once per crossed integer minute. */
78
+ export function checkDoze(p, at) {
79
+ const d = p.body.doze;
80
+ const hazard = readDoze(p)?.hazardPerMinute ?? 0;
81
+ if (!d || hazard <= 0 || random(p.rng) >= hazard)
82
+ return false;
83
+ d.since = at;
84
+ p.body.asleep = true;
85
+ p.body.since = at;
86
+ return true;
87
+ }
88
+ /** @internal */
89
+ export function wakeDoze(p, at) {
90
+ if (p.body.doze?.since === undefined)
91
+ return false;
92
+ delete p.body.doze.since;
93
+ p.body.lastSleep = { from: p.body.since, to: at };
94
+ p.body.asleep = false;
95
+ p.body.since = at;
96
+ return true;
97
+ }
98
+ /** @internal Transfer without resetting the continuous sleep span. */
99
+ export function enterVoluntarySleep(p) {
100
+ if (p.body.doze)
101
+ delete p.body.doze.since;
102
+ }
103
+ /** @internal */
104
+ export function sanitizeDoze(raw, b, now) {
105
+ if (!isObj(raw))
106
+ return undefined;
107
+ const ctx = context(raw.context);
108
+ if (!ctx)
109
+ return undefined;
110
+ const out = { context: ctx };
111
+ if (isNum(raw.since) && raw.since <= now && raw.since === b.since && b.alive && b.asleep)
112
+ out.since = raw.since;
113
+ return out;
114
+ }
@@ -1,2 +1,5 @@
1
+ export { BLADDER_DEFAULTS, enableBladder, readBladder, voidBladder } from './bladder.ts';
1
2
  export * from './body.ts';
3
+ export { DOZE_DEFAULTS, enableDoze, nextDozeCheck, readDoze, setDozeContext } from './doze.ts';
2
4
  export * from './injury.ts';
5
+ export { estimateQuantity } from './quantity.ts';
@@ -1,2 +1,5 @@
1
+ export { BLADDER_DEFAULTS, enableBladder, readBladder, voidBladder } from './bladder.js';
1
2
  export * from './body.js';
3
+ export { DOZE_DEFAULTS, enableDoze, nextDozeCheck, readDoze, setDozeContext } from './doze.js';
2
4
  export * from './injury.js';
5
+ export { estimateQuantity } from './quantity.js';
@@ -0,0 +1,8 @@
1
+ import type { Person } from '../types.ts';
2
+ export declare function estimateQuantity(p: Person, input: {
3
+ truth: number;
4
+ difficulty: number;
5
+ }): {
6
+ value: number;
7
+ confidence: number;
8
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * SCOPE: explicit scalar observation, never a render-time reader. Valid nonzero truth and
3
+ * difficulty consume one seeded draw. Zero truth or zero difficulty return exact truth without
4
+ * drawing; invalid/overflowing requests reject before drawing.
5
+ * Van Dongen et al. 2003 (PMID 12683469) supports separating objective impairment from
6
+ * subjective awareness, not quantity-task coefficients. Uniform errors and certainty are
7
+ * engineering defaults, not calibrated error intervals. No history or belief mutation.
8
+ */
9
+ import { clamp01, random } from '../core/index.js';
10
+ import { readBody } from './body.js';
11
+ export function estimateQuantity(p, input) {
12
+ const { truth, difficulty } = input;
13
+ if (!Number.isFinite(truth) ||
14
+ truth < 0 ||
15
+ !Number.isFinite(difficulty) ||
16
+ difficulty < 0 ||
17
+ difficulty > 1)
18
+ throw new RangeError('Quantity requires finite non-negative truth and difficulty in [0,1]');
19
+ if (truth === 0 || difficulty === 0)
20
+ return { value: truth, confidence: 1 };
21
+ const body = readBody(p);
22
+ const error = difficulty * (0.05 + 0.45 * body.sleepiness);
23
+ if (!Number.isFinite(truth * (1 + error)))
24
+ throw new RangeError('Quantity estimate would overflow');
25
+ const value = truth * (1 + (2 * random(p.rng) - 1) * error);
26
+ return { value, confidence: clamp01(1 - difficulty * (0.05 + 0.45 * body.perceived.sleepiness)) };
27
+ }
@@ -1,3 +1,40 @@
1
+ /**
2
+ * SCOPE: scoring of offered options. `consider` sums the utility terms listed in docs/framework.md
3
+ * ("Utility of an option"): need terms use the person's need urgencies times the gain they *believe* an
4
+ * option gives (the advertisement blended with learned expectation, times believed success); norm,
5
+ * commitment/goal, habit, emotion-tendency, social, effort, risk, material, precommitment and suggestion
6
+ * terms come from the owning faculties or from simple coefficient forms. Traits and values only scale
7
+ * terms. `decide` processes affordances in stable id order, scores them all and hands the list to `will`
8
+ * for vetoes and selection. Borrowed shapes: additive multi-attribute utility with a saturating money
9
+ * term (diminishing marginal value of material gain), prediction-error-adjusted action values
10
+ * (model-free cached values alongside the host's model-based advertisement). It does NOT claim calibrated
11
+ * weights, rational-choice optimality, or that a weighted sum exhausts human deliberation; every term is
12
+ * kept in the trace so the UI can show why.
13
+ *
14
+ * SCOPE (lane will+cognition, 2026-10-03): several voices add one `suggestion:<voiceId>` term each, with
15
+ * reactance summed into one `autonomy` term; remembered advice adds `suggestion:remembered:<source>` terms
16
+ * (see `rememberedTerms` and `will.rememberAdvice`). Scarcity (N13): a host-supplied 0..1 shortfall raises the
17
+ * weight of the material term, shrinks its saturation scale so small sums still count, and raises the
18
+ * `need:safety` term. Borrowed shapes: diminishing marginal utility of wealth (the same sum weighs more to
19
+ * someone who has less) and the scarcity account of Mullainathan & Shafir, where a shortfall captures
20
+ * attention and raises the felt value of the scarce resource. Neither source is catalogued in
21
+ * research/empirical-models.md yet; the coefficients are engineering defaults. It does not model the claimed
22
+ * cognitive cost of scarcity ("bandwidth tax"), debt itself (a host ledger plus commitments), or present
23
+ * bias; and because losses are amplified too, a host must carry the benefit of paying a debt (a commitment,
24
+ * an advertised `safety` gain) or paying looks only like a loss. The spec's "security need" is read as the
25
+ * `safety` need: `security` is a value, not a need, and no need is added.
26
+ *
27
+ * SCOPE (2.8.0, both additive): an offer naming `alongside` partners (people already taking that course) gets one
28
+ * `together:<id>` term per known, living partner, `togetherScale` × closeness, without belonging satiation: the
29
+ * pull is not to be left behind, which stays when the pair is together and belonging is full (The Rope's resting
30
+ * partner, 2026-10-18 reproduction: belonging terms near 0.01 against inertia of 0.07-0.15). Shape only, after
31
+ * joint commitment and the sense of commitment in joint action (Gilbert 1990; Michael and Pacherie 2015; cited unaudited, research/empirical-models.md §6b); it does
32
+ * not model who leads, negotiation, or obligations between the partners. With opt-in `body.fullness`, an offer
33
+ * that advertises food, water or relief while that need is past its comfort threshold gets `full:<need>` =
34
+ * -fullnessScale × gain × over²: eating or drinking when full is mildly unpleasant (negative alliesthesia, unaudited citation, Cabanac
35
+ * 1971), so idle choice stops cycling short consumption at full hydration (The Rope, owner Kestrel export
36
+ * 2026-10-19). It models no bladder, sensory-specific satiety by food, or overeating for comfort.
37
+ */
1
38
  import type { BodyPerceptionContext } from '../body/index.ts';
2
39
  import { type HabitContext } from '../habits/index.ts';
3
40
  import { type PromptingSeen } from '../promptings/index.ts';
@@ -38,8 +38,10 @@
38
38
  import { emotionLevel, inBreak } from '../affect/index.js';
39
39
  import { agendaTermsWithGoalEstimates } from '../agenda/index.js';
40
40
  import { trustOf } from '../beliefs/index.js';
41
+ import { bladderMotivation } from '../body/bladder.js';
41
42
  import { normTerms } from '../conscience/index.js';
42
43
  import { clamp01, cmpStr, dexp, minuteOfDay, round } from '../core/index.js';
44
+ import { keysOf } from '../core/keys.js';
43
45
  import { curiosityTerms } from '../curiosity/index.js';
44
46
  import { habitEase, habitPull } from '../habits/index.js';
45
47
  import { expectedEffect } from '../memory/index.js';
@@ -270,7 +272,9 @@ export function consider(p, aff, ctx) {
270
272
  };
271
273
  // Needs: urgency × believed gain × believed success.
272
274
  const expected = expectedEffect(p, aff);
273
- for (const id of Object.keys(expected.needs)) {
275
+ if (p.body.bladder)
276
+ push('need:bladder', bladderMotivation(p, aff, expected.successRate));
277
+ for (const id of keysOf(expected.needs)) {
274
278
  const gain = expected.needs[id] ?? 0;
275
279
  if (gain === 0)
276
280
  continue;
@@ -296,7 +300,7 @@ export function consider(p, aff, ctx) {
296
300
  let relevance = 1;
297
301
  if (expected.valence > 0) {
298
302
  let served = -1;
299
- for (const id of Object.keys(expected.needs))
303
+ for (const id of keysOf(expected.needs))
300
304
  if ((expected.needs[id] ?? 0) > 0)
301
305
  served = Math.max(served, urgencyOf(ctx.needs, id));
302
306
  if (served >= 0)
@@ -1,3 +1,4 @@
1
+ export * from './keys.ts';
1
2
  export * from './libm.ts';
2
3
  export * from './math.ts';
3
4
  export * from './random.ts';
@@ -1,3 +1,4 @@
1
+ export * from './keys.js';
1
2
  export * from './libm.js';
2
3
  export * from './math.js';
3
4
  export * from './random.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Own enumerable string keys of a typed record, in Object.keys order.
3
+ * Callers must supply records whose runtime keys match their declared key type.
4
+ * This only types enumeration; it does not sort, validate keys or change state.
5
+ */
6
+ export declare function keysOf<K extends string>(record: Partial<Record<K, unknown>>): K[];
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Own enumerable string keys of a typed record, in Object.keys order.
3
+ * Callers must supply records whose runtime keys match their declared key type.
4
+ * This only types enumeration; it does not sort, validate keys or change state.
5
+ */
6
+ export function keysOf(record) {
7
+ return Object.keys(record);
8
+ }
@@ -88,7 +88,7 @@ function sanitizeAptitudes(x) {
88
88
  out[k] = clamp(v, 0.25, 4);
89
89
  return Object.keys(out).length > 0 ? out : undefined;
90
90
  }
91
- /** Validate a saved family slice; undefined when nothing usable remains (used by `restore`). @internal */
91
+ /** Validate a saved family slice; drop malformed pregnancies, keep usable fields. Undefined when none remain. @internal */
92
92
  export function sanitizeFamily(x) {
93
93
  if (!isObj(x))
94
94
  return undefined;
@@ -101,9 +101,9 @@ export function sanitizeFamily(x) {
101
101
  if (pg &&
102
102
  typeof pg === 'object' &&
103
103
  typeof pg.fatherId === 'string' &&
104
- typeof pg.conceivedAt === 'number' &&
105
- typeof pg.dueAt === 'number' &&
106
- typeof pg.seed === 'number')
104
+ isNum(pg.conceivedAt) &&
105
+ isNum(pg.dueAt) &&
106
+ isNum(pg.seed))
107
107
  out.pregnancy = { fatherId: pg.fatherId, conceivedAt: pg.conceivedAt, dueAt: pg.dueAt, seed: pg.seed };
108
108
  if (isNum(f.attachment))
109
109
  out.attachment = clamp01(f.attachment);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.19.0";
2
+ export declare const FRAMEWORK_VERSION = "2.21.0";
3
3
  export type { CrisisEvent, PracticeKind } from './affect/index.ts';
4
4
  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.ts';
5
5
  export type { GoalTemplate, MakeUpReceipt, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
@@ -28,6 +28,7 @@ export type { MigrationStep } from './migrate.ts';
28
28
  export { MIGRATIONS, migratableVersions, migrate } from './migrate.ts';
29
29
  export * from './narrate/index.ts';
30
30
  export * from './needs/index.ts';
31
+ export { confirmObservedReputation } from './observed-reputation.ts';
31
32
  export * from './partnering/index.ts';
32
33
  export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistence/index.ts';
33
34
  export * from './person.ts';
@@ -38,6 +39,7 @@ export { adoptProjectTask, considerProjects, enableProjects, PROJECTS_DEFAULTS,
38
39
  export * from './promptings/index.ts';
39
40
  export type { ConsequenceEvidence, PlanRoute, PlanStep } from './prospection/index.ts';
40
41
  export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.ts';
42
+ export { type QalbDescription, qalbReading } from './qalb.ts';
41
43
  export { fingerprint, restore, snapshot } from './restore.ts';
42
44
  export * from './review/index.ts';
43
45
  export * from './scenarios/index.ts';