@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/dist/person.js CHANGED
@@ -27,17 +27,18 @@ import { ageCharacter, enableCharacterChange, noteCharacterDay, noteCharacterSoc
27
27
  import { closeDay, enableYearbook, endDay, noteAnswer, noteCommitments, noteDecision, noteMood, noteOutcome, trimChronicle, } from './chronicle/index.js';
28
28
  import { decide as cognitionDecide, desperationOf, prospectiveRoute, scoreAll } from './cognition/index.js';
29
29
  import { createConscience, guiltLoad, heldNorms, normVeto, recordDeed, recordRepair, repent, } from './conscience/index.js';
30
- import { clamp01, clampSigned, createRng, dayOf, isObj, minuteOfDay, round } from './core/index.js';
30
+ import { clamp01, clampSigned, createRng, dayOf, isObj, keysOf, minuteOfDay, round } from './core/index.js';
31
31
  import { enableCuriosity, noteCuriosity } from './curiosity/index.js';
32
32
  import { ambientBodyParams, ambientModifiers, ambientMood, ambientNeeds } from './environment/index.js';
33
33
  import { aptitudeOf, createFamily, pregnancyModifiers } from './family/index.js';
34
- import { advanceHabits, reinforce, withholdCued } from './habits/index.js';
34
+ import { advanceHabits, HABIT_DEFAULTS, reinforce, withholdCued } from './habits/index.js';
35
35
  import { ageYears, learningMultiplier, lifeModifiers } from './lifecourse/index.js';
36
- import { advanceMemory, createMemory, enableGists, foldGists, learnOutcome, recallByCue, remember, } from './memory/index.js';
36
+ import { advanceMemory, createMemory, enableGists, foldGists, learnOutcome, MEMORY_DEFAULTS, recallByCue, remember, } from './memory/index.js';
37
37
  import { intentionFor, narrateDecision, voiceLine } from './narrate/index.js';
38
38
  import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisfy, urgencyOf, } from './needs/index.js';
39
39
  import { widow } from './partnering/index.js';
40
40
  import { beginPursuit, enablePersistence, releasePursuit } from './persistence/index.js';
41
+ import { commitmentLabel } from './presentation.js';
41
42
  import { considerProjects, enableProjects, noteProjectAct, noteProjectStatus, perceiveProspect, } from './projects/index.js';
42
43
  import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
43
44
  import { bodyForecastFor, consumeConsequences, enablePlanning, forecastLine, forecastThresholds, pruneConsequences, receiveConsequences, } from './prospection/index.js';
@@ -113,7 +114,56 @@ function fill(keys, given, def) {
113
114
  }
114
115
  return out;
115
116
  }
