@adam0white/human-framework 2.22.0 → 3.0.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.
Files changed (43) hide show
  1. package/README.md +138 -55
  2. package/dist/agenda/agenda.js +1 -1
  3. package/dist/beliefs/beliefs.js +2 -2
  4. package/dist/body/doze.d.ts +6 -3
  5. package/dist/body/doze.js +7 -4
  6. package/dist/conscience/conscience.d.ts +2 -4
  7. package/dist/conscience/conscience.js +3 -5
  8. package/dist/conversation/conversation.js +11 -10
  9. package/dist/core/index.d.ts +0 -1
  10. package/dist/core/index.js +0 -1
  11. package/dist/core/math.d.ts +0 -2
  12. package/dist/core/math.js +3 -3
  13. package/dist/family/family.d.ts +0 -5
  14. package/dist/family/family.js +12 -6
  15. package/dist/impression.d.ts +2 -7
  16. package/dist/impression.js +5 -7
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.js +2 -1
  19. package/dist/longrun.d.ts +2 -2
  20. package/dist/longrun.js +1 -2
  21. package/dist/memory/memory.d.ts +0 -4
  22. package/dist/memory/memory.js +20 -6
  23. package/dist/migrate.js +5 -0
  24. package/dist/narrate/narrate.js +3 -1
  25. package/dist/observed-reputation.js +1 -1
  26. package/dist/person.d.ts +5 -1
  27. package/dist/person.js +16 -10
  28. package/dist/persona.js +1 -0
  29. package/dist/presentation.js +1 -0
  30. package/dist/promptings/promptings.js +2 -2
  31. package/dist/qalb.d.ts +0 -12
  32. package/dist/qalb.js +22 -8
  33. package/dist/sim/longrun.d.ts +3 -3
  34. package/dist/sim/longrun.js +1 -1
  35. package/dist/sim/sim.js +9 -4
  36. package/dist/social/impressions.d.ts +0 -12
  37. package/dist/social/impressions.js +13 -8
  38. package/dist/social/social.js +2 -2
  39. package/dist/tick-input.d.ts +24 -0
  40. package/dist/tick-input.js +56 -0
  41. package/dist/types.d.ts +1 -1
  42. package/dist/types.js +1 -1
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -4,10 +4,50 @@ HF is a TypeScript library for simulated people in games and headless simulation
4
4
  possible actions. Each person weighs needs, habits, duties, goals and suggestions, then chooses.
5
5
  A player can suggest an action; the person can defer or refuse it.
6
6
 
7
- HF 2.14.1 uses utility scoring and seeded randomness, with no LLM in the actor loop and no runtime
7
+ HF 3.0.0 uses utility scoring and seeded randomness, with no LLM in the actor loop and no runtime
8
8
  dependencies. Decision records expose scored terms and template narration. Person state is plain JSON.
9
9
  This is an authored simulation model, not a validated predictor of real people.
10
10
 
11
+ ## Migrating to v3
12
+
13
+ The API migration keeps engine `2.23.0`. Relative to HF 2.23.0, it does not change seeded draws, numeric rules or saved Person state. The package version is `3.0.0`; publication is recorded separately in the release history.
14
+
15
+ | Before | v3 |
16
+ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
17
+ | `glimpse(p, id, signs, { at, clarity })` | `glimpse(p, id, signs, at, clarity)` |
18
+ | `glimpseOf(p, target, at, clarity)` or `glimpseOf(p, target, { at, clarity })` | `glimpsePerson(p, target, at, clarity)` |
19
+ | `acquaintWith(p, target, familiarity, at)` | `acquaintPerson(p, target, familiarity, at)` |
20
+ | `hear(p, id, key, value, { at, weight })` | `hear(p, id, key, value, at, weight)` |
21
+ | `conceive(mother, father, { seed, at })` | `conceive(mother, father, seed, at)` |
22
+ | `retell(teller, listener, { at, trust, limit, placeIds })` | `retell(teller, listener, at, { trust, limit, placeIds })` |
23
+ | `TRAIT_NAMES` | `TRAIT_KEYS` (same order) |
24
+ | `expit(x)` | `sigmoid(x)` (same function body) |
25
+ | `keysOf(record)` | `Object.keys(record)` (same enumeration order; narrow host key types explicitly when needed) |
26
+ | `repent(p, breachId, unusedTime)` | `repent(p, breachId)` |
27
+ | `recordRepair(p, victimId, unusedTime)` | `recordRepair(p, victimId)` |
28
+ | `liveRoutine(p, to, fixedRoutine)` | `liveRoutine(p, to, () => fixedRoutine)` |
29
+ | `routineFor: (p, day, community) => choose(p, day, community)` | `routineFor: (p, day) => choose(p, day, community)` with `community` captured from the host scope |
30
+
31
+ Removed options-object calls from untyped JavaScript throw `TypeError` before changing Person state. Their time argument must be a finite number in the positional slot.
32
+
33
+ Both routine drivers use `RoutineSource = (p: Person, day: number) => Routine`. `day` is the absolute day being started. Existing two-argument sources continue unchanged. A community source closes over the community for that run; bind a new closure after restoring a different community. Sources still run immediately before each person's routine day. Community dawn hooks still run first, and earlier people's days are already applied when later sources read community state. Do not precompute all routines at dawn if that would change those reads.
34
+
35
+ The primitive names `glimpse` and `acquaint` stay. Their Person composites use the explicit `Person` suffix. The proposed `skip` → `skipTo` rename is dropped (Q15): it provides no player value and `skip` is clear in context.
36
+
37
+ ### Tuning defaults (Q5)
38
+
39
+ Assignment to existing mutable defaults remains the tuning API in v3. These settings apply to every run in the same module instance. Set them before creating or restoring a run, then keep them fixed while it runs. Already-frozen defaults remain frozen.
40
+
41
+ Deterministic replay requires the same engine version, effective tuning values, initial state, seeded streams, inputs and call order. Person snapshots do not store these global settings. A host that tunes must store its complete effective configuration or its overrides relative to a known pristine baseline in its save or replay envelope. Before restoring or continuing, restore that baseline first, then apply the saved overrides. A fresh module instance is another way to start from the baseline. Applying overrides over a previous run's settings can leak tuning between runs.
42
+
43
+ Two worlds with different settings cannot safely share one module instance. Tests that tune must restore values in a `finally` block and must not mutate them concurrently. This decision preserves existing hosts and seeded behavior. It adds no global configuration function or per-person settings. A future isolated settings API must first demonstrate exact snapshot, restore and continuation under overrides.
44
+
45
+ ## Ordering inputs for one logical tick (opt-in)
46
+
47
+ `canonicalTickInputs(tick, inputs, expected)` waits for the complete declared sender/sequence set and returns a new envelope array sorted by sender, then sequence. It returns `undefined` while an input is missing and throws for duplicate, unexpected or wrong-tick inputs. Payloads remain host-owned. Use the returned array only after the barrier completes; save pending input with host state.
48
+
49
+ This is a bounded host convention, not transport, consensus or fairness. It reads no Person or RNG. Existing actor calls never use it implicitly. The H3 controls compare reordered deliveries, withheld input, restore and a behaviorally different unsorted control. The copyable two-person starter in `examples/h3-starter` owns its own clock and replay envelope; it is not a complete game engine or part of the npm artifact.
50
+
11
51
  ## Try the games
