@adam0white/human-framework 2.18.0 → 2.19.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 +40 -0
- package/dist/agenda/agenda.d.ts +47 -0
- package/dist/agenda/agenda.js +4 -1
- package/dist/chronicle/story.js +3 -2
- package/dist/conversation/conversation.js +10 -4
- package/dist/family/family.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/longrun.js +1 -1
- 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/partnering/partnering.js +1 -1
- package/dist/person.js +34 -7
- 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/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/types.d.ts +2 -0
- package/dist/will/will.js +22 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -489,3 +489,43 @@ 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.
|
package/dist/agenda/agenda.d.ts
CHANGED
|
@@ -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;
|
package/dist/agenda/agenda.js
CHANGED
|
@@ -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:
|
|
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
|
}
|
package/dist/chronicle/story.js
CHANGED
|
@@ -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 = {};
|
|
@@ -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:
|
|
248
|
-
?
|
|
249
|
-
|
|
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:
|
|
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;
|
package/dist/family/family.js
CHANGED
|
@@ -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.
|
|
2
|
+
export declare const FRAMEWORK_VERSION = "2.19.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';
|
|
@@ -33,6 +33,7 @@ export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistenc
|
|
|
33
33
|
export * from './person.ts';
|
|
34
34
|
export * from './persona.ts';
|
|
35
35
|
export * from './persona-calendar.ts';
|
|
36
|
+
export * from './presentation.ts';
|
|
36
37
|
export { adoptProjectTask, considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal, } from './projects/index.ts';
|
|
37
38
|
export * from './promptings/index.ts';
|
|
38
39
|
export type { ConsequenceEvidence, PlanRoute, PlanStep } from './prospection/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework public surface. */
|
|
2
|
-
export const FRAMEWORK_VERSION = '2.
|
|
2
|
+
export const FRAMEWORK_VERSION = '2.19.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';
|
|
@@ -30,6 +30,7 @@ export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistenc
|
|
|
30
30
|
export * from './person.js';
|
|
31
31
|
export * from './persona.js';
|
|
32
32
|
export * from './persona-calendar.js';
|
|
33
|
+
export * from './presentation.js';
|
|
33
34
|
export { adoptProjectTask, considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal, } from './projects/index.js';
|
|
34
35
|
export * from './promptings/index.js';
|
|
35
36
|
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/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,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;
|
|
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
|
package/dist/narrate/lexicon.js
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/narrate/narrate.js
CHANGED
|
@@ -5,23 +5,23 @@
|
|
|
5
5
|
* consuming the person's RNG. When memory shaped an option (the record's `recalled` episodes), the narration
|
|
6
6
|
* cites the episode summary. People are named through `nameOf` (lexicon name, role phrase, or id). This is
|
|
7
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.
|
|
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.
|
|
9
10
|
*/
|
|
10
11
|
import { NORM_SCOPE } from '../conscience/index.js';
|
|
12
|
+
import { labelLine } from '../presentation.js';
|
|
11
13
|
import { capitalize, linesFor, lowerFirst, nameOf, phraseLine, pickLine } from './lexicon.js';
|
|
12
14
|
/**
|
|
13
15
|
* Devotional intention: 'for Allah' for norms the built-in catalog scopes as religious, else 'for God'. The phrase
|
|
14
16
|
* follows the norm, not the person (review 2026-10-03): a host whose people differ in faith overrides
|
|
15
17
|
* `devotion.allah` / `devotion.god` per person through `Person.lexicon` (from `PersonSpec.lexicon`).
|
|
16
18
|
*/
|
|
17
|
-
function devotion(p, normId, lex) {
|
|
19
|
+
function devotion(p, normId, lex, say = phraseLine) {
|
|
18
20
|
if (p.values.tradition < 0.6)
|
|
19
|
-
return normId === undefined
|
|
20
|
-
? phraseLine('devotion.keep', {}, lex)
|
|
21
|
-
: phraseLine('intention.right', {}, lex);
|
|
21
|
+
return normId === undefined ? say('devotion.keep', {}, lex) : say('intention.right', {}, lex);
|
|
22
22
|
if (normId !== undefined && NORM_SCOPE[normId] === 'religious')
|
|
23
|
-
return
|
|
24
|
-
return
|
|
23
|
+
return say('devotion.allah', {}, lex);
|
|
24
|
+
return say('devotion.god', {}, lex);
|
|
25
25
|
}
|
|
26
26
|
const topTerm = (c) => {
|
|
27
27
|
let best;
|
|
@@ -33,77 +33,108 @@ const topTerm = (c) => {
|
|
|
33
33
|
/** Voices the narration addresses as "you" (the host's own voice); other voices are named when a lexicon names them. */
|
|
34
34
|
const PLAYER_VOICES = new Set(['player', 'you']);
|
|
35
35
|
/** Intention string for a decision: 'for Allah', 'to feed myself', 'to keep my promise to X', ... */
|
|
36
|
-
|
|
36
|
+
function intentionPhrase(p, record, lexicon, selected) {
|
|
37
37
|
const lex = lexicon ?? p.lexicon;
|
|
38
|
+
const say = (key, vars = {}, lex) => {
|
|
39
|
+
if (selected)
|
|
40
|
+
selected.key = key;
|
|
41
|
+
return phraseLine(key, vars, lex);
|
|
42
|
+
};
|
|
38
43
|
const chosen = record.considered.find((c) => c.affordanceId === record.chosenAffordanceId);
|
|
39
44
|
if (!chosen)
|
|
40
|
-
return
|
|
45
|
+
return say('intention.none', {}, lex);
|
|
41
46
|
if (record.suggestion?.verdict === 'complied')
|
|
42
|
-
return
|
|
47
|
+
return say('intention.complied', {}, lex);
|
|
43
48
|
if (record.suggestion?.verdict === 'commanded')
|
|
44
|
-
return
|
|
49
|
+
return say('intention.commanded', {}, lex);
|
|
45
50
|
const top = topTerm(chosen);
|
|
46
51
|
if (!top)
|
|
47
|
-
return
|
|
52
|
+
return say('intention.default', {}, lex);
|
|
48
53
|
const [kind, rest] = [top.source.split(':')[0] ?? '', top.source.slice(top.source.indexOf(':') + 1)];
|
|
49
54
|
switch (kind) {
|
|
50
55
|
case 'need':
|
|
51
56
|
return linesFor(`intention:${rest}`, lex).length > 0
|
|
52
|
-
?
|
|
53
|
-
:
|
|
57
|
+
? say(`intention:${rest}`, {}, lex)
|
|
58
|
+
: say('intention.need', { need: rest }, lex);
|
|
54
59
|
case 'norm': {
|
|
55
60
|
const tracked = p.agenda.commitments.some((c) => c.normId === rest && c.kind === 'worship');
|
|
56
61
|
if (NORM_SCOPE[rest] === 'religious' || tracked)
|
|
57
|
-
return devotion(p, rest, lex);
|
|
58
|
-
return
|
|
62
|
+
return devotion(p, rest, lex, say);
|
|
63
|
+
return say('intention.right', {}, lex);
|
|
59
64
|
}
|
|
60
65
|
case 'conscience':
|
|
61
|
-
return
|
|
66
|
+
return say(rest === 'repair' ? 'intention.repair' : 'intention.repent', {}, lex);
|
|
62
67
|
case 'commitment': {
|
|
63
68
|
const c = p.agenda.commitments.find((x) => x.id === rest);
|
|
64
69
|
if (c?.kind === 'worship')
|
|
65
70
|
return c.normId === undefined || p.values.tradition < 0.6
|
|
66
|
-
? devotion(p, undefined, lex)
|
|
67
|
-
: devotion(p, c.normId, lex);
|
|
71
|
+
? devotion(p, undefined, lex, say)
|
|
72
|
+
: devotion(p, c.normId, lex, say);
|
|
68
73
|
if (c?.toId && c.toId !== 'self')
|
|
69
|
-
return
|
|
74
|
+
return say('intention.promiseTo', { who: nameOf(p, c.toId, lex) }, lex);
|
|
70
75
|
// A host may phrase a labelled commitment its own way ('intention.label:suhoor' → "for suhoor").
|
|
71
76
|
if (c?.label !== undefined && linesFor(`intention.label:${c.label}`, lex).length > 0)
|
|
72
|
-
return
|
|
73
|
-
return
|
|
77
|
+
return say(`intention.label:${c.label}`, {}, lex);
|
|
78
|
+
return say('intention.word', {}, lex);
|
|
74
79
|
}
|
|
75
80
|
case 'goal': {
|
|
76
81
|
const g = p.agenda.goals.find((x) => x.id === rest);
|
|
77
82
|
return g
|
|
78
|
-
?
|
|
79
|
-
:
|
|
83
|
+
? say('intention.goal', { goal: lex?.plainEnglish ? labelLine(g.label, lex) : g.label.replace(/-/g, ' ') }, lex)
|
|
84
|
+
: say('intention.goalFallback', {}, lex);
|
|
80
85
|
}
|
|
81
86
|
case 'social':
|
|
82
87
|
return rest === 'group'
|
|
83
|
-
?
|
|
84
|
-
:
|
|
88
|
+
? say('intention.group', {}, lex)
|
|
89
|
+
: say('intention.for', { who: nameOf(p, rest, lex) }, lex);
|
|
85
90
|
case 'suggestion': {
|
|
86
91
|
const voice = rest.startsWith('remembered:') ? rest.slice('remembered:'.length) : rest;
|
|
87
92
|
if (lex?.names?.[voice] !== undefined && !PLAYER_VOICES.has(voice))
|
|
88
|
-
return
|
|
89
|
-
return
|
|
93
|
+
return say('intention.suggestionBy', { who: nameOf(p, voice, lex) }, lex);
|
|
94
|
+
return say('intention.suggestion', {}, lex);
|
|
90
95
|
}
|
|
91
96
|
case 'habit':
|
|
92
|
-
return
|
|
97
|
+
return say('intention.habit', {}, lex);
|
|
93
98
|
case 'emotion':
|
|
94
|
-
return
|
|
99
|
+
return say('intention.emotion', {}, lex);
|
|
95
100
|
case 'material':
|
|
96
101
|
return lex?.materialMeaning !== undefined
|
|
97
|
-
?
|
|
98
|
-
:
|
|
102
|
+
? say('intention.material.named', { material: lex.materialMeaning }, lex)
|
|
103
|
+
: say('intention.material', {}, lex);
|
|
99
104
|
case 'precommit':
|
|
100
|
-
return
|
|
105
|
+
return say('intention.precommit', {}, lex);
|
|
101
106
|
case 'expectation':
|
|
102
|
-
return
|
|
107
|
+
return say('intention.expectation', {}, lex);
|
|
103
108
|
default:
|
|
104
|
-
return
|
|
109
|
+
return say('intention.default', {}, lex);
|
|
105
110
|
}
|
|
106
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* A plain intention is a sentence; legacy intentions remain phrases for existing hosts.
|
|
114
|
+
* Host-authored sentences pass through. This renderer never changes the record or RNG.
|
|
115
|
+
*/
|
|
116
|
+
export function intentionFor(p, record, lexicon) {
|
|
117
|
+
const lex = lexicon ?? p.lexicon;
|
|
118
|
+
const selected = {};
|
|
119
|
+
const phrase = intentionPhrase(p, record, lex, selected);
|
|
120
|
+
if (selected.key !== undefined && lex?.lines?.[selected.key] !== undefined)
|
|
121
|
+
return phrase;
|
|
122
|
+
if (!lex?.plainEnglish || /[.!?]$/.test(phrase))
|
|
123
|
+
return phrase;
|
|
124
|
+
if (phrase === 'nothing to do')
|
|
125
|
+
return 'There is nothing for me to do.';
|
|
126
|
+
if (phrase === 'to earn')
|
|
127
|
+
return 'I want to earn money.';
|
|
128
|
+
if (phrase.startsWith('to '))
|
|
129
|
+
return `I want ${phrase}.`;
|
|
130
|
+
if (phrase.startsWith('because '))
|
|
131
|
+
return `I am doing this ${phrase}.`;
|
|
132
|
+
if (phrase.startsWith('for '))
|
|
133
|
+
return `I am doing this ${phrase}.`;
|
|
134
|
+
if (phrase === 'out of habit')
|
|
135
|
+
return 'I am doing this out of habit.';
|
|
136
|
+
return `My reason is ${phrase}.`;
|
|
137
|
+
}
|
|
107
138
|
function citeEpisode(p, ids, key, lex) {
|
|
108
139
|
if (!ids || ids.length === 0)
|
|
109
140
|
return undefined;
|
|
@@ -111,7 +142,7 @@ function citeEpisode(p, ids, key, lex) {
|
|
|
111
142
|
if (!ep)
|
|
112
143
|
return undefined;
|
|
113
144
|
const summary = ep.summary.endsWith('.') ? ep.summary : `${ep.summary}.`;
|
|
114
|
-
return pickLine(ep.valence < 0 ? 'cite.bad' : 'cite.good', key, { summary: lowerFirst(summary) }, lex);
|
|
145
|
+
return pickLine(ep.valence < 0 ? 'cite.bad' : 'cite.good', key, { summary: lex?.plainEnglish ? summary : lowerFirst(summary) }, lex);
|
|
115
146
|
}
|
|
116
147
|
/** A rival within this fraction of the chosen utility makes the choice "a close call". */
|
|
117
148
|
const CLOSE_CALL_FRACTION = 0.2;
|
|
@@ -212,13 +243,17 @@ export function voiceLine(p, res, key = res.reason, lexicon) {
|
|
|
212
243
|
}
|
|
213
244
|
case 'modified': {
|
|
214
245
|
const alt = res.counterOffer?.label ?? phraseLine('voice.modified.default', {}, lex);
|
|
215
|
-
return pickLine('voice.modified', key, { alt: lowerFirst(alt) }, lex);
|
|
246
|
+
return pickLine('voice.modified', key, { alt: lex?.plainEnglish ? alt : lowerFirst(alt) }, lex);
|
|
216
247
|
}
|
|
217
248
|
case 'refused': {
|
|
218
249
|
if (res.reason === 'distrust') {
|
|
219
250
|
const ep = res.episodeId ? p.memory.episodes.find((e) => e.id === res.episodeId) : undefined;
|
|
220
251
|
if (ep)
|
|
221
|
-
return pickLine('voice.distrust.episode', key, {
|
|
252
|
+
return pickLine('voice.distrust.episode', key, {
|
|
253
|
+
what: lex?.plainEnglish
|
|
254
|
+
? ep.summary.replace(/[.!?]$/, '')
|
|
255
|
+
: lowerFirst(ep.summary.replace(/\.$/, '')),
|
|
256
|
+
}, lex);
|
|
222
257
|
return pickLine('voice.distrust', key, {}, lex);
|
|
223
258
|
}
|
|
224
259
|
if (res.kind === 'willNot' && res.commitmentId !== undefined) {
|
|
@@ -228,7 +263,7 @@ export function voiceLine(p, res, key = res.reason, lexicon) {
|
|
|
228
263
|
: duty?.label
|
|
229
264
|
? duty.label
|
|
230
265
|
: phraseLine('voice.omission.default', {}, lex);
|
|
231
|
-
return pickLine('voice.omission', key, { what }, lex);
|
|
266
|
+
return pickLine('voice.omission', key, { what: labelLine(what, lex) }, lex);
|
|
232
267
|
}
|
|
233
268
|
if (res.kind === 'willNot' && res.reason.startsWith('duty:')) {
|
|
234
269
|
// An abstention held as a duty (the fast): the veto names what is being kept, not a forbidden act.
|
|
@@ -296,6 +331,13 @@ export function describePerson(p, lexicon) {
|
|
|
296
331
|
const act = p.activity
|
|
297
332
|
? `${p.activity.action}${p.activity.targetId ? ` (${p.activity.targetId})` : ''}`
|
|
298
333
|
: phraseLine('describe.idle', {}, lex);
|
|
334
|
+
if (lex?.plainEnglish) {
|
|
335
|
+
const state = bits.length > 0 ? bits.join(', ') : phraseLine('describe.well', {}, lex);
|
|
336
|
+
const activity = p.activity
|
|
337
|
+
? `${p.name} is doing this: ${p.activity.affordance.label}${p.activity.targetId ? ` (${nameOf(p, p.activity.targetId, lex)})` : ''}.`
|
|
338
|
+
: `${p.name} is ${phraseLine('describe.idle', {}, lex)}.`;
|
|
339
|
+
return phraseLine('describe.line', { name: p.name, act: lex.lines?.['describe.line'] ? act : activity, state }, lex);
|
|
340
|
+
}
|
|
299
341
|
const state = bits.length > 0 ? bits.join(', ') : phraseLine('describe.well', {}, lex);
|
|
300
342
|
return phraseLine('describe.line', { name: p.name, act, state }, lex);
|
|
301
343
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE: literal English narration for hosts that opt in to plain English. Keys, slot names,
|
|
3
|
+
* template counts and ordering match the legacy English pack, preserving deterministic selection.
|
|
4
|
+
* Internal word and intention slots remain phrases for composition. This pack changes presentation
|
|
5
|
+
* only; it does not change simulation state, choose templates, or rewrite host-authored text.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PLAIN_EN_LINES: Readonly<Record<string, readonly string[]>>;
|