117
+ function validateHostSetup(spec) {
118
+ const finite = (n) => Number.isFinite(n);
119
+ const unit = (n) => finite(n) && n >= 0 && n <= 1;
120
+ const count = (n) => Number.isSafeInteger(n) && n >= 0;
121
+ const optional = (n, valid) => n === undefined || valid(n);
122
+ if (spec.habits !== undefined) {
123
+ if (!Array.isArray(spec.habits) || spec.habits.length > HABIT_DEFAULTS.maxHabits)
124
+ throw new RangeError('Invalid habits setup bound');
125
+ for (const h of spec.habits) {
126
+ if (!h ||
127
+ !isObj(h.cue) ||
128
+ typeof h.action !== 'string' ||
129
+ h.action.length === 0 ||
130
+ !unit(h.strength) ||
131
+ !count(h.repetitions) ||
132
+ !finite(h.lastAt) ||
133
+ !optional(h.withheld, count) ||
134
+ !optional(h.lastWithheldAt, finite) ||
135
+ !optional(h.craving, unit) ||
136
+ !optional(h.cue.hour, (n) => finite(n) && n >= 0 && n < 24) ||
137
+ (h.cue.placeId !== undefined && typeof h.cue.placeId !== 'string') ||
138
+ (h.cue.after !== undefined && typeof h.cue.after !== 'string'))
139
+ throw new RangeError('Invalid habit setup record');
140
+ }
141
+ }
142
+ if (spec.expectations !== undefined) {
143
+ if (!Array.isArray(spec.expectations) || spec.expectations.length > MEMORY_DEFAULTS.maxExpectations)
144
+ throw new RangeError('Invalid expectations setup bound');
145
+ for (const e of spec.expectations) {
146
+ if (!e ||
147
+ typeof e.key !== 'string' ||
148
+ e.key.length === 0 ||
149
+ !isObj(e.needs) ||
150
+ !unit(e.successRate) ||
151
+ !count(e.samples) ||
152
+ !finite(e.valence) ||
153
+ Math.abs(e.valence) > 1 ||
154
+ Object.entries(e.needs).some(([key, n]) => ![...PHYSIOLOGICAL_NEEDS, ...PSYCHOLOGICAL_NEEDS].some((need) => need === key) ||
155
+ (n !== undefined && !finite(n))))
156
+ throw new RangeError('Invalid expectation setup record');
157
+ }
158
+ }
159
+ }
160
+ /**
161
+ * SCOPE (host setup): optional habits and expectations copy authored records, including nested cues and
162
+ * need deltas, in source order. Invalid numbers, domains or collection bounds reject with RangeError;
163
+ * setup never clamps, evicts, learns, advances time or consumes RNG. Omitted options keep legacy state.
164
+ */
116
165
  export function createPerson(spec) {
166
+ validateHostSetup(spec);
117
167
  const now = spec.now ?? 0;
118
168
  const traits = fill(TRAIT_KEYS, spec.traits, 0.5);
119
169
  const values = fill(VALUE_KEYS, spec.values, 0.5);
@@ -142,6 +192,38 @@ export function createPerson(spec) {
142
192
  trace: [],
143
193
  nextDecision: 0,
144
194
  };
195
+ if (spec.habits)
196
+ p.habits = spec.habits.map((h) => {
197
+ const cue = {};
198
+ if (h.cue.hour !== undefined)
199
+ cue.hour = h.cue.hour;
200
+ if (h.cue.placeId !== undefined)
201
+ cue.placeId = h.cue.placeId;
202
+ if (h.cue.after !== undefined)
203
+ cue.after = h.cue.after;
204
+ const out = {
205
+ cue,
206
+ action: h.action,
207
+ strength: h.strength,
208
+ repetitions: h.repetitions,
209
+ lastAt: h.lastAt,
210
+ };
211
+ if (h.craving !== undefined)
212
+ out.craving = h.craving;
213
+ if (h.withheld !== undefined)
214
+ out.withheld = h.withheld;
215
+ if (h.lastWithheldAt !== undefined)
216
+ out.lastWithheldAt = h.lastWithheldAt;
217
+ return out;
218
+ });
219
+ if (spec.expectations)
220
+ p.memory.expectations = spec.expectations.map((e) => ({
221
+ key: e.key,
222
+ needs: Object.fromEntries(Object.entries(e.needs)),
223
+ successRate: e.successRate,
224
+ samples: e.samples,
225
+ valence: e.valence,
226
+ }));
145
227
  if (spec.lexicon)
146
228
  p.lexicon = structuredClone(spec.lexicon);
147
229
  if (spec.retention)
@@ -304,7 +386,11 @@ function recordMissed(p, broken, now, brokenBy) {
304
386
  action,
305
387
  actorId: p.id,
306
388
  valence: -0.5 * c.importance,
307
- summary: `missed ${c.kind}: ${action}`,
389
+ summary: p.lexicon?.plainEnglish
390
+ ? c.kind === 'abstain'
391
+ ? `I broke this abstention (${commitmentLabel(c, p.lexicon)}) by doing this: ${action.replace(/[-_]/g, ' ')}.`
392
+ : `I did not complete this commitment (${commitmentLabel(c, p.lexicon)}). It required this action: ${action.replace(/[-_]/g, ' ')}.`
393
+ : `missed ${c.kind}: ${action}`,
308
394
  tags: ['missed', c.kind, ...(isNight(now) ? ['night'] : [])],
309
395
  ...(c.toId !== undefined && c.toId !== 'self' ? { targetId: c.toId } : {}),
310
396
  });
@@ -488,7 +574,13 @@ function crisisStep(p, at) {
488
574
  action: ev.behaviourId,
489
575
  actorId: p.id,
490
576
  valence: ev.kind === 'onset' ? -0.6 : 0.2,
491
- summary: ev.kind === 'onset' ? `I broke down (${label})` : 'I came back to myself',
577
+ summary: p.lexicon?.plainEnglish
578
+ ? ev.kind === 'onset'
579
+ ? `I had a breakdown (${label}).`
580
+ : 'My breakdown ended.'
581
+ : ev.kind === 'onset'
582
+ ? `I broke down (${label})`
583
+ : 'I came back to myself',
492
584
  tags: ['break', ev.kind],
493
585
  });
494
586
  if (ev.kind === 'onset')
