@adam0white/human-framework 2.18.0 → 2.20.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 (49) hide show
  1. package/README.md +51 -0
  2. package/dist/agenda/agenda.d.ts +47 -0
  3. package/dist/agenda/agenda.js +4 -1
  4. package/dist/body/body.d.ts +10 -0
  5. package/dist/body/body.js +15 -0
  6. package/dist/chronicle/story.js +3 -2
  7. package/dist/cognition/cognition.d.ts +37 -0
  8. package/dist/cognition/cognition.js +3 -2
  9. package/dist/conversation/conversation.js +10 -4
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.js +1 -0
  12. package/dist/core/keys.d.ts +6 -0
  13. package/dist/core/keys.js +8 -0
  14. package/dist/family/family.js +5 -5
  15. package/dist/index.d.ts +4 -1
  16. package/dist/index.js +4 -1
  17. package/dist/longrun.js +1 -1
  18. package/dist/memory/memory.d.ts +26 -0
  19. package/dist/memory/memory.js +3 -9
  20. package/dist/narrate/lexicon.d.ts +2 -1
  21. package/dist/narrate/lexicon.js +2 -1
  22. package/dist/narrate/narrate.d.ts +14 -1
  23. package/dist/narrate/narrate.js +81 -39
  24. package/dist/narrate/plain.d.ts +7 -0
  25. package/dist/narrate/plain.js +293 -0
  26. package/dist/observed-reputation.d.ts +4 -0
  27. package/dist/observed-reputation.js +16 -0
  28. package/dist/partnering/partnering.js +1 -1
  29. package/dist/person.d.ts +5 -0
  30. package/dist/person.js +119 -11
  31. package/dist/presentation.d.ts +10 -0
  32. package/dist/presentation.js +88 -0
  33. package/dist/promptings/promptings.d.ts +1 -0
  34. package/dist/promptings/promptings.js +40 -4
  35. package/dist/prospection/forecast.d.ts +2 -0
  36. package/dist/prospection/forecast.js +15 -1
  37. package/dist/qalb.d.ts +28 -0
  38. package/dist/qalb.js +67 -0
  39. package/dist/review/review.d.ts +32 -0
  40. package/dist/review/review.js +6 -2
  41. package/dist/scenarios/village.js +63 -26
  42. package/dist/sim/longrun.js +20 -3
  43. package/dist/sim/sim.d.ts +1 -1
  44. package/dist/sim/sim.js +93 -15
  45. package/dist/social/social.js +12 -2
  46. package/dist/types.d.ts +6 -0
  47. package/dist/will/will.d.ts +12 -0
  48. package/dist/will/will.js +43 -4
  49. package/package.json +1 -1
