@adam0white/human-framework 2.21.0 → 2.23.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`, currently `2.21.0`) and `FRAMEWORK_VERSION`, the same
225
+ - **The release version** is the package version (`package.json`, currently `2.23.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,15 +236,19 @@ 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)
239
+ ## Observation and inherited aptitude
240
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.
241
+ `observeSkill(p, skill, minutes, modelLevel, domain, { inheritedAptitude: true })` applies the person's existing inherited skill factor to observation. An attended `Percept.demonstrates` can declare the same flag. Only literal `true` enables it. Without it, watching keeps its previous rate, including for people whose saved family already has aptitudes. This adds no Person state or RNG draw and does not change direct practice. Save the host's observation inputs and pin the package when opting in.
242
+
243
+ ## Optional felt body (2.21 release)
244
+
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 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.
242
246
 
243
247
  `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
248
 
245
249
  `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
250
 
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.
251
+ 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. The bodily options were introduced in HF 2.21.0. Site games pin their adopted package separately from the current library; upgrading the library does not enable these faculties or silently change game runtime.
248
252
 
249
253
  ## Long runs (2.0)
250
254
 
@@ -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) {
@@ -1,3 +1,44 @@
1
+ /**
2
+ * SCOPE: one conversation turn between two people: what the speaker tells the listener, drawn from the
3
+ * speaker's own confident beliefs, and what advice they give, drawn from the speaker's beliefs about what the
4
+ * listener should do. Output is plain data: `told` percepts (testimony with claims at the speaker's credence)
5
+ * and advice percepts that the host delivers through the listener's `perceive`, a `Suggestion` per piece of
6
+ * advice for the listener's next decision, and social events for both sides. Reputation is nothing more than
7
+ * these claims spreading belief by belief (`social.applyReputationBelief` turns a changed character belief
8
+ * into a changed relationship).
9
+ *
10
+ * Choice of what to say is deterministic and explainable: each candidate claim gets a relevance score
11
+ * (confidence, recency, host topics, the speaker's feeling about its subject); an unfavourable claim about an
12
+ * absent third party is tagged with the catalog norm 'backbiting' (Qur'an 49:12, as catalogued in
13
+ * `conscience/catalog.ts`) and costs the speaker in proportion to their OWN held understanding of that norm and
14
+ * their honesty-humility, so a speaker who holds it firmly withholds it and one who does not tells it. The
15
+ * definition used here (an unfavourable character claim about someone not present) is an engineering
16
+ * assumption for the tag, not a ruling on what counts as backbiting. Deceit happens only when the host offers a
17
+ * temptation (the speaker gains if the listener believes something the speaker does not): temptation scales
18
+ * with (1 − honesty-humility) and is opposed by the speaker's held 'lying' norm. The lie is a host-visible flag
19
+ * (`ToldClaim.deceit`); the listener cannot see it. Named shapes: honesty-humility as the HEXACO factor most
20
+ * predictive of lower dishonesty (Hilbig & Zettler 2009; Heck et al. 2018 meta-analysis), gossip as a large
21
+ * share of conversation about absent third parties that spreads reputational information (Dunbar 2004), and
22
+ * extraversion as talkativeness (how many claims are offered).
23
+ *
24
+ * SCOPE (host options): `loyalWarning` admits only unfavourable listener claims when affection is at least
25
+ * 0.5 (an engineering threshold). `confidentMin` sets a per-call relay threshold. `phrase` renders selected
26
+ * testimony only; the host owns authored facts, delivery and subsequent disclosure. No hidden relay occurs.
27
+ *
28
+ * It does NOT generate language, model turn-taking, persuasion beyond source trust, what the speaker believes
29
+ * the listener already knows, lies of omission, white lies told to spare feelings, or the speaker's fear of
30
+ * being found out. Coefficients are engineering defaults, not calibrated estimates. Nothing here judges the
31
+ * worth of either person; norms are the people's own understandings with catalog provenance.
32
+ * Repetition (integration, 2026-10-03): a claim the listener already holds at the told confidence produces no
33
+ * percept (it is in `claims`, not `told`). A claim this listener already heard from this speaker, in the same
34
+ * direction, is dropped before the speaker chooses, so it is not even a deed. The speaker keeps no memory of what was said to whom; the listener's
35
+ * belief stands in for it.
36
+ *
37
+ * SCOPE (Word and witness, 2.10.0): `provenance: true` carries each claim's first known origin and opaque
38
+ * lineage into testimony. Missing provenance names the speaker as origin. Metadata changes neither claim
39
+ * selection nor repetition rules; the host still owns the relay graph and disclosure. It does not establish
40
+ * that an origin is truthful, expose deceit to the listener, or infer who else heard a claim.
41
+ */
1
42
  import { type TraitEffect } from '../social/index.ts';
2
43
  import type { EntityId, Minute, NormTag, Percept, Person, PersonId, SocialEvent, Suggestion, Unit } from '../types.ts';
3
44
  export declare const CONVERSATION_DEFAULTS: {
@@ -40,6 +40,7 @@
40
40
  * that an origin is truthful, expose deceit to the listener, or infer who else heard a claim.
41
41
  */
42
42
  import { clamp01, expit, round } 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 = {
@@ -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'}`,
@@ -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) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.21.0";
2
+ export declare const FRAMEWORK_VERSION = "2.23.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';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export const FRAMEWORK_VERSION = '2.21.0';
2
+ export const FRAMEWORK_VERSION = '2.23.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';
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.21.0': {
45
+ to: '2.23.0',
46
+ note: 'shape unchanged; doze delivery and plain narration fixes apply to opted-in runs',
47
+ apply: stamp,
48
+ },
44
49
  '2.17.0': {
45
50
  to: '2.21.0',
46
51
  note: 'doze and bladder state start absent; old callers retain their continuation',
@@ -332,7 +332,9 @@ export function describePerson(p, lexicon) {
332
332
  ? `${p.activity.action}${p.activity.targetId ? ` (${p.activity.targetId})` : ''}`
333
333
  : phraseLine('describe.idle', {}, lex);
334
334
  if (lex?.plainEnglish) {
335
- const state = bits.length > 0 ? bits.join(', ') : phraseLine('describe.well', {}, lex);
335
+ const state = bits.length > 1
336
+ ? `${bits.slice(0, -1).join(', ')} and ${bits.at(-1)}`
337
+ : (bits[0] ?? phraseLine('describe.well', {}, lex));
336
338
  const activity = p.activity
337
339
  ? `${p.name} is doing this: ${p.activity.affordance.label}${p.activity.targetId ? ` (${nameOf(p, p.activity.targetId, lex)})` : ''}.`
338
340
  : `${p.name} is ${phraseLine('describe.idle', {}, lex)}.`;
@@ -1,6 +1,6 @@
1
1
  /**
2
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,
3
+ * Owns no state or formula. This host composition handles observed-trait correction: direct observation corrects the belief,
4
4
  * recalibrates its sources and applies the credence change to the subject's relationship. It does
5
5
  * not infer opposite traits, accuse a source of lying, run attention or record a percept. Hosts
6
6
  * supply only observations their person has access to. Existing callers remain unchanged.
package/dist/person.d.ts CHANGED
@@ -196,7 +196,11 @@ export interface BeginOptions {
196
196
  /** Joint-activity proposal id (set by `sim/` when both partners begin together). */
197
197
  jointId?: string;
198
198
  }
199
- /** Start an activity chosen by `record`. Returns null when the person is dead. */
199
+ /**
200
+ * Start an activity chosen by `record`. Returns null when dead, or when dozed work
201
+ * is unresolved. Call `finish` for the stopped activity before starting any replacement,
202
+ * including voluntary sleep or an activity chosen after a waking percept.
203
+ */
200
204
  export declare function begin(p: Person, aff: Affordance, record: DecisionRecord, opts?: BeginOptions): Activity | null;
201
205
  /**
202
206
  * Push the review time forward after a decision to continue the current activity, refresh the cached
@@ -229,9 +233,13 @@ export declare function learningFor(p: Person, domain?: LearningDomain): number;
229
233
  /**
230
234
  * Watch someone practise skill `skill` at `modelLevel` for `minutes` (observational learning, 1.8.0): the
231
235
  * composite's side of `skills.learnByWatching` at the person's age-and-domain learning rate. A `Percept.demonstrates` on an
232
- * attended percept does the same. Returns the level before and after.
236
+ * attended percept does the same. `inheritedAptitude: true` applies the family's existing aptitude factor to
237
+ * watching. Absent or false keeps earlier observation learning, including for a person with an authored family.
238
+ * This changes no practice rule, draws no RNG, and stores no new state. Returns the level before and after.
233
239
  */
234
- export declare function observeSkill(p: Person, skill: string, minutes: number, modelLevel: Unit, domain?: LearningDomain): {
240
+ export declare function observeSkill(p: Person, skill: string, minutes: number, modelLevel: Unit, domain?: LearningDomain, options?: {
241
+ inheritedAptitude?: boolean;
242
+ }): {
235
243
  before: Unit;
236
244
  after: Unit;
237
245
  };
package/dist/person.js CHANGED
@@ -748,7 +748,8 @@ function perceiveOne(p, pc, options) {
748
748
  // Watching someone skilled at work teaches a little (1.8.0, observational learning).
749
749
  const demo = pc.demonstrates;
750
750
  if (demo && !byMe && demo.minutes > 0) {
751
- learnByWatching(p, demo.skill, demo.minutes, demo.level, learningFor(p, demo.domain), p.now);
751
+ const learning = learningFor(p, demo.domain);
752
+ learnByWatching(p, demo.skill, demo.minutes, demo.level, demo.inheritedAptitude === true ? learning * aptitudeOf(p, demo.skill) : learning, p.now);
752
753
  }
753
754
  // Relationships: interactions I took part in.
754
755
  const social = socialKindOf(pc.kind);
@@ -864,17 +865,18 @@ function judgeDeed(p, actor, pc, at) {
864
865
  export function perceive(p, percepts, options = {}) {
865
866
  if (percepts.length === 0 || !p.body.alive)
866
867
  return [];
867
- if (p.body.doze?.since !== undefined) {
868
+ const dozeSince = p.body.doze?.since;
869
+ if (dozeSince !== undefined) {
868
870
  const wakes = percepts.some((pc) => Number.isFinite(pc.at) &&
869
- pc.at === p.now &&
871
+ pc.at >= dozeSince &&
872
+ pc.at <= p.now &&
870
873
  Number.isFinite(pc.salience) &&
871
874
  pc.salience >= DOZE_DEFAULTS.wakeSalience &&
872
875
  pc.near !== false &&
873
876
  (pc.targetId === p.id || pc.near === true) &&
874
877
  (pc.channel === 'heard' || pc.channel === 'felt' || pc.channel === 'social'));
875
- if (!wakes)
876
- return [];
877
- wakeDoze(p, p.now);
878
+ if (wakes)
879
+ wakeDoze(p, p.now);
878
880
  }
879
881
  const body = readBodyOf(p);
880
882
  const attended = attend(p, [...percepts], {
@@ -1169,9 +1171,13 @@ function thresholdFor(p, load, review) {
1169
1171
  const t = nextBodyThreshold(p, load, segmentModifiers(p), PERSON_DEFAULTS.interruptThresholds, review, p.ambient ? ambientBodyParams(p, BODY_DEFAULTS) : BODY_DEFAULTS, fastingCtx(p));
1170
1172
  return Number.isFinite(t) ? p.now + t : undefined;
1171
1173
  }
1172
- /** Start an activity chosen by `record`. Returns null when the person is dead. */
1174
+ /**
1175
+ * Start an activity chosen by `record`. Returns null when dead, or when dozed work
1176
+ * is unresolved. Call `finish` for the stopped activity before starting any replacement,
1177
+ * including voluntary sleep or an activity chosen after a waking percept.
1178
+ */
1173
1179
  export function begin(p, aff, record, opts = {}) {
1174
- if (!p.body.alive)
1180
+ if (!p.body.alive || p.activity?.dozedAt !== undefined)
1175
1181
  return null;
1176
1182
  if (p.body.doze?.since !== undefined && aff.mode !== 'sleep')
1177
1183
  return null;
@@ -1346,10 +1352,13 @@ export function learningFor(p, domain) {
1346
1352
  /**
1347
1353
  * Watch someone practise skill `skill` at `modelLevel` for `minutes` (observational learning, 1.8.0): the
1348
1354
  * composite's side of `skills.learnByWatching` at the person's age-and-domain learning rate. A `Percept.demonstrates` on an
1349
- * attended percept does the same. Returns the level before and after.
1355
+ * attended percept does the same. `inheritedAptitude: true` applies the family's existing aptitude factor to
1356
+ * watching. Absent or false keeps earlier observation learning, including for a person with an authored family.
1357
+ * This changes no practice rule, draws no RNG, and stores no new state. Returns the level before and after.
1350
1358
  */
1351
- export function observeSkill(p, skill, minutes, modelLevel, domain) {
1352
- return learnByWatching(p, skill, minutes, modelLevel, learningFor(p, domain), p.now);
1359
+ export function observeSkill(p, skill, minutes, modelLevel, domain, options) {
1360
+ const learning = learningFor(p, domain);
1361
+ return learnByWatching(p, skill, minutes, modelLevel, options?.inheritedAptitude === true ? learning * aptitudeOf(p, skill) : learning, p.now);
1353
1362
  }
1354
1363
  /** Realized need changes, using readings captured before and after the outcome's deltas. */
1355
1364
  function realizedNeedDeltas(act, given, before, after) {
package/dist/persona.js CHANGED
@@ -188,6 +188,7 @@ export function validatePersonaProjection(input) {
188
188
  keys(input.source, ['schema', 'engine', 'id', 'now']);
189
189
  // The 2.14.1 and 2.17.0 portable fields retain their semantics across the bodily opt-ins.
190
190
  const compatibleEngine = input.source.engine === ENGINE_VERSION ||
191
+ input.source.engine === '2.21.0' ||
191
192
  input.source.engine === '2.17.0' ||
192
193
  input.source.engine === '2.14.1';
193
194
  if (input.source.schema !== PERSON_SCHEMA || !compatibleEngine)
@@ -68,6 +68,7 @@ export function reasonLine(reason, lexicon) {
68
68
  'need:water': 'I need to drink first.',
69
69
  'need:sleep': 'I need to sleep first.',
70
70
  'need:rest': 'I need to rest first.',
71
+ 'need:bladder': 'I need to use the toilet first.',
71
72
  'need:survival': 'I need to meet an urgent bodily need first.',
72
73
  };
73
74
  if (Object.hasOwn(lines, reason))
@@ -360,7 +360,7 @@ export function nafsPatterns(p, inputs) {
360
360
  ref: 'Qur’an 12:53',
361
361
  evident: assentedAgainstNorm.length > 0,
362
362
  evidence: p.lexicon?.plainEnglish
363
- ? `The person acted on ${assentedAgainstNorm.length} recent urges despite a rule they hold.`
363
+ ? `The person acted on ${assentedAgainstNorm.length} recent ${assentedAgainstNorm.length === 1 ? 'urge' : 'urges'} despite a rule they hold.`
364
364
  : `${assentedAgainstNorm.length} recent prompts followed against a held norm`,
365
365
  },
366
366
  {
@@ -376,7 +376,7 @@ export function nafsPatterns(p, inputs) {
376
376
  ref: 'Qur’an 89:27',
377
377
  evident: letPass > 0 && assentedAgainstNorm.length === 0 && !reproach,
378
378
  evidence: p.lexicon?.plainEnglish
379
- ? `${letPass} recent urges passed without action. ${assentedAgainstNorm.length} were acted on against a held rule. Current guilt or unresolved breach: ${reproach ? 'yes' : 'no'}.`
379
+ ? `${letPass} recent ${letPass === 1 ? 'urge' : 'urges'} passed without action. ${assentedAgainstNorm.length} ${assentedAgainstNorm.length === 1 ? 'was' : 'were'} acted on against a held rule. Current guilt or unresolved breach: ${reproach ? 'yes' : 'no'}.`
380
380
  : `${letPass} recent prompts let pass, none followed against a norm, no felt reproach`,
381
381
  },
382
382
  ];
package/dist/qalb.d.ts CHANGED
@@ -1,15 +1,3 @@
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
1
  import type { Person } from './types.ts';
14
2
  export interface QalbDescription {
15
3
  facet: 'attention' | 'held-norm' | 'mood' | 'trust';
package/dist/qalb.js CHANGED
@@ -1,3 +1,18 @@
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 { DEFAULT_NORMS } from './conscience/catalog.js';
14
+ import { nameOf } from './narrate/lexicon.js';
15
+ import { normLabel } from './presentation.js';
1
16
  /**
2
17
  * Plain English descriptions; no scalar summary, comparison or hidden update.
3
18
  * Hosts must not present these records as spiritual health or a religious assessment.
@@ -19,18 +34,17 @@ export function qalbReading(p) {
19
34
  if (!p.conscience.norms.length)
20
35
  add('held-norm', 'unrecorded', 'Their records contain no held rules.', ['conscience.norms']);
21
36
  for (const n of p.conscience.norms) {
37
+ const rule = normLabel(DEFAULT_NORMS.find((rule) => rule.id === n.normId) ?? { id: n.normId, label: n.normId }, { ...p.lexicon, plainEnglish: true });
22
38
  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']);
39
+ ? `They have a recorded rule, “${rule}”, with no stated conviction.`
40
+ : `They hold an understanding of the rule “${rule}”. This is their view.`, [`norm:${n.normId}`, `standing:${n.standing}`, 'conscience.norms.conviction']);
25
41
  }
26
42
  add('mood', p.affect.mood.valence < 0 ? 'unpleasant' : p.affect.mood.valence > 0 ? 'pleasant' : 'neutral', p.affect.mood.valence < 0
27
43
  ? 'Their recorded mood feels unpleasant.'
28
44
  : p.affect.mood.valence > 0
29
45
  ? 'Their recorded mood feels pleasant.'
30
46
  : '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']);
47
+ add('mood', p.affect.mood.arousal === 0 ? 'no-arousal' : 'some-arousal', p.affect.mood.arousal === 0 ? 'Their recorded mood is calm.' : 'Their recorded mood is stirred.', ['affect.mood.arousal']);
34
48
  // Keep stores distinct. Do not substitute relationship trust for source or advising-voice trust.
35
49
  const ties = p.social.relationships.map((r) => ({
36
50
  store: 'social.relationships',
@@ -53,10 +67,10 @@ export function qalbReading(p) {
53
67
  for (const t of trusts) {
54
68
  // 0.5 is the numeric range midpoint, not a faith threshold or personal baseline.
55
69
  const subject = t.store === 'social.relationships'
56
- ? 'this person'
70
+ ? `the person ${nameOf(p, t.id)}`
57
71
  : t.store === 'will.voices'
58
- ? 'this advising voice'
59
- : 'this information source';
72
+ ? `the advising voice ${nameOf(p, t.id)}`
73
+ : `the information source ${nameOf(p, t.id)}`;
60
74
  add('trust', t.trust < 0.5 ? 'below-midpoint' : t.trust > 0.5 ? 'above-midpoint' : 'at-midpoint', t.trust < 0.5
61
75
  ? `Their trust in ${subject} is below the middle of its range.`
62
76
  : t.trust > 0.5
package/dist/sim/sim.js CHANGED
@@ -840,10 +840,15 @@ export function stepCommunity(c, world, until, opts = {}) {
840
840
  at: t,
841
841
  personId: p.id,
842
842
  kind: 'finish',
843
- detail: `${act.action} interrupted by doze`,
843
+ detail: outcome.summary ??
844
+ (p.lexicon?.plainEnglish
845
+ ? `${p.name} stopped the task “${act.affordance.label}” because they dozed off.`
846
+ : `${act.action} interrupted by doze`),
847
+ decisionId: act.decisionId,
848
+ affordanceId: act.affordanceId,
844
849
  action: act.action,
845
850
  status: outcome.status,
846
- });
851
+ }, outcome.summary !== undefined);
847
852
  }
848
853
  c.idleUntil[p.id] = t + quantum;
849
854
  if (!ordinaryDue)
@@ -995,12 +1000,12 @@ export function stepCommunity(c, world, until, opts = {}) {
995
1000
  detail: p.lexicon?.plainEnglish
996
1001
  ? proposal.status === 'declined'
997
1002
  ? `The shared task “${chosen.label}” could not start because a partner was unavailable.`
998
- : `${chosen.label} with ${proposal.partnerIds.map((id) => personById(c, id)?.name ?? id).join(', ')}`
1003
+ : `${p.name} proposed the shared task “${chosen.label}” with ${proposal.partnerIds.map((id) => personById(c, id)?.name ?? id).join(', ')}.`
999
1004
  : `${chosen.label} with ${proposal.partnerIds.join(', ')}`,
1000
1005
  affordanceId: chosen.id,
1001
1006
  action: chosen.action,
1002
1007
  jointId: proposal.id,
1003
- }, Boolean(p.lexicon?.plainEnglish && proposal.status === 'declined'));
1008
+ }, Boolean(p.lexicon?.plainEnglish));
1004
1009
  continue;
1005
1010
  }
1006
1011
  if (act) {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * SCOPE: opt-in host input ordering for one declared logical tick. Waits for the
3
+ * caller's complete expected identity set, then sorts by sender and sequence.
4
+ * Lexical sender priority is deterministic host policy, not fairness or
5
+ * consensus. For conflicting same-voice requests, earlier senders take priority.
6
+ * Owns no Person, clock, queue or RNG state. Does not supply transport, consensus,
7
+ * payload validation, retries or snapshot storage. Payloads remain caller-owned;
8
+ * hosts must agree on their meaning and save pending input alongside host state.
9
+ */
10
+ export interface TickInputKey {
11
+ sender: string;
12
+ sequence: number;
13
+ }
14
+ export interface TickInput<T> extends TickInputKey {
15
+ tick: number;
16
+ payload: T;
17
+ }
18
+ /**
19
+ * Returns undefined while a declared input is missing. Unexpected, duplicate or
20
+ * wrong-tick inputs throw. An empty expected set is a complete barrier. This
21
+ * reader never mutates input arrays or envelopes and never reads payload fields.
22
+ * Call only when explicitly opting into this host scheduling convention.
23
+ */
24
+ export declare function canonicalTickInputs<T>(tick: number, inputs: readonly TickInput<T>[], expected: readonly TickInputKey[]): TickInput<T>[] | undefined;
@@ -0,0 +1,56 @@
1
+ const MAX_INPUTS = 256;
2
+ function identity(key) {
3
+ if (typeof key.sender !== 'string' ||
4
+ key.sender.length === 0 ||
5
+ key.sender.length > 128 ||
6
+ !Number.isSafeInteger(key.sequence) ||
7
+ key.sequence < 0) {
8
+ throw new Error('Invalid tick input identity');
9
+ }
10
+ return JSON.stringify([key.sender, key.sequence]);
11
+ }
12
+ /**
13
+ * Returns undefined while a declared input is missing. Unexpected, duplicate or
14
+ * wrong-tick inputs throw. An empty expected set is a complete barrier. This
15
+ * reader never mutates input arrays or envelopes and never reads payload fields.
16
+ * Call only when explicitly opting into this host scheduling convention.
17
+ */
18
+ export function canonicalTickInputs(tick, inputs, expected) {
19
+ if (!Number.isSafeInteger(tick) || tick < 0)
20
+ throw new Error('Invalid logical tick');
21
+ if (inputs.length > MAX_INPUTS || expected.length > MAX_INPUTS) {
22
+ throw new Error('Tick input limit exceeded');
23
+ }
24
+ const wanted = new Set();
25
+ for (const key of expected) {
26
+ const id = identity(key);
27
+ if (wanted.has(id))
28
+ throw new Error('Duplicate expected tick input');
29
+ wanted.add(id);
30
+ }
31
+ const received = new Set();
32
+ for (const input of inputs) {
33
+ const id = identity(input);
34
+ if (input.tick !== tick)
35
+ throw new Error('Input belongs to another tick');
36
+ if (!wanted.has(id))
37
+ throw new Error('Unexpected tick input');
38
+ if (received.has(id))
39
+ throw new Error('Duplicate tick input');
40
+ received.add(id);
41
+ }
42
+ if (received.size !== wanted.size)
43
+ return undefined;
44
+ return inputs
45
+ .map((input) => ({
46
+ tick: input.tick,
47
+ sender: input.sender,
48
+ sequence: input.sequence,
49
+ payload: input.payload,
50
+ }))
51
+ .sort((a, b) => {
52
+ if (a.sender !== b.sender)
53
+ return a.sender < b.sender ? -1 : 1;
54
+ return a.sequence - b.sequence;
55
+ });
56
+ }
package/dist/types.d.ts CHANGED
@@ -915,7 +915,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
915
915
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
916
916
  * `dutyReviewAt`). Person shape unchanged.
917
917
  */
918
- export declare const ENGINE_VERSION = "2.21.0";
918
+ export declare const ENGINE_VERSION = "2.23.0";
919
919
  /** A pregnancy carried by this person (host-driven; see `family/`). */
920
920
  export interface Pregnancy {
921
921
  fatherId: PersonId;
@@ -1301,6 +1301,8 @@ export interface Percept {
1301
1301
  level: Unit;
1302
1302
  minutes: number;
1303
1303
  domain?: LearningDomain;
1304
+ /** Apply the watcher's inherited skill aptitude. Absent keeps earlier observation learning exactly. */
1305
+ inheritedAptitude?: boolean;
1304
1306
  };
1305
1307
  /** At most four additions/withdrawals combined; attended external testimony to this person or a broadcast. */
1306
1308
  consequences?: readonly ReceivedConsequence[];
package/dist/types.js CHANGED
@@ -120,4 +120,4 @@ export const PERSON_SCHEMA = 'human/person@1';
120
120
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
121
121
  * `dutyReviewAt`). Person shape unchanged.
122
122
  */
123
- export const ENGINE_VERSION = '2.21.0';
123
+ export const ENGINE_VERSION = '2.23.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "2.21.0",
3
+ "version": "2.23.0",
4
4
  "description": "Deterministic, explainable simulated people: body, needs, affect, memory, beliefs, skills, habits, relationships, values and a will that can refuse a suggestion",
5
5
  "keywords": [
6
6
  "simulation",