@@ -510,7 +602,13 @@ function rememberDowned(p, at, kind, reason) {
510
602
  action: reason,
511
603
  actorId: p.id,
512
604
  valence: kind === 'down' ? -0.5 : 0.3,
513
- summary: kind === 'down' ? 'I went down and could not get up' : 'I got back on my feet',
605
+ summary: p.lexicon?.plainEnglish
606
+ ? kind === 'down'
607
+ ? 'I could not stand up.'
608
+ : 'I could stand up again.'
609
+ : kind === 'down'
610
+ ? 'I went down and could not get up'
611
+ : 'I got back on my feet',
514
612
  tags: ['downed', kind, reason],
515
613
  });
516
614
  }
@@ -940,7 +1038,9 @@ export function decide(p, affordances, opts = {}) {
940
1038
  actorId: res.voiceId,
941
1039
  targetId: p.id,
942
1040
  valence: -0.3,
943
- summary: `${res.voiceId} insisted that I ${chosen?.label ?? 'do as told'}`,
1041
+ summary: p.lexicon?.plainEnglish
1042
+ ? `${p.lexicon.names?.[res.voiceId] ?? res.voiceId} insisted: ${chosen?.label ?? 'follow the request'}.`
1043
+ : `${res.voiceId} insisted that I ${chosen?.label ?? 'do as told'}`,
944
1044
  tags: ['suggestion', 'insist'],
945
1045
  voiceId: res.voiceId,
946
1046
  });
@@ -992,7 +1092,9 @@ function applyCommandOutcome(p, co, list) {
992
1092
  actorId: c.voiceId,
993
1093
  targetId: p.id,
994
1094
  valence: -0.2 - 0.3 * Math.min(1, co.margin),
995
- summary: `${c.voiceId} took charge and had me ${target?.label ?? 'do as ordered'}`,
1095
+ summary: p.lexicon?.plainEnglish
1096
+ ? `${p.lexicon.names?.[c.voiceId] ?? c.voiceId} ordered: ${target?.label ?? 'follow the order'}.`
1097
+ : `${c.voiceId} took charge and had me ${target?.label ?? 'do as ordered'}`,
996
1098
  tags: ['suggestion', 'command'],
997
1099
  voiceId: c.voiceId,
998
1100
  });