package/README.md CHANGED
@@ -489,3 +489,54 @@ const returnInput = advanceChapterClock(clock, 48 * 3_600_000);
489
489
  ```
490
490
 
491
491
  The host owns elapsed-time pacing, narrative interpretation, storage, consent and persona admission. A slot is an opportunity for coarse fiction. It is not proof of a lived day, settled duty, completed project or recovered body. Existing actor state and engine2.17.0 continuation remain unchanged unless the host explicitly uses the helper.
492
+
493
+
494
+ ## Plain English (2.19.0)
495
+
496
+ Set `lexicon: { plainEnglish: true }` in `PersonSpec` for new people. Change an
497
+ existing person's choice only through an explicit recorded host action. Do not
498
+ overwrite a loaded run's saved choice. Built-in decision narration,
499
+ spoken replies, intentions and person summaries then use complete, literal English.
500
+ Keep this choice in the saved person and replay setup. It changes recorded prose.
501
+ Without the option, legacy wording and continuation stay exact. Engine2.17.0 remains
502
+ unchanged: this option changes presentation, not decisions or save structure.
503
+
504
+ Host `lines`, `extend`, names and action forms still apply. A host phrase pack owns
505
+ its own grammar. HF does not translate arbitrary host labels or memory summaries.
506
+ Short words used inside templates remain phrases, so they compose grammatically.
507
+ Pass the same lexicon to renderers without a Person, including chronicle renderers
508
+ and label/reason readers.
509
+ Use authored `actions` forms for domain actions that English cannot inflect from an id.
510
+
511
+ Run `node packages/human/examples/plain-narration-control.ts` to compare legacy and
512
+ plain replies without advancing the person's state or RNG.
513
+
514
+ `labelLine(label, lexicon)` and `commitmentLabel(commitment, lexicon)` explain built-in
515
+ prayer and purpose labels without changing stored identity keys. `normLabel(norm, lexicon)`
516
+ explains selected built-in norm labels. `reasonLine(code, lexicon)` gives a sentence
517
+ for a known reason; an unknown plain reason returns an empty string so a host can
518
+ supply its own explanation. Without the option, each reader returns the legacy text.
519
+
520
+ `promptingLine(..., { plainEnglish: true })` supports direct calls.
521
+ `promptingsReading`, `reviewLine` and `bodyForecast` read the saved person's choice.
522
+ A person’s held interpretation of a thought stays distinct from an observed response.
523
+ Neither wording nor a missed-duty explanation changes obligations or excuses.
524
+
525
+ For domain-specific conversation facts, supply `converse`'s `phrase` callback.
526
+ HF cannot infer the English meaning of an opaque proposition id. Without authored
527
+ wording, the generic fact sentence still includes that id so the claim is not hidden.
528
+
529
+ Stock village outcomes, birth/marriage memories, routine fallbacks and generated
530
+ community log details use the same per-person option. World-supplied outcome
531
+ summaries and routine summaries remain exact.
532
+
533
+
534
+ ### Authored setup and explicit host effects
535
+
536
+ 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.
537
+
538
+ 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.
539
+
540
+ `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.
541
+
542
+ `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.
@@ -1,3 +1,50 @@
1
+ /**
2
+ * SCOPE: The agenda holds a person's commitments (promises, duties, appointments, worship windows, jobs)
3
+ * and goals, turns them into utility terms, closes them when kept or broken, recurs periodic ones, and
4
+ * proposes spontaneous purposes when a psychological need goes unserved. Shapes borrowed: deadline
5
+ * pressure rising toward the end of a window (temporal-motivation style, monotone and saturating, not
6
+ * hyperbolic-discounting fitted values), SDT-style needs (belonging, competence, meaning) as the sources
7
+ * of spontaneous goals. Breaking a commitment is reported to the composite, which routes any linked norm
8
+ * through conscience; the agenda itself does not judge. It does not plan multi-step routes, model
9
+ * implementation intentions, or claim calibrated procrastination curves.
10
+ *
11
+ * SCOPE (abstention, lane agenda+conscience): an `abstain` commitment (a fast, e.g. "no work after the evening prayer") is kept by
12
+ * not completing any `violatedBy` action whose span overlaps the window, and broken by the first one that does.
13
+ * While open it pushes against violating options with a constant negative term (importance x held conviction);
14
+ * it does not rise with deadline pressure, because the cost of breaking does not grow toward the end. Shape:
15
+ * self-imposed restraint as a standing cost on the tempting option (precommitment, cf. Ariely & Wertenbroch 2002
16
+ * for the qualitative effect only; no fitted values). Interrupted outcomes do not break it (hosts report a
17
+ * partial meal as completed if it should). Exemptions (illness, travel) come from the norm catalog's
18
+ * `exemptions` with their provenance, release that day's instance and record an owed make-up; they are not the
19
+ * necessity exception of Qur'an 2:173. The illness threshold is an engineering stand-in for the person's own
20
+ * judgment that they are ill; the fidya provision of 2:184 and any expiation (kaffara) for deliberate breaking are
21
+ * not modelled (kaffara is disputed: research/fasting-sources.md §1). A deliberately broken obligatory fast is a
22
+ * breach and also leaves a make-up owed (1.7.0; research/decisions.md, fasting-sources.md §1: the day is made up).
23
+ * A break under necessity is excused with a make-up owed (see the necessity-break SCOPE below).
24
+ *
25
+ * SCOPE (missed duties and make-up debt, engine 1.7.0; research/decisions.md, research/capacity-and-excuse-sources.md):
26
+ * the sources keep blame (a breach) apart from debt (qada) (capacity-and-excuse-sources.md §0). When a `worship`
27
+ * window linked to a norm the person holds as obligatory closes unkept, or an obligatory `abstain` is broken, an
28
+ * `OwedMakeUp` is recorded in `agenda.owed` whatever the cause; what the cause decides is the blame:
29
+ * - Asleep throughout (§4: sleep lifts blame, not the debt): the window opened while the person was already
30
+ * asleep and they woke no earlier than `wakeGrace` minutes before it closed (read from `body.since` and
31
+ * `body.lastSleep`). Sleep begun inside the open window is not excused: whether the sleeper expected to wake is
32
+ * not represented, so the framework takes the stricter reading.
33
+ * - Downed when it closed (§2, unconsciousness; `body.downed`, `body.lastDowned`): blame lifted, debt owed, but
34
+ * once more than `lapseWaiver` (5) windows have closed within one downing, that stretch's debt drops and no more
35
+ * is added (the Hanafi count by prayer times, kept as the default by decisions.md; Maliki and Shafi'i drop it
36
+ * sooner, Hanbali never). Downing is involuntary, so going down after the window opened still excuses it.
37
+ * - Excused instances close `released` with `exempt.reason` 'sleep' or 'unconscious', so the composite books no
38
+ * breach, distress or esteem cost; the chain recurs as for any exemption.
39
+ * - A mental break (`affect/crisis.ts`, reason intact) is not an excuse (§6: the test is retained capacity, and
40
+ * motivation is irrelevant): the window closes broken, with its breach, and the debt is owed (reason 'missed').
41
+ * - Forgetting lifts blame in the sources (§4) but the framework has no state for having forgotten a duty, and no
42
+ * state for loss of reason (junun, §1) or intoxication (§3); none of them is detected. Coercion and commands are
43
+ * not excuses here (§7).
44
+ * A make-up commitment (`makeUpOf`, see `scheduleMakeUp`) that closes unkept, or is broken, carries no blame: qada
45
+ * has no fixed time in the sources, so it closes `released` and its debt is open again for the host to reschedule.
46
+ * The framework never computes whether a prayer or a make-up is accepted.
47
+ */
1
48
  import type { Affordance, AgendaState, Commitment, Episode, Goal, Minute, MissedExcuse, NeedId, NeedReading, NormDefinition, Outcome, OwedMakeUp, Person, PersonId, PsychologicalNeed, Term } from '../types.ts';
2
49
  export interface GoalTemplate {
3
50
  label: string;
@@ -49,6 +49,7 @@
49
49
  import { DEFAULT_NORMS } from '../conscience/catalog.js';
50
50
  import { clamp01, dayOf, dpow, hasNumbers, isNum, isObj } from '../core/index.js';
51
51
  import { expectedEffect } from '../memory/index.js';
52
+ import { labelLine } from '../presentation.js';
52
53
  import { diminishesProject } from '../projects/index.js';
53
54
  import { MINUTES_PER_DAY } from '../types.js';
54
55
  export const AGENDA_DEFAULTS = {
@@ -1015,7 +1016,9 @@ export function revisePurposes(p, now) {
1015
1016
  kind: 'abandoned',
1016
1017
  actorId: p.id,
1017
1018
  valence: -0.3 * g.baseImportance,
1018
- summary: `gave up on ${g.label}`,
1019
+ summary: p.lexicon?.plainEnglish
1020
+ ? `I stopped trying to reach this goal: ${labelLine(g.label, p.lexicon)}.`
1021
+ : `gave up on ${g.label}`,
1019
1022
  tags: ['abandoned', 'goal', g.label, ...g.serves],
1020
1023
  });
1021
1024
  }
@@ -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
@@ -578,6 +578,21 @@ export function readBody(p, params = BODY_DEFAULTS, ctx) {
578
578
  },
579
579
  };
580
580
  }
581
+ /**
582
+ * SCOPE (host-caused tiredness): add nonnegative sleep-pressure and exertion costs, clamped to 0..1.
583
+ * The host supplies magnitudes; these are not calibrated claims. Absent, negative and non-finite costs
584
+ * do nothing. A dead body is unchanged, as with consume. Writes only these two body fields: no ticking,
585
+ * sleep transition, depletion, memory, RNG or other effects. Recovery remains with the existing body APIs.
586
+ */
587
+ export function addTiredness(p, costs) {
588
+ if (!p.body.alive)
589
+ return;
590
+ for (const key of ['sleepPressure', 'exertion']) {
591
+ const amount = costs[key];
592
+ if (amount !== undefined && Number.isFinite(amount) && amount > 0)
593
+ p.body[key] = clamp01(p.body[key] + amount);
594
+ }
595
+ }
581
596
  /** Eat, drink or take pain relief. `relief` lowers current pain; it then relaxes back toward its sources. */
582
597
  export function consume(p, deltas) {
583
598
  const b = p.body;
@@ -8,6 +8,7 @@
8
8
  * activity. Lines report behaviour and stated relationships; they never grade devotion, worth or acceptance.
9
9
  */
10
10
  import { actionForms, capitalize, displayName, nameOf, phraseLine } from '../narrate/lexicon.js';
11
+ import { labelLine } from '../presentation.js';
11
12
  /** Thresholds for `diffChronicle` (share of days). */
12
13
  export const STORY_DEFAULTS = {
13
14
  /** An item done unprompted on at least this share of days counts as done "without being told". */
@@ -97,7 +98,7 @@ function worshipActions(records) {
97
98
  }
98
99
  function formsOf(item, lex) {
99
100
  if (item.startsWith('prayer:')) {
100
- const label = item.slice('prayer:'.length);
101
+ const label = labelLine(item.slice('prayer:'.length), lex);
101
102
  return {
102
103
  base: phraseLine('prayer.base', { label }, lex),
103
104
  past: phraseLine('prayer.past', { label }, lex),
@@ -138,7 +139,7 @@ export function narrateChronicle(chronicle, opts = {}) {
138
139
  ? 'chronicle.prayer.one'
139
140
  : 'chronicle.prayer.all'
140
141
  : 'chronicle.prayer';
141
- raw.push({ key, vars: { label, n, days } });
142
+ raw.push({ key, vars: { label: labelLine(label, lex), n, days } });
142
143
  }
143
144
  // Commitments kept by kind (worship is covered above).
144
145
  const kept = {};
@@ -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';
@@ -40,6 +40,7 @@ import { agendaTermsWithGoalEstimates } from '../agenda/index.js';
40
40
  import { trustOf } from '../beliefs/index.js';
41
41
  import { normTerms } from '../conscience/index.js';
42
42
  import { clamp01, cmpStr, dexp, minuteOfDay, round } from '../core/index.js';
43
+ import { keysOf } from '../core/keys.js';
43
44
  import { curiosityTerms } from '../curiosity/index.js';
44
45
  import { habitEase, habitPull } from '../habits/index.js';
45
46
  import { expectedEffect } from '../memory/index.js';
@@ -270,7 +271,7 @@ export function consider(p, aff, ctx) {
270
271
  };
271
272
  // Needs: urgency × believed gain × believed success.
272
273
  const expected = expectedEffect(p, aff);
273
- for (const id of Object.keys(expected.needs)) {
274
+ for (const id of keysOf(expected.needs)) {
274
275
  const gain = expected.needs[id] ?? 0;
275
276
  if (gain === 0)
276
277
  continue;
@@ -296,7 +297,7 @@ export function consider(p, aff, ctx) {
296
297
  let relevance = 1;
297
298
  if (expected.valence > 0) {
298
299
  let served = -1;
299
- for (const id of Object.keys(expected.needs))
300
+ for (const id of keysOf(expected.needs))
300
301
  if ((expected.needs[id] ?? 0) > 0)
301
302
  served = Math.max(served, urgencyOf(ctx.needs, id));
302
303
  if (served >= 0)
@@ -244,9 +244,13 @@ export function converse(speaker, listener, ctx) {
244
244
  actorId: speaker.id,
245
245
  salience: round(0.3 + 0.4 * c.confidence),
246
246
  claims: [{ prop: c.prop, value: c.value, confidence: c.confidence }],
247
- summary: c.about !== undefined
248
- ? `${nameOf(speaker)} says ${c.about} is ${c.value ? '' : 'not '}${c.prop.split(':')[1] ?? ''}`
249
- : `${nameOf(speaker)} says ${c.prop} is ${c.value ? 'so' : 'not so'}`,
247
+ summary: speaker.lexicon?.plainEnglish
248
+ ? 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}.`
251
+ : c.about !== undefined
252
+ ? `${nameOf(speaker)} says ${c.about} is ${c.value ? '' : 'not '}${c.prop.split(':')[1] ?? ''}`
253
+ : `${nameOf(speaker)} says ${c.prop} is ${c.value ? 'so' : 'not so'}`,
250
254
  };