12
52
 
13
53
  - [Twice at the Well](https://human.adamwhite.work/colony/): compare a colony without and with HF.
@@ -20,21 +60,23 @@ This is an authored simulation model, not a validated predictor of real people.
20
60
  Use Node 24 or newer. In an empty directory, install the version this README describes:
21
61
 
22
62
  ```sh
23
- npm install @adam0white/human-framework@2.14.1
63
+ npm install @adam0white/human-framework@3.0.0
24
64
  ```
25
65
 
26
66
  Save these 20 lines as `quickstart.mjs`, then run `node quickstart.mjs`:
27
67
 
28
68
  ```js
29
69
  import {
30
- createCommunity, createPerson, createVillage,
31
- predict, stepCommunity, villagerSpec,
70
+ createCommunity,
71
+ createPerson,
72
+ createVillage,
73
+ predict,
74
+ stepCommunity,
75
+ villagerSpec,
32
76
  } from '@adam0white/human-framework';
33
77
  const start = 6 * 60; // simulation minutes
34
78
  const ids = ['ada', 'bora'];
35
- const people = ids.map((id, i) => createPerson(
36
- villagerSpec(id, id, i + 1, { now: start, others: ids }),
37
- ));
79
+ const people = ids.map((id, i) => createPerson(villagerSpec(id, id, i + 1, { now: start, others: ids })));
38
80
  const world = createVillage(people, { seed: 7 });
39
81
  const community = createCommunity(people);
40
82
  const ada = community.people[0];
@@ -83,14 +125,14 @@ A suggestion is one urge among many. Its weight is strength × trust in the voic
83
125
  fits the person's motives, and a suggestion alone never overrides a veto. Each one resolves to a typed
84
126
  verdict (`SuggestionResolution`):
85
127
 
86
- | verdict | meaning | `kind` |
87
- |---|---|---|
88
- | `assented` | the suggested option won | none |
89
- | `deferred` | the person's own preference won; carries a `counterOffer` ("after I eat lunch") | `notNow` |
90
- | `modified` | a near alternative with the same action or aim won; the `counterOffer` names it | `notNow` |
91
- | `refused` | cannot (asleep, exhausted, skill far too low, a pressing bodily need) or will not (a firmly held norm, broken trust) | `cannot` / `willNot` |
92
- | `complied` | the voice insisted on a "not now"; done under protest (`activity.protest`), at a cost to autonomy and trust | `notNow` |
93
- | `commanded` | the host took direct control (see [Direct control](#direct-control)); the person does the order whatever they would have chosen | none |
128
+ | verdict | meaning | `kind` |
129
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
130
+ | `assented` | the suggested option won | none |
131
+ | `deferred` | the person's own preference won; carries a `counterOffer` ("after I eat lunch") | `notNow` |
132
+ | `modified` | a near alternative with the same action or aim won; the `counterOffer` names it | `notNow` |
133
+ | `refused` | cannot (asleep, exhausted, skill far too low, a pressing bodily need) or will not (a firmly held norm, broken trust) | `cannot` / `willNot` |
134
+ | `complied` | the voice insisted on a "not now"; done under protest (`activity.protest`), at a cost to autonomy and trust | `notNow` |
135
+ | `commanded` | the host took direct control (see [Direct control](#direct-control)); the person does the order whatever they would have chosen | none |
94
136
 
95
137
  Insisting (`insist: true`) turns only a `notNow` into compliance, and not when a bodily need is pressing. It
96
138
  never overrides `cannot` or `willNot`.
@@ -132,11 +174,12 @@ An offer describes expected effects. An outcome reports what happened. The host
132
174
  contact, inventory and payment; HF uses the reported outcome to update the person. Do not copy
133
175
  `Affordance.advertises` into `Outcome.needs` as a general completion rule.
134
176
 
135
- | Host situation | Offer | Completion report |
136
- |---|---|---|
137
- | A person sleeps for part of the planned night. | Advertise typical sleep/rest benefits and set `mode: 'sleep'`. | Leave `sleep` and `rest` absent from `Outcome.needs`. `finish` advances the body to the actual end time and measures their change since the activity began. Explicit values replace that measurement; copying the advertisement can overstate the realized recovery reported to learning. |
138
- | A task promises four coins but actually pays two. | Set expected `Affordance.material` to four host units. | Update the host ledger by two and report `Outcome.material: 2`. A completed task does not imply the promised payment arrived. Report zero for no payment and a negative value for an actual cost. HF does not maintain the host's money ledger. |
139
- | A person can talk with someone who is present. | Tag the offer `social` when it really involves social contact; put participants in `with`. | Resolve actual participation. An empty room, a sleeping neighbour or someone who arrives after the conversation ends is not a completed encounter. Do not award contact or belonging merely because the activity uses a shared place. |
177
+ <!-- biome-ignore format: Biome 2.6.3 adds column padding on each pass for this inline-code table; preserve stable source. -->
178
+ | Host situation | Offer | Completion report |
179
+ | ------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
180
+ | A person sleeps for part of the planned night. | Advertise typical sleep/rest benefits and set `mode: 'sleep'`. | Leave `sleep` and `rest` absent from `Outcome.needs`.`finish` advances the body to the actual end time and measures their change since the activity began. Explicit values replace that measurement; copying the advertisement can overstate the realized recovery reported to learning. |
181
+ | A task promises four coins but actually pays two. | Set expected `Affordance.material` to four host units. | Update the host ledger by two and report `Outcome.material: 2`. A completed task does not imply the promised payment arrived. Report zero for no payment and a negative value for an actual cost. HF does not maintain the host's money ledger. |
182
+ | A person can talk with someone who is present. | Tag the offer `social` when it really involves social contact; put participants in `with`. | Resolve actual participation. An empty room, a sleeping neighbour or someone who arrives after the conversation ends is not a completed encounter. Do not award contact or belonging merely because the activity uses a shared place. |
140
183
 
141
184
  For in-person activities, the host must establish co-presence and overlapping participation. HF has
142
185
  no map and does not discover companions. `with` names people taking part, not everyone assigned to a
@@ -211,7 +254,7 @@ reference host: `createVillage`.
211
254
  const village2 = createVillage(people, { seed: 0, state: s.world });
212
255
  const c2 = createCommunity(people, s.community); // continues exactly as the unsaved run would
213
256
  ```
214
- - **Time arguments.** A host event takes `at` as its last required argument, with optional knobs after it (`glimpseOf(observer, target, at, clarity?)`, `retell(teller, listener, at, opts?)`); a query evaluated at a time takes `now`. The 2.0 options-object forms still work and are deprecated until 3.0. See docs/framework.md, "Argument order".
257
+ - **Time arguments.** A host event takes `at` as its last required argument, with optional knobs after it (`glimpsePerson(observer, target, at, clarity?)`, `retell(teller, listener, at, opts?)`); a query evaluated at a time takes `now`. The former 2.0 options-object forms are removed in v3. See docs/framework.md, "Argument order".
215
258
  - Hosts that live people sparsely over decades can shrink what a save carries per person with
216
259
  `setRetention(p, { trace, chronicleDays })` (or `PersonSpec.retention`): fewer decision records, and day records
217
260
  bounded by age as well as count. No decision changes. `restore` also refuses a non-finite or out-of-range `now`
@@ -222,7 +265,7 @@ reference host: `createVillage`.
222
265
 
223
266
  There are two version numbers, and they move independently.
224
267
 
225
- - **The release version** is the package version (`package.json`, currently `2.22.0`) and `FRAMEWORK_VERSION`, the same
268
+ - **The release version** is the package version (`package.json`, currently `3.0.0`) and `FRAMEWORK_VERSION`, the same
226
269
  string compiled into the build; a test keeps them equal. It versions the public API under
227
270
  [semver](https://semver.org/): a breaking change to an exported name or signature bumps the major, a new
228
271
  faculty or export the minor, a fix the patch. Each release is a tag `vX.Y.Z` and a GitHub release whose notes
@@ -242,7 +285,7 @@ this policy.
242
285
 
243
286
  ## Optional felt body (2.21 release)
244
287
 
245
- `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.
288
+ `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 with `finish`. Until that call, `begin` refuses every replacement, including voluntary sleep and work chosen after waking. Doze does not wake spontaneously. `perceive` wakes for a delivered percept stamped within the current doze span through `p.now`, including an alarm that occurred between community checkpoints, 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. Other delivered percepts pass through ordinary bounded attention, as during voluntary sleep. These hazard and salience values are engineering constants.
246
289
 
247
290
  `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.
248
291
 
@@ -269,8 +312,8 @@ of it runs as before.
269
312
  - **Surroundings.** `setAmbient` (cold, dark, crowding, beauty, weather, day length) shifts mood, body and needs.
270
313
  - **Multi-year stepping.** `liveRoutine` and `liveCommunity` live people by routine a coarse day at a time, with
271
314
  natural death, chronic onsets, deaths told and minors raised.
272
- - **Impressions of others.** What one person believes about another (`glimpseOf(observer, target, at, clarity?)`, `observeAct`, `hear(observer, targetId, key, value, at, weight?)`,
273
- `acquaintWith`), with confidence; `predictAs` runs a prediction on the person as the observer pictures them.
315
+ - **Impressions of others.** What one person believes about another (`glimpsePerson(observer, target, at, clarity?)`, `observeAct`, `hear(observer, targetId, key, value, at, weight?)`,
316
+ `acquaintPerson`), with confidence; `predictAs` runs a prediction on the person as the observer pictures them.
274
317
  - **Switching on at creation.** `PersonSpec.enable: { gists, yearbook, character, skillRetention }` turns the long-run faculties above on when the person is created.
275
318
  - **Answering between decisions.** `answerNow` books a refusal at once without touching the running activity.
276
319
 
@@ -314,7 +357,7 @@ that place lose appeal until the fear fades. Groups and factions themselves stay
314
357
  the bundled `DEFAULT_NORMS` is Islam-guided; each reference's verification status is listed in
315
358
  [research/norm-sources.md](https://github.com/adam0white/human-framework/blob/main/research/norm-sources.md)). A host with its own catalog passes it as
316
359
  `World.catalog`, so necessity excuses and exemptions read that catalog; a norm not in it is not excused.
317
- A person holds an *understanding* of a norm, with a standing and a conviction. That understanding is
360
+ A person holds an _understanding_ of a norm, with a standing and a conviction. That understanding is
318
361
  not a ruling. The library never computes divine acceptance. Where schools of thought differ, the
319
362
  position the bundled catalog takes, and why, is recorded in
320
363
  [research/decisions.md](https://github.com/adam0white/human-framework/blob/main/research/decisions.md).
@@ -341,18 +384,30 @@ Use observations the person actually received; the host owns what those observat
341
384
  ```ts
342
385
  receiveMethodCues(learner, {
343
386
  contextId: 'observed-example',
344
- cues: [{ cue: 'route', value: 'north' }, { cue: 'signal', value: 'clear' }],
387
+ cues: [
388
+ { cue: 'route', value: 'north' },
389
+ { cue: 'signal', value: 'clear' },
390
+ ],
345
391
  });
346
392
  demonstrateMethod(learner, {
347
- id: 'routing-north', demonstrationId: 'example-1',
348
- contextId: 'observed-example', sourceId: 'instructor',
349
- conditions: [{ cue: 'route', value: 'north' }, { cue: 'signal', value: 'clear' }],
350
- action: 'send-north', outcome: 'success',
393
+ id: 'routing-north',
394
+ demonstrationId: 'example-1',
395
+ contextId: 'observed-example',
396
+ sourceId: 'instructor',
397
+ conditions: [
398
+ { cue: 'route', value: 'north' },
399
+ { cue: 'signal', value: 'clear' },
400
+ ],
401
+ action: 'send-north',
402
+ outcome: 'success',
351
403
  });
352
404
  // A new parcel can share the learned observation structure.
353
405
  receiveMethodCues(learner, {
354
406
  contextId: 'new-parcel',
355
- cues: [{ cue: 'route', value: 'north' }, { cue: 'signal', value: 'clear' }],
407
+ cues: [
408
+ { cue: 'route', value: 'north' },
409
+ { cue: 'signal', value: 'clear' },
410
+ ],
356
411
  });
357
412
  const evidence = methodEvidence(learner, 'send-north', 'new-parcel');
358
413
  // Offers for that parcel carry methodContext: 'new-parcel'.
@@ -369,39 +424,67 @@ HF 2.3.0 lets an attended explanation connect one current action to one possible
369
424
 
370
425
  ```ts
371
426
  import {
372
- type Affordance, type ReceivedConsequence,
373
- consequenceEvidence, createPerson, decide, perceive,
427
+ type Affordance,
428
+ type ReceivedConsequence,
429
+ consequenceEvidence,
430
+ createPerson,
431
+ decide,
432
+ perceive,
374
433
  } from '@adam0white/human-framework';
375
434
 
376
435
  const person = createPerson({
377
- id: 'courier', name: 'Courier', seed: 19, now: 600,
378
- bornAt: -30 * 365 * 1440, sex: 'female',
379
- goals: [{
380
- id: 'delivery', label: 'Deliver the parcel', serves: ['achievement'],
381
- advancedBy: [{ action: 'deliver', amount: 1 }], importance: 0.8,
382
- temporal: { halfLifeMinutes: 30 },
383
- }],
436
+ id: 'courier',
437
+ name: 'Courier',
438
+ seed: 19,
439
+ now: 600,
440
+ bornAt: -30 * 365 * 1440,
441
+ sex: 'female',
442
+ goals: [
443
+ {
444
+ id: 'delivery',
445
+ label: 'Deliver the parcel',
446
+ serves: ['achievement'],
447
+ advancedBy: [{ action: 'deliver', amount: 1 }],
448
+ importance: 0.8,
449
+ temporal: { halfLifeMinutes: 30 },
450
+ },
451
+ ],
384
452
  });
385
453
  const route: ReceivedConsequence = {
386
- id: 'access-window', revision: 0,
454
+ id: 'access-window',
455
+ revision: 0,
387
456
  trigger: { action: 'ask-access', targetId: 'keeper' },
388
457
  successor: { action: 'deliver', duration: 20, effort: 0.1 },
389
- availableFrom: 620, availableUntil: 680, expiresAt: 800,
390
- chance: 0.8, confidence: 0.9,
458
+ availableFrom: 620,
459
+ availableUntil: 680,
460
+ expiresAt: 800,
461
+ chance: 0.8,
462
+ confidence: 0.9,
391
463
  };
392
- perceive(person, [{
393
- at: 600, channel: 'told', kind: 'explanation', actorId: 'keeper',
394
- targetId: person.id, salience: 1,
395
- summary: 'If your request succeeds, this route may become available.',
396
- consequences: [route],
397
- }]);
464
+ perceive(person, [
465
+ {
466
+ at: 600,
467
+ channel: 'told',
468
+ kind: 'explanation',
469
+ actorId: 'keeper',
470
+ targetId: person.id,
471
+ salience: 1,
472
+ summary: 'If your request succeeds, this route may become available.',
473
+ consequences: [route],
474
+ },
475
+ ]);
398
476
  const request: Affordance = {
399
- id: 'ask', action: 'ask-access', label: 'Request access', targetId: 'keeper',
400
- duration: 10, effort: 0.1, advertises: {},
477
+ id: 'ask',
478
+ action: 'ask-access',
479
+ label: 'Request access',
480
+ targetId: 'keeper',
481
+ duration: 10,
482
+ effort: 0.1,
483
+ advertises: {},
401
484
  };
402
- const offers: Affordance[] = [request,
403
- { id: 'direct', action: 'deliver', label: 'Deliver directly',
404
- duration: 180, effort: 0.1, advertises: {} },
485
+ const offers: Affordance[] = [
486
+ request,
487
+ { id: 'direct', action: 'deliver', label: 'Deliver directly', duration: 180, effort: 0.1, advertises: {} },
405
488
  ];
406
489
  const decision = decide(person, offers);
407
490
  const evidence = consequenceEvidence(person, request); // pure; no RNG consumed
@@ -897,7 +897,7 @@ function boundOwed(state) {
897
897
  return;
898
898
  while (owed.length > AGENDA_DEFAULTS.maxOwed) {
899
899
  const i = owed.findIndex((o) => o.scheduledAs !== undefined);
900
- owed.splice(i >= 0 ? i : 0, 1);
900
+ owed.splice(Math.max(i, 0), 1);
901
901
  }
902
902
  }
903
903
  /** A kept, closed instance is positive recorded evidence; scheduling or missing history is not. */
@@ -24,7 +24,7 @@
24
24
  * not prove deceit, change the belief, or change relationship or voice trust. Loss and retention limits are
25
25
  * engineering defaults, not calibrated effect sizes. Deduplication lasts only while its bounded record stays.
26
26
  */
27
- import { clamp, clamp01, decay, dpow, expit, isNum, isObj, logit } from '../core/index.js';
27
+ import { clamp, clamp01, decay, dpow, isNum, isObj, logit, sigmoid } from '../core/index.js';
28
28
  import { MAX_MINUTE, MINUTES_PER_DAY } from '../types.js';
29
29
  export const BELIEF_DEFAULTS = {
30
30
  maxBeliefs: 300,
@@ -229,7 +229,7 @@ export function believe(p, prop, value, confidence, sourceId, now) {
229
229
  }
230
230
  export function credence(p, prop) {
231
231
  const b = findBelief(p, prop);
232
- return b ? expit(b.logOdds) : 0.5;
232
+ return b ? sigmoid(b.logOdds) : 0.5;
233
233
  }
234
234
  /**
235
235
  * Direct observation of the truth: set the belief firmly and recalibrate the sources that spoke on it.
@@ -8,13 +8,16 @@ export declare const DOZE_DEFAULTS: Readonly<{
8
8
  }>;
9
9
  /**
10
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
11
+ * Hosts must deliver detectable heard/felt/social alarms with salience >= 0.8
12
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.
13
+ * percepts sleep continues. Alarm timestamps must be within the current doze span
14
+ * through p.now; delayed interval delivery can wake. Other delivered percepts still
15
+ * pass through ordinary bounded attention, as during voluntary sleep. The community driver checks percepts on its ordinary idle cadence.
14
16
  * Enabled tick integration uses one-minute boundaries even with closed gates or during sleep;
15
17
  * cost is O(elapsed minutes) and continuous state can differ from the absent opt-in arm.
16
18
  * 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;
19
+ * Activity.dozedAt pins stopped work until the host resolves it with finish, even after an alarm;
20
+ * begin refuses all replacements while that pin remains;
18
21
  * the pending interrupt reason may retain an earlier host/percept cause.
19
22
  */
20
23
  export declare function enableDoze(p: Person, input: DozeContext): void;
package/dist/body/doze.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * SCOPE: opt-in minute-grid involuntary onset and current urgent-percept waking. Cole 1989
2
+ * SCOPE: opt-in minute-grid involuntary onset and interval-delivered urgent-percept waking. Cole 1989
3
3
  * (PMID 2606843) supports posture context; auditory arousal studies (PMIDs 8776786,
4
4
  * 8153254) distinguish arousal from attention. None calibrates these engineering gates,
5
5
  * hazard weights or salience. No stages, inertia, spontaneous wake or clinical prediction.
@@ -24,13 +24,16 @@ function context(raw) {
24
24
  }
25
25
  /**
26
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
27
+ * Hosts must deliver detectable heard/felt/social alarms with salience >= 0.8
28
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.
29
+ * percepts sleep continues. Alarm timestamps must be within the current doze span
30
+ * through p.now; delayed interval delivery can wake. Other delivered percepts still
31
+ * pass through ordinary bounded attention, as during voluntary sleep. The community driver checks percepts on its ordinary idle cadence.
30
32
  * Enabled tick integration uses one-minute boundaries even with closed gates or during sleep;
31
33
  * cost is O(elapsed minutes) and continuous state can differ from the absent opt-in arm.
32
34
  * 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;
35
+ * Activity.dozedAt pins stopped work until the host resolves it with finish, even after an alarm;
36
+ * begin refuses all replacements while that pin remains;
34
37
  * the pending interrupt reason may retain an earlier host/percept cause.
35
38
  */
36
39
  export function enableDoze(p, input) {
@@ -100,13 +100,11 @@ export declare function recordDeed(p: Person, aff: Affordance, intention: string
100
100
  * The person repents a breach: regret, stopping and resolve. When another person was wronged,
101
101
  * restitution is also required, so the breach closes only after `recordRepair` for that victim.
102
102
  * Returns whether the breach is now closed. This records the person's acts, not acceptance.
103
- * The unused `_now` parameter is optional since 2.9.1 and is removed in 3.0.
104
103
  */
105
- export declare function repent(p: Person, id: string, _now?: Minute): boolean;
104
+ export declare function repent(p: Person, id: string): boolean;
106
105
  /**
107
106
  * Restitution/apology delivered to a wronged person (host-attested): closes their open breaches.
108
- * The unused `_now` parameter is optional since 2.9.1 and is removed in 3.0.
109
107
  */
110
- export declare function recordRepair(p: Person, victimId: string, _now?: Minute): number;
108
+ export declare function recordRepair(p: Person, victimId: string): number;
111
109
  /** Felt weight of open breaches, saturating: 1 - Π(1 - weight). Input for guilt in affect. */
112
110
  export declare function guiltLoad(p: Person): Unit;
@@ -278,7 +278,7 @@ function breachId(p, at, normId) {
278
278
  function boundBreaches(c) {
279
279
  while (c.breaches.length > CONSCIENCE_DEFAULTS.maxBreaches) {
280
280
  const i = c.breaches.findIndex((b) => b.repaired);
281
- c.breaches.splice(i >= 0 ? i : 0, 1);
281
+ c.breaches.splice(Math.max(i, 0), 1);
282
282
  }
283
283
  }
284
284
  /**
@@ -358,9 +358,8 @@ export function recordDeed(p, aff, intention, now, completed, opts = {}) {
358
358
  * The person repents a breach: regret, stopping and resolve. When another person was wronged,
359
359
  * restitution is also required, so the breach closes only after `recordRepair` for that victim.
360
360
  * Returns whether the breach is now closed. This records the person's acts, not acceptance.
361
- * The unused `_now` parameter is optional since 2.9.1 and is removed in 3.0.
362
361
  */
363
- export function repent(p, id, _now) {
362
+ export function repent(p, id) {
364
363
  const breach = p.conscience.breaches.find((b) => b.id === id);
365
364
  if (!breach)
366
365
  return false;
@@ -371,9 +370,8 @@ export function repent(p, id, _now) {
371
370
  }
372
371
  /**
373
372
  * Restitution/apology delivered to a wronged person (host-attested): closes their open breaches.
374
- * The unused `_now` parameter is optional since 2.9.1 and is removed in 3.0.
375
373
  */
376
- export function recordRepair(p, victimId, _now) {
374
+ export function recordRepair(p, victimId) {
377
375
  let count = 0;
378
376
  for (const b of p.conscience.breaches) {
379
377
  if (!b.repaired && b.victimId === victimId) {
@@ -39,7 +39,8 @@
39
39
  * selection nor repetition rules; the host still owns the relay graph and disclosure. It does not establish
40
40
  * that an origin is truthful, expose deceit to the listener, or infer who else heard a claim.
41
41
  */
42
- import { clamp01, expit, round } from '../core/index.js';
42
+ import { clamp01, round, sigmoid } from '../core/index.js';
43
+ import { nameOf as listenerNameOf } from '../narrate/lexicon.js';
43
44
  import { isDeceased, isUnfavourableTrait, parseTraitProp, REPUTATION_TRAITS, relationshipWith, } from '../social/index.js';
44
45
  import { MINUTES_PER_DAY } from '../types.js';
45
46
  export const CONVERSATION_DEFAULTS = {
@@ -156,7 +157,7 @@ export function converse(speaker, listener, ctx) {
156
157
  const subject = b.prop.split(':')[0];
157
158
  if (subject === speaker.id)
158
159
  continue;
159
- const cred = expit(b.logOdds);
160
+ const cred = sigmoid(b.logOdds);
160
161
  const threshold = Number.isFinite(ctx.confidentMin) &&
161
162
  ctx.confidentMin >= 0 &&
162
163
  ctx.confidentMin <= 0.5
@@ -180,7 +181,7 @@ export function converse(speaker, listener, ctx) {
180
181
  // Temptations: claims the speaker would gain from the listener believing.
181
182
  for (const t of [...(ctx.temptations ?? [])].sort((a, b) => (a.prop < b.prop ? -1 : 1))) {
182
183
  const b = speaker.memory.beliefs.find((x) => x.prop === t.prop);
183
- const cred = b ? expit(b.logOdds) : 0.5;
184
+ const cred = b ? sigmoid(b.logOdds) : 0.5;
184
185
  const forValue = t.value ? cred : 1 - cred;
185
186
  const gain = clamp01(t.gain);
186
187
  if (forValue >= 0.5) {
@@ -233,7 +234,7 @@ export function converse(speaker, listener, ctx) {
233
234
  // deed (counted above) but yields no percept, so the same gossip retold at every tea is not judged afresh.
234
235
  const heard = listener.memory.beliefs.find((x) => x.prop === c.prop);
235
236
  if (heard) {
236
- const cred = expit(heard.logOdds);
237
+ const cred = sigmoid(heard.logOdds);
237
238
  if ((c.value ? cred : 1 - cred) >= c.confidence)
238
239
  continue;
239
240
  }
@@ -244,10 +245,10 @@ export function converse(speaker, listener, ctx) {
244
245
  actorId: speaker.id,
245
246
  salience: round(0.3 + 0.4 * c.confidence),
246
247
  claims: [{ prop: c.prop, value: c.value, confidence: c.confidence }],
247
- summary: speaker.lexicon?.plainEnglish
248
+ summary: listener.lexicon?.plainEnglish
248
249
  ? c.about !== undefined
249
- ? `${nameOf(speaker)} says ${speaker.lexicon.names?.[c.about] ?? c.about} ${c.prop.split(':')[1] === 'owes' ? `${c.value ? 'owes' : 'does not owe'} money to ${speaker.lexicon.names?.[c.prop.split(':').slice(2).join(':')] ?? c.prop.split(':').slice(2).join(':')}` : `is ${c.value ? '' : 'not '}${c.prop.split(':')[1] ?? ''}`}.`
250
- : `${nameOf(speaker)} says this is ${c.value ? 'true' : 'false'}: ${c.prop}.`
250
+ ? `${listener.lexicon.names?.[speaker.id] ?? nameOf(speaker)} says ${listenerNameOf(listener, c.about)} ${c.prop.split(':')[1] === 'owes' ? `${c.value ? 'owes' : 'does not owe'} money to ${listenerNameOf(listener, c.prop.split(':').slice(2).join(':'))}` : `is ${c.value ? '' : 'not '}${c.prop.split(':')[1] ?? ''}`}.`
251
+ : `${listener.lexicon.names?.[speaker.id] ?? nameOf(speaker)} says this is ${c.value ? 'true' : 'false'}: ${c.prop}.`
251
252
  : c.about !== undefined
252
253
  ? `${nameOf(speaker)} says ${c.about} is ${c.value ? '' : 'not '}${c.prop.split(':')[1] ?? ''}`
253
254
  : `${nameOf(speaker)} says ${c.prop} is ${c.value ? 'so' : 'not so'}`,
@@ -280,7 +281,7 @@ export function converse(speaker, listener, ctx) {
280
281
  for (const b of beliefs) {
281
282
  if (!b.prop.startsWith(prefix))
282
283
  continue;
283
- const cred = expit(b.logOdds);
284
+ const cred = sigmoid(b.logOdds);
284
285
  if (cred < C.adviceMin)
285
286
  continue;
286
287
  const strength = clamp01((cred - 0.5) * 2 * (0.3 + 0.7 * care) * (0.5 + benevolence));
@@ -297,8 +298,8 @@ export function converse(speaker, listener, ctx) {
297
298
  valence: 0.1,
298
299
  salience: round(0.4 + 0.3 * a.strength),
299
300
  claims: [{ prop: `advice:${a.action}`, value: true, confidence: round(a.cred) }],
300
- summary: speaker.lexicon?.plainEnglish
301
- ? `${nameOf(speaker)} suggests this action: ${a.action.replace(/[-_]/g, ' ')}.`
301
+ summary: listener.lexicon?.plainEnglish
302
+ ? `${listener.lexicon.names?.[speaker.id] ?? nameOf(speaker)} suggests this action: ${a.action.replace(/[-_]/g, ' ')}.`
302
303
  : `${nameOf(speaker)} says I should ${a.action}`,
303
304
  };
304
305
  if (ctx.provenance) {
@@ -1,4 +1,3 @@
1
- export * from './keys.ts';
2
1
  export * from './libm.ts';
3
2
  export * from './math.ts';
4
3
  export * from './random.ts';
@@ -1,4 +1,3 @@
1
- export * from './keys.js';
2
1
  export * from './libm.js';
3
2
  export * from './math.js';
4
3
  export * from './random.js';
@@ -5,8 +5,6 @@ export declare const clampSigned: (value: number) => number;
5
5
  export declare const lerp: (a: number, b: number, t: number) => number;
6
6
  export declare const sigmoid: (x: number) => number;
7
7
  export declare const logit: (p: number) => number;
8
- /** @deprecated since 2.9.1, removed in 3.0; use sigmoid */
9
- export declare const expit: typeof sigmoid;
10
8
  /** Exponential decay of `value` toward `target` over `dt` minutes with the given half-life. */
11
9
  export declare function decay(value: number, dt: number, halfLife: number, target?: number): number;
12
10
  export declare const minuteOfDay: (now: number) => number;
package/dist/core/math.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { MINUTES_PER_DAY } from '../types.js';
2
2
  import { dexp, dlog, dpow } from './libm.js';
3
3
  /** Clamp into [min, max]. NaN maps to `min`, so one bad host number cannot propagate through state. */
4
- export const clamp = (value, min, max) => Number.isNaN(value) ? min : value < min ? min : value > max ? max : value;
4
+ export const clamp = (value, min, max) =>
5
+ // biome-ignore lint/complexity/useMathMinMax: Comparisons preserve signed zero and NaN bounds; Math.min/max change them.
6
+ Number.isNaN(value) ? min : value < min ? min : value > max ? max : value;
5
7
  export const clamp01 = (value) => clamp(value, 0, 1);
6
8
  export const clampSigned = (value) => clamp(value, -1, 1);
7
9
  /** A finite number. Restore sanitizers use it. @internal */
@@ -36,8 +38,6 @@ export const logit = (p) => {
36
38
  const q = clamp(p, 1e-6, 1 - 1e-6);
37
39
  return dlog(q / (1 - q));
38
40
  };
39
- /** @deprecated since 2.9.1, removed in 3.0; use sigmoid */
40
- export const expit = sigmoid;
41
41
  /** Exponential decay of `value` toward `target` over `dt` minutes with the given half-life. */
42
42
  export function decay(value, dt, halfLife, target = 0) {
43
43
  if (halfLife <= 0)
@@ -40,11 +40,6 @@ export declare function conceptionChance(mother: Person, father: Person, days: n
40
40
  * median (an engineering bound against extreme draws). Returns the pregnancy, or undefined if one is already open.
41
41
  */
42
42
  export declare function conceive(mother: Person, father: Person | PersonId, seed: number, at: Minute): Pregnancy | undefined;
43
- /** @deprecated since 2.1: pass `at` (and the optional knobs) positionally. Removed in 3.0. */
44
- export declare function conceive(mother: Person, father: Person | PersonId, opts: {
45
- at: Minute;
46
- seed: number;
47
- }): Pregnancy | undefined;
48
43
  /**
49
44
  * Weeks since conception (0 when not pregnant). Counted from conception, not from the last menstrual period, which
50
45
  * is where obstetric weeks and trimesters are conventionally counted from (about two weeks earlier). The second
@@ -161,19 +161,25 @@ export function conceptionChance(mother, father, days) {
161
161
  return 0;
162
162
  return clamp01(1 - dpow(1 - c, days / FAMILY_DEFAULTS.cycleDays));
163
163
  }
164
- export function conceive(mother, father, seedOrOpts, when) {
165
- const opts = typeof seedOrOpts === 'number' ? { seed: seedOrOpts, at: when } : seedOrOpts;
164
+ /**
165
+ * Record that `mother` has conceived by `father` at `at`. The due date is drawn from a stream derived from `seed`;
166
+ * `seed` also becomes the child's seed at delivery. The drawn length is clamped to 4 sd below and 3 sd above the
167
+ * median (an engineering bound against extreme draws). Returns the pregnancy, or undefined if one is already open.
168
+ */
169
+ export function conceive(mother, father, seed, at) {
170
+ if (!Number.isFinite(at))
171
+ throw new TypeError('conceive requires a finite positional time');
166
172
  const fam = familyOf(mother);
167
173
  if (fam.pregnancy)
168
174
  return undefined;
169
- const rng = createRng((opts.seed ^ 0x9e3779b9) >>> 0);
175
+ const rng = createRng((seed ^ 0x9e3779b9) >>> 0);
170
176
  const F = FAMILY_DEFAULTS;
171
177
  const days = clamp(normal(rng, F.gestationDays, F.gestationSdDays), F.gestationDays - 4 * F.gestationSdDays, F.gestationDays + 3 * F.gestationSdDays);
172
178
  const pregnancy = {
173
179
  fatherId: typeof father === 'string' ? father : father.id,
174
- conceivedAt: opts.at,
175
- dueAt: opts.at + Math.round(days * MINUTES_PER_DAY),
176
- seed: opts.seed,
180
+ conceivedAt: at,
181
+ dueAt: at + Math.round(days * MINUTES_PER_DAY),
182
+ seed,
177
183
  };
178
184
  fam.pregnancy = pregnancy;
179
185
  return { ...pregnancy };
@@ -6,14 +6,9 @@ export declare function outwardSigns(target: Person): OutwardSigns;
6
6
  /** What `target` says of themself: words hide more than faces (`wordsReserve`), and a limp is not mentioned. */
7
7
  export declare function selfReport(target: Person): OutwardSigns;
8
8
  /** `observer` looks at `target` at `at` (clarity 0..1: a clear look near by, or a distant figure; default 1). */
9
- export declare function glimpseOf(observer: Person, target: Person, at: Minute, clarity?: Unit): void;
10
- /** @deprecated since 2.1: pass `at` (and the optional knobs) positionally. Removed in 3.0. */
11
- export declare function glimpseOf(observer: Person, target: Person, opts: {
12
- at: Minute;
13
- clarity?: Unit;
14
- }): void;
9
+ export declare function glimpsePerson(observer: Person, target: Person, at: Minute, clarity?: Unit): void;
15
10
  /** `observer` has known `target` for a long time (familiarity 0..1): traits and family ties, condensed. */
16
- export declare function acquaintWith(observer: Person, target: Person, familiarity: Unit, at: Minute): void;
11
+ export declare function acquaintPerson(observer: Person, target: Person, familiarity: Unit, at: Minute): void;
17
12
  /** How sure the observer is of the things that drive an answer: traits and the state cues, 0..1. */
18
13
  export declare function impressionConfidence(observer: Person, targetId: PersonId, now: Minute): Unit;
19
14
  /**