@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.
- package/README.md +51 -0
- package/dist/agenda/agenda.d.ts +47 -0
- package/dist/agenda/agenda.js +4 -1
- package/dist/body/body.d.ts +10 -0
- package/dist/body/body.js +15 -0
- package/dist/chronicle/story.js +3 -2
- package/dist/cognition/cognition.d.ts +37 -0
- package/dist/cognition/cognition.js +3 -2
- package/dist/conversation/conversation.js +10 -4
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/keys.d.ts +6 -0
- package/dist/core/keys.js +8 -0
- package/dist/family/family.js +5 -5
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/longrun.js +1 -1
- package/dist/memory/memory.d.ts +26 -0
- package/dist/memory/memory.js +3 -9
- package/dist/narrate/lexicon.d.ts +2 -1
- package/dist/narrate/lexicon.js +2 -1
- package/dist/narrate/narrate.d.ts +14 -1
- package/dist/narrate/narrate.js +81 -39
- package/dist/narrate/plain.d.ts +7 -0
- package/dist/narrate/plain.js +293 -0
- package/dist/observed-reputation.d.ts +4 -0
- package/dist/observed-reputation.js +16 -0
- package/dist/partnering/partnering.js +1 -1
- package/dist/person.d.ts +5 -0
- package/dist/person.js +119 -11
- package/dist/presentation.d.ts +10 -0
- package/dist/presentation.js +88 -0
- package/dist/promptings/promptings.d.ts +1 -0
- package/dist/promptings/promptings.js +40 -4
- package/dist/prospection/forecast.d.ts +2 -0
- package/dist/prospection/forecast.js +15 -1
- package/dist/qalb.d.ts +28 -0
- package/dist/qalb.js +67 -0
- package/dist/review/review.d.ts +32 -0
- package/dist/review/review.js +6 -2
- package/dist/scenarios/village.js +63 -26
- package/dist/sim/longrun.js +20 -3
- package/dist/sim/sim.d.ts +1 -1
- package/dist/sim/sim.js +93 -15
- package/dist/social/social.js +12 -2
- package/dist/types.d.ts +6 -0
- package/dist/will/will.d.ts +12 -0
- package/dist/will/will.js +43 -4
- 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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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 ??
|
|
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 {
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
}
|
|
@@ -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
|
+
}
|
package/dist/review/review.d.ts
CHANGED
|
@@ -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. */
|
package/dist/review/review.js
CHANGED
|
@@ -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 = (
|
|
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')
|