251
255
  if (ctx.phrase)
252
256
  pc.summary = ctx.phrase(Object.freeze(structuredClone(c)), pc.summary ?? '');
@@ -293,7 +297,9 @@ export function converse(speaker, listener, ctx) {
293
297
  valence: 0.1,
294
298
  salience: round(0.4 + 0.3 * a.strength),
295
299
  claims: [{ prop: `advice:${a.action}`, value: true, confidence: round(a.cred) }],
296
- summary: `${nameOf(speaker)} says I should ${a.action}`,
300
+ summary: speaker.lexicon?.plainEnglish
301
+ ? `${nameOf(speaker)} suggests this action: ${a.action.replace(/[-_]/g, ' ')}.`
302
+ : `${nameOf(speaker)} says I should ${a.action}`,
297
303
  };
298
304
  if (ctx.provenance) {
299
305
  const meta = beliefs.find((b) => b.prop === `${prefix}${a.action}`)?.provenance;
@@ -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);
@@ -215,7 +215,7 @@ export function deliver(mother, child, at = mother.now) {
215
215
  targetId: child.id,
216
216
  valence: 0.9,
217
217
  salience: 0.95,
218
- summary: 'gave birth',
218
+ summary: mother.lexicon?.plainEnglish ? 'I gave birth.' : 'gave birth',
219
219
  tags: ['birth', 'family'],
220
220
  });