@@ -1378,7 +1480,7 @@ export function finish(p, outcome, opts = {}) {
1378
1480
  const realized = realizedNeedDeltas(act, given, before, after);
1379
1481
  // Felt valence.
1380
1482
  let felt = 0;
1381
- for (const id of Object.keys(realized)) {
1483
+ for (const id of keysOf(realized)) {
1382
1484
  felt += D.feltGain * (D.feltBase + urgencyOf(before, id)) * (realized[id] ?? 0);
1383
1485
  }
1384
1486
  if (outcome.status === 'failed')
@@ -1429,7 +1531,10 @@ export function finish(p, outcome, opts = {}) {
1429
1531
  action: aff.action,
1430
1532
  actorId: p.id,
1431
1533
  valence: felt,
1432
- summary: outcome.summary ?? `${aff.label}: ${outcome.status}`,
1534
+ summary: outcome.summary ??
1535
+ (p.lexicon?.plainEnglish
1536
+ ? `${outcome.status === 'completed' ? 'I completed' : outcome.status === 'interrupted' ? 'I was interrupted during' : 'I could not complete'} this action: ${aff.label}.`
1537
+ : `${aff.label}: ${outcome.status}`),
1433
1538
  tags: [...(aff.tags ?? []), outcome.status, ...(isNight(act.startedAt) ? ['night'] : [])],
1434
1539
  };
1435
1540
  if (aff.targetId !== undefined)
@@ -1552,5 +1657,8 @@ export function nafsReading(p) {
1552
1657
  */
1553
1658
  export function bodyForecast(p, aff) {
1554
1659
  const spans = bodyForecastFor(p, aff, lifeModifiers(p), fastingCtx(p));
1555
- return { spans, line: forecastLine(spans, forecastThresholds(aff)) };
1660
+ return {
1661
+ spans,
1662
+ line: forecastLine(spans, forecastThresholds(aff), { plainEnglish: p.lexicon?.plainEnglish }),
1663
+ };
1556
1664
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * SCOPE: opt-in English display labels for framework-owned tokens. These readers do not rewrite stored
3
+ * identifiers, labels or host prose. They do not change decisions, religious understandings or RNG state.
4
+ * Unknown reason codes have no plain display text; the host supplies their meaning.
5
+ */
6
+ import type { Commitment, Lexicon, NormDefinition } from './types.ts';
7
+ export declare function labelLine(label: string, lexicon?: Lexicon): string;
8
+ export declare function commitmentLabel(c: Pick<Commitment, 'label' | 'kind'>, lexicon?: Lexicon): string;
9
+ export declare function normLabel(norm: Pick<NormDefinition, 'id' | 'label'>, lexicon?: Lexicon): string;
10
+ export declare function reasonLine(reason: string, lexicon?: Lexicon): string;
@@ -0,0 +1,88 @@
1
+ const LABELS = {
2
+ Fajr: 'the dawn prayer',
3
+ Dhuhr: 'the midday prayer',
4
+ Asr: 'the afternoon prayer',
5
+ Maghrib: 'the sunset prayer',
6
+ Isha: 'the night prayer',
7
+ suhoor: 'the meal before dawn',
8
+ iftar: 'the meal to end the fast',
9
+ 'Eid prayer': 'the Eid holiday prayer',
10
+ 'make-friend': 'making a friend',
11
+ 'master-skill': 'learning a skill',
12
+ 'earn-respect': 'earning respect',
13
+ serve: 'helping others',
14
+ 'make-up': 'a missed commitment to complete later',
15
+ };
16
+ export function labelLine(label, lexicon) {
17
+ if (!lexicon?.plainEnglish)
18
+ return label;
19
+ const known = Object.hasOwn(LABELS, label) ? LABELS[label] : undefined;
20
+ if (known !== undefined)
21
+ return known;
22
+ const missedName = label.startsWith('make-up ') ? label.slice(8) : '';
23
+ const missed = Object.hasOwn(LABELS, missedName) ? LABELS[missedName] : undefined;
24
+ return missed === undefined ? label : `${missed}, performed later because it was missed`;
25
+ }
26
+ export function commitmentLabel(c, lexicon) {
27
+ if (lexicon?.plainEnglish && c.label === undefined) {
28
+ const kinds = {
29
+ worship: 'my prayer',
30
+ promise: 'my promise',
31
+ duty: 'my duty',
32
+ appointment: 'my appointment',
33
+ job: 'my work shift',
34
+ abstain: 'my abstention',
35
+ };
36
+ return Object.hasOwn(kinds, c.kind) ? (kinds[c.kind] ?? c.kind) : c.kind;
37
+ }
38
+ return labelLine(c.label ?? c.kind, lexicon);
39
+ }
40
+ export function normLabel(norm, lexicon) {
41
+ if (!lexicon?.plainEnglish)
42
+ return norm.label;
43
+ const labels = {
44
+ zakat: 'Give the required share of wealth to those entitled to it',
45
+ charity: 'Give voluntary charity',
46
+ backbiting: 'Speaking about someone in a way they would dislike',
47
+ 'kin-marriage': 'Do not marry someone forbidden by the held rules about family relationships',
48
+ };
49
+ return Object.hasOwn(labels, norm.id) ? (labels[norm.id] ?? norm.label) : norm.label;
50
+ }
51
+ export function reasonLine(reason, lexicon) {
52
+ if (!lexicon?.plainEnglish)
53
+ return reason;
54
+ const lines = {
55
+ dead: 'The person has died.',
56
+ invalid: 'That action does not make sense.',
57
+ break: 'I cannot do that during this breakdown.',
58
+ downed: 'I cannot get up.',
59
+ asleep: 'I am asleep.',
60
+ 'not-sleepy': 'I am not sleepy enough.',
61
+ capacity: 'I cannot manage that physically.',
62
+ unavailable: 'That option is not available.',
63
+ distrust: 'I do not trust this advice.',
64
+ commanded: 'I am following an order.',
65
+ command: 'I am following an order.',
66
+ busy: 'I am doing something else right now.',
67
+ 'need:food': 'I need to eat first.',
68
+ 'need:water': 'I need to drink first.',
69
+ 'need:sleep': 'I need to sleep first.',
70
+ 'need:rest': 'I need to rest first.',
71
+ 'need:survival': 'I need to meet an urgent bodily need first.',
72
+ };
73
+ if (Object.hasOwn(lines, reason))
74
+ return lines[reason] ?? '';
75
+ if (reason.startsWith('duty:'))
76
+ return 'I need to keep this duty.';
77
+ if (reason.startsWith('norm:'))
78
+ return 'This conflicts with a rule I hold.';
79
+ if (reason.startsWith('rule:'))
80
+ return 'This conflicts with a personal rule I set.';
81
+ if (reason.startsWith('capacity:'))
82
+ return 'I cannot manage that physically.';
83
+ if (reason.startsWith('skill:'))
84
+ return 'I do not have enough skill for that.';
85
+ if (reason.startsWith('voice:'))
86
+ return 'I am following another person’s request.';
87
+ return '';
88
+ }
@@ -72,6 +72,7 @@ export declare function notePromptings(p: Person, seen: readonly PromptingSeen[]
72
72
  /** Vocabulary for one prompt under a held reading (narration only; it changes nothing). */
73
73
  export declare function promptingLine(reading: PromptingReading, outcome: PromptingRecord['outcome'], kind?: PromptingResponseKind, options?: {
74
74
  voice?: 'first' | 'third';
75
+ plainEnglish?: boolean;
75
76
  }): string;
76
77
  /** Read-only summary of recent prompts (a lens; computes nothing new about the person). */
77
78
  export interface PromptingsReadingView {
@@ -257,6 +257,34 @@ export function notePromptings(p, seen, chosenId, now) {
257
257
  }
258
258
  /** Vocabulary for one prompt under a held reading (narration only; it changes nothing). */
259
259
  export function promptingLine(reading, outcome, kind, options = {}) {
260
+ if (options.plainEnglish) {
261
+ const third = options.voice === 'third';
262
+ const who = third ? 'The person' : 'I';
263
+ const felt = third
264
+ ? 'The person felt an urge.'
265
+ : {
266
+ whisper: 'I felt a whisper urging me to act.',
267
+ inclination: 'I felt an urge from my own desires.',
268
+ intrusion: 'I had a thought. It does not define who I am.',
269
+ fault: 'I had a thought that felt like wrongdoing.',
270
+ none: 'I felt an urge.',
271
+ }[reading];
272
+ if (outcome === 'assented') {
273
+ const urge = third
274
+ ? 'The person felt an urge.'
275
+ : reading === 'whisper' || reading === 'inclination'
276
+ ? felt
277
+ : 'I felt an urge.';
278
+ return `${urge} ${who} acted on it.`;
279
+ }
280
+ if (outcome === 'passed')
281
+ return `${felt} It passed without action.`;
282
+ if (kind === 'redirect')
283
+ return `${felt} ${who} chose something else and let it pass.`;
284
+ if (kind === 'refuge')
285
+ return `${felt} ${who} sought refuge and let it pass.`;
286
+ return `${felt} ${who} did not act on it.`;
287
+ }
260
288
  // Plain third-person observations describe the response, without inferring the prompt's origin.
261
289
  if (options.voice === 'third') {
262
290
  if (outcome === 'assented')
@@ -313,7 +341,9 @@ export function promptingsReading(p, now = p.now) {
313
341
  practice: round(practiceAt(r, now, halfLifeOf(s))),
314
342
  ...(r.cue !== undefined ? { cue: r.cue } : {}),
315
343
  })),
316
- lines: s.recent.slice(-5).map((r) => promptingLine(s.reading, r.outcome, kindOf(r.responseId))),
344
+ lines: s.recent
345
+ .slice(-5)
346
+ .map((r) => promptingLine(s.reading, r.outcome, kindOf(r.responseId), { plainEnglish: p.lexicon?.plainEnglish })),
317
347
  };
318
348
  if (s.readingSource !== undefined)
319
349
  view.readingSource = s.readingSource;
@@ -329,19 +359,25 @@ export function nafsPatterns(p, inputs) {
329
359
  name: 'urging',
330
360
  ref: 'Qur’an 12:53',
331
361
  evident: assentedAgainstNorm.length > 0,
332
- evidence: `${assentedAgainstNorm.length} recent prompts followed against a held norm`,
362
+ evidence: p.lexicon?.plainEnglish
363
+ ? `The person acted on ${assentedAgainstNorm.length} recent urges despite a rule they hold.`
364
+ : `${assentedAgainstNorm.length} recent prompts followed against a held norm`,
333
365
  },
334
366
  {
335
367
  name: 'reproaching',
336
368
  ref: 'Qur’an 75:2',
337
369
  evident: reproach,
338
- evidence: `open-breach weight ${round(inputs.guiltLoad, 2)}, felt guilt ${round(inputs.guilt, 2)}`,
370
+ evidence: p.lexicon?.plainEnglish
371
+ ? `The weight of unresolved breaches is ${round(inputs.guiltLoad, 2)}. The strength of felt guilt is ${round(inputs.guilt, 2)}.`
372
+ : `open-breach weight ${round(inputs.guiltLoad, 2)}, felt guilt ${round(inputs.guilt, 2)}`,
339
373
  },
340
374
  {
341
375
  name: 'at rest',
342
376
  ref: 'Qur’an 89:27',
343
377
  evident: letPass > 0 && assentedAgainstNorm.length === 0 && !reproach,
344
- evidence: `${letPass} recent prompts let pass, none followed against a norm, no felt reproach`,
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'}.`
380
+ : `${letPass} recent prompts let pass, none followed against a norm, no felt reproach`,
345
381
  },
346
382
  ];
347
383
  }
@@ -54,4 +54,6 @@ export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeM
54
54
  export declare function forecastLine(spans: readonly BodyForecastSpan[], at?: {
55
55
  comfortAt: number;
56
56
  spentAt: number;
57
+ }, options?: {
58
+ plainEnglish?: boolean;
57
59
  }): string;
@@ -103,7 +103,7 @@ export function bodyForecastTerm(p, aff, mods, perception) {
103
103
  return v < 0 ? { source: 'forecast:body', value: v } : undefined;
104
104
  }
105
105
  /** A first-person line for the forecast (narration only); `at` is the offer's thresholds (2.8.0, default as before). */
106
- export function forecastLine(spans, at = FORECAST_DEFAULTS) {
106
+ export function forecastLine(spans, at = FORECAST_DEFAULTS, options = {}) {
107
107
  const after = spans.slice(1);
108
108
  if (after.length === 0)
109
109
  return '';
@@ -111,6 +111,20 @@ export function forecastLine(spans, at = FORECAST_DEFAULTS) {
111
111
  for (const s of after)
112
112
  if (s.worst > w.worst)
113
113
  w = s;
114
+ if (options.plainEnglish) {
115
+ const label = w.label === 'after' ? 'what comes next' : w.label;
116
+ const state = {
117
+ fatigue: 'too tired',
118
+ sleepiness: 'too sleepy',
119
+ hunger: 'too hungry',
120
+ thirst: 'too thirsty',
121
+ }[w.worstOf];
122
+ if (w.worst >= at.spentAt)
123
+ return `I can do this. I will be ${state} for ${label}.`;
124
+ if (w.worst >= at.comfortAt)
125
+ return `I can do this, but ${label} will be hard.`;
126
+ return `I can manage ${label} afterward.`;
127
+ }
114
128
  const what = { fatigue: 'spent', sleepiness: 'too sleepy', hunger: 'too hungry', thirst: 'too thirsty' }[w.worstOf];
115
129
  if (w.worst >= at.spentAt)
116
130
  return `I can do it, but I'll be ${what} for ${w.label}.`;
package/dist/qalb.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * SCOPE: a read-only narration lens over retained attention evidence, held norms and conviction,
3
+ * mood and the three distinct trust stores. Owns no state and supplies no decision input. It does
4
+ * not infer spiritual health, rank people, judge norms, measure the ruh or compute divine acceptance.
5
+ * Retained memories are evidence available for narration, not a complete attention history.
6
+ *
7
+ * Provenance: research/reconciliation.md §2–4, §7 (Q7: Qur'an 22:46; H9: Bukhari 52 /
8
+ * Muslim 1599a); research/islamic-foundations.md §2 (qalb as an overlapping interpretive view);
9
+ * research/decisions.md, “Qalb, nafs, fitra, ʿaql as state”. These support the lens, not a
10
+ * diagnostic mapping. Sign/zero categories below are engineering descriptions of stored values,
11
+ * not religious categories. Mood and interpersonal trust imply nothing about faith.
12
+ */
13
+ import type { Person } from './types.ts';
14
+ export interface QalbDescription {
15
+ facet: 'attention' | 'held-norm' | 'mood' | 'trust';
16
+ category: string;
17
+ /** Every description is attributed to this simulated person's records. */
18
+ personId: string;
19
+ text: string;
20
+ /** Detached identifiers and state paths, never mutable state objects or spiritual scores. */
21
+ evidence: string[];
22
+ sources: string[];
23
+ }
24
+ /**
25
+ * Plain English descriptions; no scalar summary, comparison or hidden update.
26
+ * Hosts must not present these records as spiritual health or a religious assessment.
27
+ */
28
+ export declare function qalbReading(p: Person): QalbDescription[];
package/dist/qalb.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Plain English descriptions; no scalar summary, comparison or hidden update.
3
+ * Hosts must not present these records as spiritual health or a religious assessment.
4
+ */
5
+ export function qalbReading(p) {
6
+ const sources = [
7
+ 'research/reconciliation.md §2–4, §7; Q7 (22:46); H9 (Bukhari 52; Muslim 1599a)',
8
+ 'research/islamic-foundations.md §2: Qalb',
9
+ 'research/decisions.md: Qalb, nafs, fitra, ʿaql as state (2026-10-18)',
10
+ ];
11
+ const rows = [];
12
+ const add = (facet, category, text, evidence) => {
13
+ rows.push({ facet, category, personId: p.id, text, evidence, sources: [...sources] });
14
+ };
15
+ const memories = [...p.memory.episodes, ...(p.memory.gists ?? [])];
16
+ add('attention', memories.length ? 'retained-evidence' : 'no-retained-evidence', memories.length
17
+ ? 'Their memories retain some experiences. These do not show everything they noticed.'
18
+ : 'Their records contain no retained experiences. This does not show what they noticed.', memories.map((e) => `memory:${e.id}:${e.kind}`));
19
+ if (!p.conscience.norms.length)
20
+ add('held-norm', 'unrecorded', 'Their records contain no held rules.', ['conscience.norms']);
21
+ for (const n of p.conscience.norms) {
22
+ add('held-norm', n.conviction === 0 ? 'no-conviction' : 'held-understanding', n.conviction === 0
23
+ ? 'They have a recorded rule with no stated conviction.'
24
+ : 'They hold an understanding of this rule. This is their view.', [`norm:${n.normId}`, `standing:${n.standing}`, 'conscience.norms.conviction']);
25
+ }
26
+ add('mood', p.affect.mood.valence < 0 ? 'unpleasant' : p.affect.mood.valence > 0 ? 'pleasant' : 'neutral', p.affect.mood.valence < 0
27
+ ? 'Their recorded mood feels unpleasant.'
28
+ : p.affect.mood.valence > 0
29
+ ? 'Their recorded mood feels pleasant.'
30
+ : 'Their recorded mood feels neutral.', ['affect.mood.valence']);
31
+ add('mood', p.affect.mood.arousal === 0 ? 'no-arousal' : 'some-arousal', p.affect.mood.arousal === 0
32
+ ? 'Their recorded mood has no arousal.'
33
+ : 'Their recorded mood has some arousal.', ['affect.mood.arousal']);
34
+ // Keep stores distinct. Do not substitute relationship trust for source or advising-voice trust.
35
+ const ties = p.social.relationships.map((r) => ({
36
+ store: 'social.relationships',
37
+ id: r.otherId,
38
+ trust: r.trust,
39
+ }));
40
+ const voices = p.will.voices.map((v) => ({ store: 'will.voices', id: v.voiceId, trust: v.trust }));
41
+ const sourcesTrust = Object.entries(p.memory.sourceTrust).map(([id, trust]) => ({
42
+ store: 'memory.sourceTrust',
43
+ id,
44
+ trust,
45
+ }));
46
+ const trusts = [...ties, ...voices, ...sourcesTrust];
47
+ if (!trusts.length)
48
+ add('trust', 'unrecorded', 'Their records contain no learned trust.', [
49
+ 'social.relationships',
50
+ 'will.voices',
51
+ 'memory.sourceTrust',
52
+ ]);
53
+ for (const t of trusts) {
54
+ // 0.5 is the numeric range midpoint, not a faith threshold or personal baseline.
55
+ const subject = t.store === 'social.relationships'
56
+ ? 'this person'
57
+ : t.store === 'will.voices'
58
+ ? 'this advising voice'
59
+ : 'this information source';
60
+ add('trust', t.trust < 0.5 ? 'below-midpoint' : t.trust > 0.5 ? 'above-midpoint' : 'at-midpoint', t.trust < 0.5
61
+ ? `Their trust in ${subject} is below the middle of its range.`
62
+ : t.trust > 0.5
63
+ ? `Their trust in ${subject} is above the middle of its range.`
64
+ : `Their trust in ${subject} is at the middle of its range.`, [`${t.store}:${t.id}`]);
65
+ }
66
+ return rows;
67
+ }
@@ -1,3 +1,35 @@
1
+ /**
2
+ * SCOPE (evening self-review, muhasaba, 2.7.0): once a day, from `at` (default 21:00), a person looks back over
3
+ * the commitments the chronicle recorded as kept or broken since their last review, and for each one they broke
4
+ * forms or renews a *resolve*: an if–then intention for that commitment's next occasion (Gollwitzer & Sheeran
5
+ * 2006, research/reconciliation.md E1). While a matching commitment is pending, the resolve adds one bounded term,
6
+ * `review:resolve` (each resolve counted once per option, the sum clamped to ±`resolveScale`; 2.9.0: the term's
7
+ * `ref` is the resolve's key when exactly one resolve weighed in), with the sign of the commitment's own term (toward a duty, away from an abstention's breaking
8
+ * act), scaled by its strength. Strength grows with diminishing gains at each further lapse, relaxes once the
9
+ * commitment is kept, and fades over `halfLifeDays` of disuse. Released and excused commitments (exemptions,
10
+ * sleep, necessity) form no resolve: what was beyond capacity is not held against the person (2:286). The review
11
+ * also runs when the day closes, if the evening passed without it or a lapse came after it.
12
+ *
13
+ * Held readings (`ReviewStyle`): `accounting` is self-accounting as the tradition describes it, naming the lapse
14
+ * with a view to tomorrow (Q 59:18, whose "tomorrow" most exegetes read as the hereafter, so a daily review is an
15
+ * inference; ʿUmar's saying reported by Tirmidhi after 2459, an athar, not a prophetic
16
+ * hadith) and turning back without despair (3:135, 39:53); it forms full resolves and adds no emotion. Reading accounting as
17
+ * non-punishing, like self-compassion after failure raising the motivation to repair (Breines & Chen 2012, E3),
18
+ * is an engineering mapping, not a finding about muhasaba. `ruminating` goes over
19
+ * the lapse abstractly: weaker resolves and a small distress emotion (Watkins 2008, E4: abstract, unconstructive
20
+ * repetitive thought). `none` never reviews. The review reads only chronicle notes and changes no other module's
21
+ * state except that one emotion.
22
+ *
23
+ * Every number is an engineering default, not fitted. Does not claim: divine reckoning or acceptance, a measure of
24
+ * piety, repentance as a ruling (tawba conditions are not modelled), effects on sleep onset (Harvey's model, E6, is
25
+ * not evidence enough), or that review improves outcomes beyond the borrowed if–then shape. It draws no randomness;
26
+ * without `enableReview` nothing here runs and existing runs replay byte for byte.
27
+ *
28
+ * Host notes (2.9.0, docs only). `reviewLine` sets commitment labels into "I didn't keep …" and "Today I kept …",
29
+ * so a label should be a noun phrase ("my call to Mum", "the evening prayer"), not a verb phrase ("calling Mum").
30
+ * A recurring commitment lapses on any day its window passes unlived: a host that skips a day (no service, a
31
+ * fast-forward) should still live, or release, that day's occasions, or the review counts each one as missed.
32
+ */
1
33
  import type { DayRecord, Minute, Person, ReviewRecord, ReviewSpec, ReviewState, ReviewStyle, Term } from '../types.ts';
2
34
  export declare const REVIEW_DEFAULTS: {
3
35
  /** Default evening review time, minute of day. */
@@ -32,6 +32,7 @@
32
32
  */
33
33
  import { clamp01, dexp, isObj, minuteOfDay, round } from '../core/index.js';
34
34
  import { pickLine } from '../narrate/lexicon.js';
35
+ import { labelLine } from '../presentation.js';
35
36
  export const REVIEW_DEFAULTS = {
36
37
  /** Default evening review time, minute of day. */
37
38
  at: 21 * 60,
@@ -198,7 +199,10 @@ export function reviewLine(p, options = {}) {
198
199
  const r = st?.last;
199
200
  if (!st || st.style === 'none' || !r)
200
201
  return undefined;
201
- const list = (xs) => xs.length <= 1 ? (xs[0] ?? '') : `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}`;
202
+ const list = (items) => {
203
+ const xs = items.map((item) => labelLine(item, p.lexicon));
204
+ return xs.length <= 1 ? (xs[0] ?? '') : `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}`;
205
+ };
202
206
  if (r.missed.length === 0)
203
207
  return r.kept.length > 0 ? `Today I kept ${list(r.kept)}.` : undefined;
204
208
  if (options.cause) {
@@ -208,7 +212,7 @@ export function reviewLine(p, options = {}) {
208
212
  : 'Next time I will make room to return to it.';
209
213
  return pickLine('review.cause', `${p.id}:${r.day}`, { cause, missed: list(r.missed), next }, p.lexicon);
210
214
  }
211
- if (options.variety ?? p.lexicon?.variety) {
215
+ if (p.lexicon?.plainEnglish || (options.variety ?? p.lexicon?.variety)) {
212
216
  return pickLine(`review.${st.style}`, `${p.id}:${r.day}`, { missed: list(r.missed) }, p.lexicon);
213
217
  }
214
218
  if (st.style === 'ruminating')