221
221
  return { id: child.id, name: child.name ?? child.id, seed: pg.seed, bornAt: at, fatherId: pg.fatherId };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.18.0";
2
+ export declare const FRAMEWORK_VERSION = "2.20.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,15 +28,18 @@ 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';
34
35
  export * from './persona.ts';
35
36
  export * from './persona-calendar.ts';
37
+ export * from './presentation.ts';
36
38
  export { adoptProjectTask, considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal, } from './projects/index.ts';
37
39
  export * from './promptings/index.ts';
38
40
  export type { ConsequenceEvidence, PlanRoute, PlanStep } from './prospection/index.ts';
39
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';
40
43
  export { fingerprint, restore, snapshot } from './restore.ts';
41
44
  export * from './review/index.ts';
42
45
  export * from './scenarios/index.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export const FRAMEWORK_VERSION = '2.18.0';
2
+ export const FRAMEWORK_VERSION = '2.20.0';
3
3
  export { AFFECT_DEFAULTS, actionTendencies, advanceAffect, appraise, breakAllows, breakBehaviour, breakHazard, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, enableBreaks, feel, inBreak, readAffect, regulate, release as releaseEmotion, skipCrisis, strain, tendencyEmotions, } from './affect/index.js';
4
4
  // `agenda.release` collides with `affect.release`; the agenda one is re-exported under a clearer name.
5
5
  export { AGENDA_DEFAULTS, abandonGoal, adoptGoal, advanceAgenda, agendaTerms, applyExemptions, CARE_DEFAULTS, calendarRetimer, careDuty, commitmentPressure, createAgenda, DEFAULT_PRAYER_TIMES, EID_PRAYER_IMPORTANCE, EXEMPTION_DEFAULTS, eidPrayer, eidWindow, fastWindow, iftarWindow, inMakruhTime, isContinuingCommitment, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, retirePaidMakeUps, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
@@ -25,14 +25,17 @@ export { clearMethodContext, demonstrateMethod, METHOD_DEFAULTS, methodEvidence,
25
25
  export { MIGRATIONS, migratableVersions, migrate } from './migrate.js';
26
26
  export * from './narrate/index.js';
27
27
  export * from './needs/index.js';
28
+ export { confirmObservedReputation } from './observed-reputation.js';
28
29
  export * from './partnering/index.js';
29
30
  export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistence/index.js';
30
31
  export * from './person.js';
31
32
  export * from './persona.js';
32
33
  export * from './persona-calendar.js';
34
+ export * from './presentation.js';
33
35
  export { adoptProjectTask, considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal, } from './projects/index.js';
34
36
  export * from './promptings/index.js';
35
37
  export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.js';
38
+ export { qalbReading } from './qalb.js';
36
39
  export { fingerprint, restore, snapshot } from './restore.js';
37
40
  export * from './review/index.js';
38
41
  export * from './scenarios/index.js';
package/dist/longrun.js CHANGED
@@ -108,7 +108,7 @@ export function routineDay(p, routine, opts = {}) {
108
108
  action: a.action,
109
109
  actorId: p.id,
110
110
  valence: clampSigned(a.valence ?? 0),
111
- summary: a.summary ?? a.action,
111
+ summary: a.summary ?? (p.lexicon?.plainEnglish ? `I completed the activity “${a.action}”.` : a.action),
112
112
  tags: [...(a.tags ?? []), 'routine'],
113
113
  };
114
114
  if (companions[0] !== undefined)
@@ -1,3 +1,29 @@
1
+ /**
2
+ * SCOPE: episodic memory and learned action expectations. Episodes carry a salience that decays
3
+ * exponentially (Ebbinghaus-style forgetting curve, modelled as a single exponential per episode) with a
4
+ * half-life that lengthens for emotionally strong episodes (emotional-enhancement of memory), and recall
5
+ * slightly raises the salience of what is retrieved (testing effect). Expectations are cached action
6
+ * values learned by a delta rule (prediction-error learning, Rescorla-Wagner shape) with a minimum
7
+ * learning rate and a clamped step, blended with the host's advertisement by sample-based confidence.
8
+ * It does NOT model reconsolidation, false memory, interference between similar episodes, sleep-dependent
9
+ * consolidation, semantic abstraction, or model-based planning; parameters are engineering defaults for
10
+ * game time scales, not calibrated estimates.
11
+ *
12
+ * SCOPE (lasting gists, 1.8.0, opt-in per person with `enableGists`): episodic detail does not last a life, but the
13
+ * gist of what mattered does. An episode that leaves the episode list (evicted beyond the bound, or older than
14
+ * `GIST_DEFAULTS.horizon` at a `foldGists` call, which the composite makes at each day's close) is folded into the
15
+ * gist with the same kind, action, people and place when it was emotional enough (|valence| ≥ `minValence`, or a
16
+ * loss); weaker ones are simply forgotten. A gist keeps the encoding-weighted mean valence, a count and the summary of
17
+ * its strongest episode, and fades with half-lives of years (`halfLifeNeutral`..`halfLifeEmotional` by |valence|).
18
+ * Gists answer `recall` and `recallByCue` like episodes (so a remembered fear or loss still comes back at its place,
19
+ * person or anniversary), and `expectedEffect` reports the gists about an option's action or place, which cognition
20
+ * weighs as a `memory` term that fades as fresh experience of the option accumulates. So a fear learned at 20 still
21
+ * shapes choices at 40 after its episodes are gone, and enough good later visits outweigh it. Named shapes: the
22
+ * fading of episodic detail while gist persists (fuzzy-trace theory, Brainerd & Reyna) and the long retention of
23
+ * emotional autobiographical memories; the horizon, thresholds, half-lives and the bound of 64 are engineering
24
+ * choices. It does not model reconstruction errors in gists, deliberate rehearsal beyond retelling (`recall`
25
+ * boosts), or semantic knowledge abstracted from gists.
26
+ */
1
27
  import type { Affordance, AppraisalEvent, EntityId, Episode, Gist, MemoryState, Minute, NeedId, Outcome, Person, Signed, Unit } from '../types.ts';
2
28
  export declare const MEMORY_DEFAULTS: {
3
29
  maxEpisodes: number;
@@ -25,6 +25,7 @@
25
25
  * boosts), or semantic knowledge abstracted from gists.
26
26
  */
27
27
  import { clamp, clamp01, clampSigned, decay, hourOf, isNum, isObj, lerp, runningMean, } from '../core/index.js';
28
+ import { keysOf } from '../core/keys.js';
28
29
  import { DAYS_PER_YEAR, MINUTES_PER_DAY, MINUTES_PER_YEAR } from '../types.js';
29
30
  export const MEMORY_DEFAULTS = {
30
31
  maxEpisodes: 200,
@@ -511,10 +512,7 @@ export function expectedEffect(p, aff) {
511
512
  const samples = learned?.samples ?? 0;
512
513
  const confidence = samples / (samples + MEMORY_DEFAULTS.confidenceK);
513
514
  const needs = {};
514
- const keys = new Set([
515
- ...Object.keys(aff.advertises),
516
- ...(learned ? Object.keys(learned.needs) : []),
517
- ]);
515
+ const keys = new Set([...keysOf(aff.advertises), ...(learned ? keysOf(learned.needs) : [])]);
518
516
  for (const k of [...keys].sort()) {
519
517
  const adv = aff.advertises[k] ?? 0;
520
518
  const lv = learned?.needs[k];
@@ -563,11 +561,7 @@ function updateExpectation(p, key, aff, success, realized, felt) {
563
561
  }
564
562
  x.samples += 1;
565
563
  const n = x.samples;
566
- const keys = new Set([
567
- ...Object.keys(x.needs),
568
- ...Object.keys(aff.advertises),
569
- ...Object.keys(realized),
570
- ]);
564
+ const keys = new Set([...keysOf(x.needs), ...keysOf(aff.advertises), ...keysOf(realized)]);
571
565
  for (const k of keys) {
572
566
  const prior = x.needs[k] ?? aff.advertises[k] ?? 0;
573
567
  x.needs[k] = step(prior, realized[k] ?? 0, n);
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * SCOPE: keyed narration templates, host phrase packs and name/role lookup (N6). Every English sentence the
3
- * framework narrates lives in `EN_LINES` under a stable key; a host `Lexicon` replaces (`lines`) or extends
3
+ * framework narrates lives in `EN_LINES` under a stable key; `Lexicon.plainEnglish` selects the matching
4
+ * literal English pack while preserving default templates and host overrides. A host `Lexicon` replaces (`lines`) or extends
4
5
  * (`extend`) any key, names entities ("Ada") and maps relationship roles to nouns ("daughter") so narration
5
6
  * can say "my daughter" or "his daughter". Selection stays deterministic: a template is picked by an FNV hash of
6
7
  * a caller key (a decision id, a day index), never by the person's RNG. This is presentation only. It is
@@ -1,3 +1,4 @@
1
+ import { PLAIN_EN_LINES } from './plain.js';
1
2
  /** FNV-1a 32-bit hash for deterministic template selection. */
2
3
  export function hashString(s) {
3
4
  let h = 0x811c9dc5;
@@ -264,7 +265,7 @@ export const EN_LINES = {
264
265
  };
265
266
  /** Templates for a key after the lexicon's replacements and extensions; empty when unknown. */
266
267
  export function linesFor(key, lex) {
267
- const base = lex?.lines?.[key] ?? EN_LINES[key] ?? [];
268
+ const base = lex?.lines?.[key] ?? (lex?.plainEnglish ? PLAIN_EN_LINES[key] : undefined) ?? EN_LINES[key] ?? [];
268
269
  const more = lex?.extend?.[key];
269
270
  return more && more.length > 0 ? [...base, ...more] : base;
270
271
  }
@@ -1,5 +1,18 @@
1
+ /**
2
+ * SCOPE: templated first-person language for decisions and suggestion verdicts, plus a short summary of a
3
+ * person for UI. Sentences are selected from keyed template sets (`EN_LINES`, overridable per key by a host
4
+ * `Lexicon`, N6) by a hash of the decision id, so the same record always narrates the same way without
5
+ * consuming the person's RNG. When memory shaped an option (the record's `recalled` episodes), the narration
6
+ * cites the episode summary. People are named through `nameOf` (lexicon name, role phrase, or id). This is
7
+ * presentation only: nothing here reads back into the simulation, and no claim is made that the templates
8
+ * reflect how people actually explain themselves. `Lexicon.plainEnglish` opts into sentence intentions
9
+ * and summaries; arbitrary host prose is not translated or grammatically inferred.
10
+ */
1
11
  import type { DecisionRecord, Episode, Lexicon, Person, SuggestionResolution } from '../types.ts';
2
- /** Intention string for a decision: 'for Allah', 'to feed myself', 'to keep my promise to X', ... */
12
+ /**
13
+ * A plain intention is a sentence; legacy intentions remain phrases for existing hosts.
14
+ * Host-authored sentences pass through. This renderer never changes the record or RNG.
15
+ */
3
16
  export declare function intentionFor(p: Person, record: DecisionRecord, lexicon?: Lexicon): string;
4
17
  /** First-person narration of a decision record. Deterministic by the decision id. */
5
18
  export declare function narrateDecision(p: Person, record: DecisionRecord, lexicon?: Lexicon): string;