@adam0white/human-framework 2.3.0 → 2.5.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 +1 -1
- package/dist/body/body.d.ts +17 -0
- package/dist/body/body.js +37 -0
- package/dist/cognition/cognition.d.ts +3 -0
- package/dist/cognition/cognition.js +29 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/migrate.js +11 -1
- package/dist/person.d.ts +15 -0
- package/dist/person.js +31 -3
- package/dist/promptings/index.d.ts +1 -0
- package/dist/promptings/index.js +1 -0
- package/dist/promptings/promptings.d.ts +100 -0
- package/dist/promptings/promptings.js +348 -0
- package/dist/prospection/forecast.d.ts +45 -0
- package/dist/prospection/forecast.js +103 -0
- package/dist/prospection/index.d.ts +1 -0
- package/dist/prospection/index.js +1 -0
- package/dist/restore.js +3 -0
- package/dist/types.d.ts +75 -2
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -348,7 +348,7 @@ Import these functions from the package. Rules/evidence live in optional `Person
|
|
|
348
348
|
|
|
349
349
|
## Receiving a possible future route
|
|
350
350
|
|
|
351
|
-
|
|
351
|
+
HF 2.3.0 lets an attended explanation connect one current action to one possible successor. The host supplies the explanation the person received, not a private success bonus.
|
|
352
352
|
|
|
353
353
|
```ts
|
|
354
354
|
import {
|
package/dist/body/body.d.ts
CHANGED
|
@@ -189,3 +189,20 @@ export interface BodyThresholds {
|
|
|
189
189
|
* then refines minute by minute inside the crossing step. Does not mutate `p`.
|
|
190
190
|
*/
|
|
191
191
|
export declare function nextBodyThreshold(p: Person, load: BodyLoad, mods: LifeModifiers, thresholds?: BodyThresholds, horizon?: number, params?: BodyParams, perception?: BodyPerceptionContext): number;
|
|
192
|
+
/** One forecast span: where it ends and the worst perceived readout during it. */
|
|
193
|
+
export interface BodyForecastSpan {
|
|
194
|
+
label: string;
|
|
195
|
+
endsAt: Minute;
|
|
196
|
+
/** Worst perceived fatigue, sleepiness, hunger or thirst during the span (0..1), and which. */
|
|
197
|
+
worst: Unit;
|
|
198
|
+
worstOf: 'fatigue' | 'sleepiness' | 'hunger' | 'thirst';
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The person's own forecast of their body through consecutive spans of known exertion, read as they perceive it
|
|
202
|
+
* (2.5.0). Simulates a cloned body in steps of at most `step` minutes; pure (does not mutate `p`).
|
|
203
|
+
*/
|
|
204
|
+
export declare function forecastBody(p: Person, spans: readonly {
|
|
205
|
+
label: string;
|
|
206
|
+
duration: number;
|
|
207
|
+
load: BodyLoad;
|
|
208
|
+
}[], mods: LifeModifiers, params?: BodyParams, step?: number): BodyForecastSpan[];
|
package/dist/body/body.js
CHANGED
|
@@ -763,3 +763,40 @@ export function nextBodyThreshold(p, load, mods, thresholds = { hunger: 0.7, thi
|
|
|
763
763
|
}
|
|
764
764
|
return Number.POSITIVE_INFINITY;
|
|
765
765
|
}
|
|
766
|
+
/**
|
|
767
|
+
* The person's own forecast of their body through consecutive spans of known exertion, read as they perceive it
|
|
768
|
+
* (2.5.0). Simulates a cloned body in steps of at most `step` minutes; pure (does not mutate `p`).
|
|
769
|
+
*/
|
|
770
|
+
export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30) {
|
|
771
|
+
const out = [];
|
|
772
|
+
const sim = { ...p, body: cloneBody(p.body) };
|
|
773
|
+
for (const s of spans) {
|
|
774
|
+
const span = { label: s.label, endsAt: sim.now, worst: 0, worstOf: 'fatigue' };
|
|
775
|
+
// Hard internal cap (a week) so a hostile span cannot stall a decision.
|
|
776
|
+
let left = Math.min(7 * MINUTES_PER_DAY, Math.max(0, Number.isFinite(s.duration) ? s.duration : 0));
|
|
777
|
+
advanceBody(sim, 0, s.load, mods, params);
|
|
778
|
+
const r0 = readBody(sim, params).perceived;
|
|
779
|
+
for (const k of ['fatigue', 'sleepiness', 'hunger', 'thirst'])
|
|
780
|
+
if (r0[k] > span.worst) {
|
|
781
|
+
span.worst = r0[k];
|
|
782
|
+
span.worstOf = k;
|
|
783
|
+
}
|
|
784
|
+
while (left > 0 && sim.body.alive) {
|
|
785
|
+
const dt = Math.min(step, left);
|
|
786
|
+
advanceBody(sim, dt, s.load, mods, params);
|
|
787
|
+
sim.now += dt;
|
|
788
|
+
left -= dt;
|
|
789
|
+
const r = readBody(sim, params).perceived;
|
|
790
|
+
for (const k of ['fatigue', 'sleepiness', 'hunger', 'thirst'])
|
|
791
|
+
if (r[k] > span.worst) {
|
|
792
|
+
span.worst = r[k];
|
|
793
|
+
span.worstOf = k;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (!sim.body.alive)
|
|
797
|
+
span.worst = 1;
|
|
798
|
+
span.endsAt = sim.now;
|
|
799
|
+
out.push(span);
|
|
800
|
+
}
|
|
801
|
+
return out;
|
|
802
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type HabitContext } from '../habits/index.ts';
|
|
2
|
+
import { type PromptingSeen } from '../promptings/index.ts';
|
|
2
3
|
import type { Affordance, BodyReadout, Command, Considered, DecisionRecord, LifeModifiers, Minute, NeedReading, Person, Suggestion, Term, Unit } from '../types.ts';
|
|
3
4
|
import { type CommandOutcome, type WillContext } from '../will/index.ts';
|
|
4
5
|
export declare const COGNITION_DEFAULTS: {
|
|
@@ -132,6 +133,8 @@ export interface Decision {
|
|
|
132
133
|
needDeltas: Partial<Record<'autonomy', number>>;
|
|
133
134
|
/** How a command in force fared (absent when none). */
|
|
134
135
|
command?: CommandOutcome;
|
|
136
|
+
/** Prompts noticed at this decision point (only with `p.promptings`); the composite records them. */
|
|
137
|
+
prompts?: PromptingSeen[];
|
|
135
138
|
}
|
|
136
139
|
/** Highest physiological urgency: the input to the necessity exception. */
|
|
137
140
|
export declare function desperationOf(needs: NeedReading[]): number;
|
|
@@ -34,7 +34,8 @@ import { expectedEffect } from '../memory/index.js';
|
|
|
34
34
|
import { methodEvidence } from '../methods/index.js';
|
|
35
35
|
import { urgencyOf } from '../needs/index.js';
|
|
36
36
|
import { partneringTerms } from '../partnering/index.js';
|
|
37
|
-
import {
|
|
37
|
+
import { promptIn, promptingTerms } from '../promptings/index.js';
|
|
38
|
+
import { bodyForecastTerm, receivedGoalRoute } from '../prospection/index.js';
|
|
38
39
|
import { companionSteadiness, socialTerms } from '../social/index.js';
|
|
39
40
|
import { PHYSIOLOGICAL_NEEDS, VALUE_KEYS } from '../types.js';
|
|
40
41
|
import { adviceWeight, resolveChoice, safeUtility, standingAdvice, suggestionTargets, voicesIn, } from '../will/index.js';
|
|
@@ -348,6 +349,12 @@ export function consider(p, aff, ctx) {
|
|
|
348
349
|
const effort = effortValue(p, aff, ctx);
|
|
349
350
|
if (effort < 0)
|
|
350
351
|
push('effort', effort);
|
|
352
|
+
// Bodily forecast (2.5.0): only offers that declare known follow-on exertion.
|
|
353
|
+
if (aff.followedBy?.length) {
|
|
354
|
+
const t = bodyForecastTerm(p, aff, ctx.mods);
|
|
355
|
+
if (t)
|
|
356
|
+
push(t.source, t.value);
|
|
357
|
+
}
|
|
351
358
|
// Risk: chance × severity × (fear + emotionality).
|
|
352
359
|
if (aff.risk && aff.risk.chance > 0 && aff.risk.severity > 0) {
|
|
353
360
|
const fear = emotionLevel(p, 'fear');
|
|
@@ -398,6 +405,10 @@ export function consider(p, aff, ctx) {
|
|
|
398
405
|
if (!inBreak(p))
|
|
399
406
|
for (const t of rememberedTerms(p, aff, ctx.now, voices))
|
|
400
407
|
push(t.source, t.value);
|
|
408
|
+
// Promptings (2.4.0, opt-in): a recurrence pull under `fault` and a practised turning-away from a noticed prompt.
|
|
409
|
+
if (p.promptings)
|
|
410
|
+
for (const t of promptingTerms(p, aff.action, terms, aff.tags, ctx.now, ctx.desperation))
|
|
411
|
+
push(t.source, t.value);
|
|
401
412
|
let utility = 0;
|
|
402
413
|
for (const t of terms)
|
|
403
414
|
utility += t.value;
|
|
@@ -471,5 +482,22 @@ export function decide(p, affordances, ctx) {
|
|
|
471
482
|
const out = { record, needDeltas };
|
|
472
483
|
if (res.command)
|
|
473
484
|
out.command = res.command;
|
|
485
|
+
if (p.promptings) {
|
|
486
|
+
const prompts = [];
|
|
487
|
+
const tagsOf = new Map(sorted.map((a) => [a.id, a.tags]));
|
|
488
|
+
for (const c of res.considered) {
|
|
489
|
+
const seen = promptIn(c.terms, tagsOf.get(c.affordanceId));
|
|
490
|
+
if (seen)
|
|
491
|
+
prompts.push({
|
|
492
|
+
affordanceId: c.affordanceId,
|
|
493
|
+
action: c.action,
|
|
494
|
+
...seen,
|
|
495
|
+
// A vetoed option was not turned from: the will blocked it, so the response earns no practice.
|
|
496
|
+
turnApplied: c.vetoed === undefined && c.terms.some((t) => t.source === 'promptings:turn'),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
if (prompts.length > 0)
|
|
500
|
+
out.prompts = prompts;
|
|
501
|
+
}
|
|
474
502
|
return out;
|
|
475
503
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework v1 public surface. */
|
|
2
|
-
export declare const FRAMEWORK_VERSION = "2.
|
|
2
|
+
export declare const FRAMEWORK_VERSION = "2.5.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, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
|
|
@@ -28,8 +28,9 @@ export * from './narrate/index.ts';
|
|
|
28
28
|
export * from './needs/index.ts';
|
|
29
29
|
export * from './partnering/index.ts';
|
|
30
30
|
export * from './person.ts';
|
|
31
|
+
export * from './promptings/index.ts';
|
|
31
32
|
export type { ConsequenceEvidence } from './prospection/index.ts';
|
|
32
|
-
export { consequenceEvidence, PROSPECTION_DEFAULTS } from './prospection/index.ts';
|
|
33
|
+
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.ts';
|
|
33
34
|
export * from './restore.ts';
|
|
34
35
|
export * from './scenarios/index.ts';
|
|
35
36
|
export * from './sim/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework v1 public surface. */
|
|
2
|
-
export const FRAMEWORK_VERSION = '2.
|
|
2
|
+
export const FRAMEWORK_VERSION = '2.5.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, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
|
|
@@ -25,7 +25,8 @@ export * from './narrate/index.js';
|
|
|
25
25
|
export * from './needs/index.js';
|
|
26
26
|
export * from './partnering/index.js';
|
|
27
27
|
export * from './person.js';
|
|
28
|
-
export
|
|
28
|
+
export * from './promptings/index.js';
|
|
29
|
+
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.js';
|
|
29
30
|
export * from './restore.js';
|
|
30
31
|
export * from './scenarios/index.js';
|
|
31
32
|
export * from './sim/index.js';
|
package/dist/migrate.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* field of the optional surroundings slice (`ambient.now` → `ambient.percept`) and changes no behaviour (the optional `social.impressions` and `social.reserve`, like `retention`, are optional and start
|
|
9
9
|
* absent in a migrated save); `restore` then fills and sanitizes
|
|
10
10
|
* the slices as for any save. 2.0.0 to 2.1.0 adds optional conditional method state, absent
|
|
11
|
-
* until explicitly used, so existing hosts retain their previous behavior; that step stamps the version. 2.1.0 to 2.2.0 preserves optional method records and stamps the version; successful alternatives now retain independent support. 2.2.0 to 2.3.0 stamps the version; received consequences and temporal goals remain absent unless explicitly supplied. A migrated save continues under the current engine's rules: it restores and runs,
|
|
11
|
+
* until explicitly used, so existing hosts retain their previous behavior; that step stamps the version. 2.1.0 to 2.2.0 preserves optional method records and stamps the version; successful alternatives now retain independent support. 2.2.0 to 2.3.0 stamps the version; received consequences and temporal goals remain absent unless explicitly supplied. 2.3.0 to 2.4.0 stamps the version; promptings remain absent unless enabled. 2.4.0 to 2.5.0 stamps the version; the bodily forecast reads only offers that declare `followedBy`. A migrated save continues under the current engine's rules: it restores and runs,
|
|
12
12
|
* but it does not reproduce what the old engine would have done next where the rules changed (1.4.0 to 1.5.0
|
|
13
13
|
* changed how standing advice is heard; 1.8.0 to 1.9.0 changed the omission rule). Saves older than 1.4.0 and unknown versions are refused with an error.
|
|
14
14
|
*
|
|
@@ -58,6 +58,16 @@ export const MIGRATIONS = {
|
|
|
58
58
|
note: 'the surroundings field `ambient.now` is renamed `ambient.percept`; per-person retention is optional and starts absent; behaviour unchanged',
|
|
59
59
|
apply: renameAmbient,
|
|
60
60
|
},
|
|
61
|
+
'2.4.0': {
|
|
62
|
+
to: '2.5.0',
|
|
63
|
+
note: 'person shape unchanged; offers may declare followedBy spans for a bodily forecast; offers without it are unchanged',
|
|
64
|
+
apply: stamp,
|
|
65
|
+
},
|
|
66
|
+
'2.3.0': {
|
|
67
|
+
to: '2.4.0',
|
|
68
|
+
note: 'promptings are optional and start absent; existing hosts retain exact behavior',
|
|
69
|
+
apply: stamp,
|
|
70
|
+
},
|
|
61
71
|
'2.2.0': {
|
|
62
72
|
to: '2.3.0',
|
|
63
73
|
note: 'received consequences and temporal goals are optional; existing hosts retain exact behavior',
|
package/dist/person.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type BodyForecastSpan } from './body/index.ts';
|
|
2
|
+
import { type NafsPattern } from './promptings/index.ts';
|
|
1
3
|
import { type SkillTransfer } from './skills/index.ts';
|
|
2
4
|
import type { Activity, Affordance, BodyLoad, BodyReadout, Command, Commitment, DecisionRecord, LearningDomain, Minute, NeedId, NeedReading, NormDefinition, Outcome, Percept, Person, PersonSpec, Retention, Suggestion, SuggestionResolution, Unit } from './types.ts';
|
|
3
5
|
import { type CommandOutcome } from './will/index.ts';
|
|
@@ -230,3 +232,16 @@ export declare function finish(p: Person, outcome: Outcome, opts?: FinishOptions
|
|
|
230
232
|
* entries are ignored; an empty or wholly invalid retention restores the defaults. Changes no decision.
|
|
231
233
|
*/
|
|
232
234
|
export declare function setRetention(p: Person, retention: Retention): void;
|
|
235
|
+
/**
|
|
236
|
+
* The nafs lens (2.4.0, read-only; see `promptings/` SCOPE): the three Qur'anic descriptions of one self (12:53,
|
|
237
|
+
* 75:2, 89:27), each with the matching evidence in existing state. Descriptions, never a level or a rank.
|
|
238
|
+
*/
|
|
239
|
+
export declare function nafsReading(p: Person): NafsPattern[];
|
|
240
|
+
/**
|
|
241
|
+
* The person's own forecast of their body through an offer and its `followedBy` spans (2.5.0, read-only), with a
|
|
242
|
+
* first-person line ("I can do it, but I'll be spent for the descent."). Empty spans without `followedBy`.
|
|
243
|
+
*/
|
|
244
|
+
export declare function bodyForecast(p: Person, aff: Affordance): {
|
|
245
|
+
spans: BodyForecastSpan[];
|
|
246
|
+
line: string;
|
|
247
|
+
};
|
package/dist/person.js
CHANGED
|
@@ -26,7 +26,7 @@ import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBo
|
|
|
26
26
|
import { ageCharacter, enableCharacterChange, noteCharacterDay, noteCharacterSocial, } from './character/index.js';
|
|
27
27
|
import { closeDay, enableYearbook, endDay, noteAnswer, noteCommitments, noteDecision, noteMood, noteOutcome, trimChronicle, } from './chronicle/index.js';
|
|
28
28
|
import { decide as cognitionDecide, desperationOf, scoreAll } from './cognition/index.js';
|
|
29
|
-
import { createConscience, heldNorms, normVeto, recordDeed, recordRepair, repent, } from './conscience/index.js';
|
|
29
|
+
import { createConscience, guiltLoad, heldNorms, normVeto, recordDeed, recordRepair, repent, } from './conscience/index.js';
|
|
30
30
|
import { clamp01, clampSigned, createRng, dayOf, isObj, minuteOfDay } from './core/index.js';
|
|
31
31
|
import { ambientBodyParams, ambientModifiers, ambientMood, ambientNeeds } from './environment/index.js';
|
|
32
32
|
import { aptitudeOf, createFamily, pregnancyModifiers } from './family/index.js';
|
|
@@ -36,7 +36,8 @@ import { advanceMemory, createMemory, enableGists, foldGists, learnOutcome, reca
|
|
|
36
36
|
import { intentionFor, narrateDecision, voiceLine } from './narrate/index.js';
|
|
37
37
|
import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisfy, urgencyOf, } from './needs/index.js';
|
|
38
38
|
import { widow } from './partnering/index.js';
|
|
39
|
-
import {
|
|
39
|
+
import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
|
|
40
|
+
import { bodyForecastFor, consumeConsequences, forecastLine, pruneConsequences, receiveConsequences, } from './prospection/index.js';
|
|
40
41
|
import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
|
|
41
42
|
import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
|
|
42
43
|
import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA, PHYSIOLOGICAL_NEEDS, PSYCHOLOGICAL_NEEDS, TRAIT_KEYS, VALUE_KEYS, } from './types.js';
|
|
@@ -153,6 +154,8 @@ export function createPerson(spec) {
|
|
|
153
154
|
enableCharacterChange(p);
|
|
154
155
|
if (on?.skillRetention)
|
|
155
156
|
enableSkillRetention(p);
|
|
157
|
+
if (on?.promptings)
|
|
158
|
+
enablePromptings(p, on.promptings);
|
|
156
159
|
return p;
|
|
157
160
|
}
|
|
158
161
|
/**
|
|
@@ -817,7 +820,7 @@ export function decide(p, affordances, opts = {}) {
|
|
|
817
820
|
const review = p.activity !== null;
|
|
818
821
|
if (review)
|
|
819
822
|
ctx.quiet = true;
|
|
820
|
-
const { record, needDeltas, command: co } = cognitionDecide(p, list, ctx);
|
|
823
|
+
const { record, needDeltas, command: co, prompts } = cognitionDecide(p, list, ctx);
|
|
821
824
|
if (review)
|
|
822
825
|
record.review = true;
|
|
823
826
|
const interruptReason = opts.interrupt ?? p.activity?.interrupt?.reason;
|
|
@@ -831,6 +834,12 @@ export function decide(p, affordances, opts = {}) {
|
|
|
831
834
|
record.narration = narrateDecision(p, record);
|
|
832
835
|
const resolutions = resolutionsOf(record);
|
|
833
836
|
const continuing = review && (record.chosenAffordanceId === null || record.chosenAffordanceId === p.activity?.affordanceId);
|
|
837
|
+
// Promptings (opt-in): a review that keeps the running activity is not a new decision point for a prompt.
|
|
838
|
+
if (prompts && !continuing) {
|
|
839
|
+
const noted = notePromptings(p, prompts, record.chosenAffordanceId, p.now);
|
|
840
|
+
if (noted.guilt > 0)
|
|
841
|
+
feel(p, 'guilt', noted.guilt, 'prompting', p.now);
|
|
842
|
+
}
|
|
834
843
|
for (const res of resolutions) {
|
|
835
844
|
res.says = voiceLine(p, res, record.id);
|
|
836
845
|
if (res.verdict === 'complied' && !continuing) {
|
|
@@ -1390,3 +1399,22 @@ export function setRetention(p, retention) {
|
|
|
1390
1399
|
p.trace.splice(0, p.trace.length - keep);
|
|
1391
1400
|
trimChronicle(p);
|
|
1392
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* The nafs lens (2.4.0, read-only; see `promptings/` SCOPE): the three Qur'anic descriptions of one self (12:53,
|
|
1404
|
+
* 75:2, 89:27), each with the matching evidence in existing state. Descriptions, never a level or a rank.
|
|
1405
|
+
*/
|
|
1406
|
+
export function nafsReading(p) {
|
|
1407
|
+
let guilt = 0;
|
|
1408
|
+
for (const e of p.affect.emotions)
|
|
1409
|
+
if (e.id === 'guilt' && e.cause !== 'prompting')
|
|
1410
|
+
guilt = Math.max(guilt, e.intensity);
|
|
1411
|
+
return nafsPatterns(p, { guiltLoad: guiltLoad(p), guilt });
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* The person's own forecast of their body through an offer and its `followedBy` spans (2.5.0, read-only), with a
|
|
1415
|
+
* first-person line ("I can do it, but I'll be spent for the descent."). Empty spans without `followedBy`.
|
|
1416
|
+
*/
|
|
1417
|
+
export function bodyForecast(p, aff) {
|
|
1418
|
+
const spans = bodyForecastFor(p, aff, lifeModifiers(p));
|
|
1419
|
+
return { spans, line: forecastLine(spans) };
|
|
1420
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './promptings.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './promptings.js';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Minute, Person, PromptingReading, PromptingRecord, PromptingResponse, PromptingResponseKind, PromptingsSpec, PromptingsState, Term, Unit } from '../types.ts';
|
|
2
|
+
export declare const PROMPTINGS_DEFAULTS: {
|
|
3
|
+
/** Pull (utility units) from which a pull against a held norm or goal is noticed. */
|
|
4
|
+
readonly noticeAt: 0.15;
|
|
5
|
+
/** Largest turning-away term at full practice (utility units). */
|
|
6
|
+
readonly turnMax: 0.8;
|
|
7
|
+
/** No turning-away term at or above this desperation (same scale as will's survival desperation). */
|
|
8
|
+
readonly survivalDesperation: 0.75;
|
|
9
|
+
/** Practice gained at a prompt where the response held (× (1 - practice)). */
|
|
10
|
+
readonly gainTurned: 0.12;
|
|
11
|
+
/** Practice gained at a prompt where the response was tried but the person assented. */
|
|
12
|
+
readonly gainAttempt: 0.04;
|
|
13
|
+
/** Practice halves over this many days without use. */
|
|
14
|
+
readonly practiceHalfLifeDays: 60;
|
|
15
|
+
/** Guilt intensity at a prompt under the `fault` reading. */
|
|
16
|
+
readonly faultGuilt: 0.15;
|
|
17
|
+
/** Recurrence pull at echo level 1 (utility units); only reached when a host sets `echoRate` > 0. */
|
|
18
|
+
readonly echoScale: 0.5;
|
|
19
|
+
readonly echoHalfLifeDays: 3;
|
|
20
|
+
readonly maxRecent: 64;
|
|
21
|
+
readonly maxResponses: 8;
|
|
22
|
+
readonly maxEcho: 32;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Turn the slice on, or reconfigure it: recent records and echo are kept, and a response whose id is already held
|
|
26
|
+
* keeps its practice and last use (the spec's `practice` only seeds new responses). Returns the state.
|
|
27
|
+
*/
|
|
28
|
+
export declare function enablePromptings(p: Person, spec?: PromptingsSpec): PromptingsState;
|
|
29
|
+
/** Practice of a response at `now`, after disuse decay. Pure. */
|
|
30
|
+
export declare function practiceAt(r: PromptingResponse, now: Minute): Unit;
|
|
31
|
+
/** Whether an option's terms carry a noticed prompt: its strongest pull and the opposing term. Pure. */
|
|
32
|
+
export declare function promptIn(terms: readonly Term[], tags: readonly string[] | undefined): {
|
|
33
|
+
source: string;
|
|
34
|
+
opposedBy: string;
|
|
35
|
+
} | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Extra terms for one option (cognition calls this before summing): the echo pull under `fault`, then the
|
|
38
|
+
* turning-away term when the option carries a prompt. Pure apart from reads; empty without the slice.
|
|
39
|
+
*/
|
|
40
|
+
export declare function promptingTerms(p: Person, action: string, terms: readonly Term[], tags: readonly string[] | undefined, now: Minute, desperation: number): Term[];
|
|
41
|
+
/** A prompt found at a decision point, handed from cognition to the composite. */
|
|
42
|
+
export interface PromptingSeen {
|
|
43
|
+
affordanceId: string;
|
|
44
|
+
action: string;
|
|
45
|
+
source: string;
|
|
46
|
+
opposedBy: string;
|
|
47
|
+
/** Whether a turning-away term was applied. */
|
|
48
|
+
turnApplied: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** What `notePromptings` did, for the composite (the `fault` guilt goes through affect). */
|
|
51
|
+
export interface PromptingsNoted {
|
|
52
|
+
records: PromptingRecord[];
|
|
53
|
+
/** Guilt intensity to feel at the prompt (only under `fault`); 0 otherwise. */
|
|
54
|
+
guilt: Unit;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Record the prompts of one decision point and learn from them. `chosenId` is the chosen affordance (null when
|
|
58
|
+
* nothing was chosen). The only writer of `p.promptings` after `enablePromptings`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function notePromptings(p: Person, seen: readonly PromptingSeen[], chosenId: string | null, now: Minute): PromptingsNoted;
|
|
61
|
+
/** Vocabulary for one prompt under a held reading (narration only; it changes nothing). */
|
|
62
|
+
export declare function promptingLine(reading: PromptingReading, outcome: PromptingRecord['outcome'], kind?: PromptingResponseKind): string;
|
|
63
|
+
/** Read-only summary of recent prompts (a lens; computes nothing new about the person). */
|
|
64
|
+
export interface PromptingsReadingView {
|
|
65
|
+
reading: PromptingReading;
|
|
66
|
+
readingSource?: string;
|
|
67
|
+
prompts: number;
|
|
68
|
+
turned: number;
|
|
69
|
+
passed: number;
|
|
70
|
+
assented: number;
|
|
71
|
+
responses: {
|
|
72
|
+
id: string;
|
|
73
|
+
kind: PromptingResponseKind;
|
|
74
|
+
practice: Unit;
|
|
75
|
+
}[];
|
|
76
|
+
/** Vocabulary for the last few prompts, newest last. */
|
|
77
|
+
lines: string[];
|
|
78
|
+
}
|
|
79
|
+
export declare function promptingsReading(p: Person, now?: Minute): PromptingsReadingView | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* The nafs lens (reconciliation.md §3): the three Qur'anic descriptions of one self, each with the evidence in
|
|
82
|
+
* existing state that matches it. Descriptions, not a level, a ladder or three agents; several can hold at once,
|
|
83
|
+
* and none says anything about rank before God. Inputs are passed in by the composite (open-breach weight and
|
|
84
|
+
* felt guilt from conscience and affect), so this module reads nothing it does not own. Guilt that the `fault` reading
|
|
85
|
+
* feels at a prompt is excluded, so the scrupulous reading is not mapped onto self-reproach (75:2). These mappings
|
|
86
|
+
* from state to description are engineering conventions (C), not interpretations of the verses.
|
|
87
|
+
*/
|
|
88
|
+
export interface NafsPattern {
|
|
89
|
+
name: 'urging' | 'reproaching' | 'at rest';
|
|
90
|
+
/** Qur'anic description it borrows (12:53 ammara, 75:2 lawwama, 89:27 mutmaʾinna). */
|
|
91
|
+
ref: string;
|
|
92
|
+
evident: boolean;
|
|
93
|
+
evidence: string;
|
|
94
|
+
}
|
|
95
|
+
export declare function nafsPatterns(p: Person, inputs: {
|
|
96
|
+
guiltLoad: Unit;
|
|
97
|
+
guilt: Unit;
|
|
98
|
+
}): NafsPattern[];
|
|
99
|
+
/** Validate a saved slice; a malformed slice is dropped (undefined), malformed entries are skipped. */
|
|
100
|
+
export declare function sanitizePromptings(input: unknown, now: Minute): PromptingsState | undefined;
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE (promptings and turning away, 2.4.0): a *prompt* is a pull toward an option that the person's own held
|
|
3
|
+
* norms or goals oppose, strong enough to be noticed. This module records prompts, lets the person turn from them
|
|
4
|
+
* by a practised response, and keeps a prompt apart from assent: only an assented act reaches `conscience/
|
|
5
|
+
* recordDeed`, so a prompt is never a breach (Bukhari 5269, research/reconciliation.md §3). It is opt-in
|
|
6
|
+
* (`enablePromptings`, or `PersonSpec.enable.promptings`); without it nothing here runs and existing runs replay
|
|
7
|
+
* byte for byte. It draws no randomness.
|
|
8
|
+
*
|
|
9
|
+
* Detection reads an option's traced terms and changes none of them: the pull is the sum of positive `habit`,
|
|
10
|
+
* `emotion:*`, `need:*` and `promptings:echo` terms (every positive term on an offer tagged `temptation`); it is a
|
|
11
|
+
* prompt when the pull reaches `noticeAt` while a `norm:*` or `goal:*` term on the same option is negative.
|
|
12
|
+
* Abstention windows are not detected (they veto, and carry no term). A person with a response gets one extra
|
|
13
|
+
* term, `promptings:turn` = -turnMax × practice, on that option; it is bounded and absent at survival desperation,
|
|
14
|
+
* so a desperate bodily need still wins (2:286). Each prompt is recorded once per decision point (a review that
|
|
15
|
+
* keeps the running activity records nothing) as `assented` (chosen), `turned` (not chosen, a response practised)
|
|
16
|
+
* or `passed` (not chosen, no response). Practice grows with diminishing gains once per decision point where the
|
|
17
|
+
* response was used (more when it held; a vetoed option earns none, since the will blocked it), and halves over `practiceHalfLifeDays` of disuse.
|
|
18
|
+
*
|
|
19
|
+
* Held readings (reconciliation.md §5): `whisper`, `inclination`, `intrusion` and `none` give identical decisions
|
|
20
|
+
* and differ only in the vocabulary of `promptingLine`. `fault` (the scrupulous reading, which both the tradition
|
|
21
|
+
* and clinicians reject) adds a small guilt emotion at the prompt itself, from the person's own understanding;
|
|
22
|
+
* that guilt is an emotion, never a breach, so it changes decisions only where an offer acts through guilt's
|
|
23
|
+
* tendencies. With a host-set `echoRate` > 0, `fault` also gains a recurrence pull on that action
|
|
24
|
+
* (`promptings:echo`), decaying over days: a labelled assumption from contested suppression-rebound evidence
|
|
25
|
+
* (P3), off by default. No reading earns a hidden bonus.
|
|
26
|
+
*
|
|
27
|
+
* Borrowed shapes: everyday desire, conflict and resistance (P1, P2); suppression rebound and redirection (P3,
|
|
28
|
+
* P13); practised if–then responses (P5). Every number is an engineering default, not fitted. Does not claim: the
|
|
29
|
+
* source of any thought, a measure of faith, spiritual rank, reward for resisting (H4 is acknowledged as a held
|
|
30
|
+
* belief and never computed), clinical OCD, or a cessation rate. The lenses (`promptingsReading`, `nafsReading`)
|
|
31
|
+
* only describe existing state; they store and compute nothing new about the person.
|
|
32
|
+
*/
|
|
33
|
+
import { clamp01, dexp, isObj, round } from '../core/index.js';
|
|
34
|
+
import { MINUTES_PER_DAY } from '../types.js';
|
|
35
|
+
export const PROMPTINGS_DEFAULTS = {
|
|
36
|
+
/** Pull (utility units) from which a pull against a held norm or goal is noticed. */
|
|
37
|
+
noticeAt: 0.15,
|
|
38
|
+
/** Largest turning-away term at full practice (utility units). */
|
|
39
|
+
turnMax: 0.8,
|
|
40
|
+
/** No turning-away term at or above this desperation (same scale as will's survival desperation). */
|
|
41
|
+
survivalDesperation: 0.75,
|
|
42
|
+
/** Practice gained at a prompt where the response held (× (1 - practice)). */
|
|
43
|
+
gainTurned: 0.12,
|
|
44
|
+
/** Practice gained at a prompt where the response was tried but the person assented. */
|
|
45
|
+
gainAttempt: 0.04,
|
|
46
|
+
/** Practice halves over this many days without use. */
|
|
47
|
+
practiceHalfLifeDays: 60,
|
|
48
|
+
/** Guilt intensity at a prompt under the `fault` reading. */
|
|
49
|
+
faultGuilt: 0.15,
|
|
50
|
+
/** Recurrence pull at echo level 1 (utility units); only reached when a host sets `echoRate` > 0. */
|
|
51
|
+
echoScale: 0.5,
|
|
52
|
+
echoHalfLifeDays: 3,
|
|
53
|
+
maxRecent: 64,
|
|
54
|
+
maxResponses: 8,
|
|
55
|
+
maxEcho: 32,
|
|
56
|
+
};
|
|
57
|
+
const READINGS = ['whisper', 'inclination', 'intrusion', 'fault', 'none'];
|
|
58
|
+
const KINDS = ['refuge', 'redirect', 'leave'];
|
|
59
|
+
const halve = (value, minutes, halfLifeDays) => minutes <= 0 ? value : value * dexp((-Math.LN2 * minutes) / (halfLifeDays * MINUTES_PER_DAY));
|
|
60
|
+
/**
|
|
61
|
+
* Turn the slice on, or reconfigure it: recent records and echo are kept, and a response whose id is already held
|
|
62
|
+
* keeps its practice and last use (the spec's `practice` only seeds new responses). Returns the state.
|
|
63
|
+
*/
|
|
64
|
+
export function enablePromptings(p, spec = {}) {
|
|
65
|
+
const prev = p.promptings;
|
|
66
|
+
const responses = [];
|
|
67
|
+
for (const r of spec.responses ?? []) {
|
|
68
|
+
if (!isObj(r) || typeof r.id !== 'string' || r.id === '' || !KINDS.includes(r.kind))
|
|
69
|
+
continue;
|
|
70
|
+
if (responses.some((x) => x.id === r.id) || responses.length >= PROMPTINGS_DEFAULTS.maxResponses)
|
|
71
|
+
continue;
|
|
72
|
+
const held = prev?.responses.find((x) => x.id === r.id);
|
|
73
|
+
const seed = Number.isFinite(r.practice) ? clamp01(r.practice) : 0.1;
|
|
74
|
+
responses.push(held ? { ...held, kind: r.kind } : { id: r.id, kind: r.kind, practice: seed, lastUsedAt: p.now });
|
|
75
|
+
}
|
|
76
|
+
const state = {
|
|
77
|
+
reading: spec.reading && READINGS.includes(spec.reading) ? spec.reading : 'none',
|
|
78
|
+
responses,
|
|
79
|
+
recent: prev?.recent ?? [],
|
|
80
|
+
echo: prev?.echo ?? {},
|
|
81
|
+
echoRate: Number.isFinite(spec.echoRate) ? clamp01(spec.echoRate) : 0,
|
|
82
|
+
};
|
|
83
|
+
if (spec.readingSource !== undefined)
|
|
84
|
+
state.readingSource = spec.readingSource;
|
|
85
|
+
p.promptings = state;
|
|
86
|
+
return state;
|
|
87
|
+
}
|
|
88
|
+
/** Practice of a response at `now`, after disuse decay. Pure. */
|
|
89
|
+
export function practiceAt(r, now) {
|
|
90
|
+
return clamp01(halve(r.practice, now - r.lastUsedAt, PROMPTINGS_DEFAULTS.practiceHalfLifeDays));
|
|
91
|
+
}
|
|
92
|
+
/** The response a person would use now: highest practice, ties by id. */
|
|
93
|
+
function bestResponse(s, now) {
|
|
94
|
+
let best;
|
|
95
|
+
for (const r of s.responses) {
|
|
96
|
+
const practice = practiceAt(r, now);
|
|
97
|
+
if (!best || practice > best.practice || (practice === best.practice && r.id < best.r.id))
|
|
98
|
+
best = { r, practice };
|
|
99
|
+
}
|
|
100
|
+
return best;
|
|
101
|
+
}
|
|
102
|
+
const isPull = (source, tagged) => tagged ||
|
|
103
|
+
source === 'habit' ||
|
|
104
|
+
source === 'promptings:echo' ||
|
|
105
|
+
source.startsWith('emotion:') ||
|
|
106
|
+
source.startsWith('need:');
|
|
107
|
+
/** Whether an option's terms carry a noticed prompt: its strongest pull and the opposing term. Pure. */
|
|
108
|
+
export function promptIn(terms, tags) {
|
|
109
|
+
const tagged = tags?.includes('temptation') ?? false;
|
|
110
|
+
let pull = 0;
|
|
111
|
+
let top;
|
|
112
|
+
let opposed;
|
|
113
|
+
for (const t of terms) {
|
|
114
|
+
if (t.source === 'promptings:turn')
|
|
115
|
+
continue;
|
|
116
|
+
if (t.value > 0 && isPull(t.source, tagged)) {
|
|
117
|
+
pull += t.value;
|
|
118
|
+
if (!top || t.value > top.value)
|
|
119
|
+
top = t;
|
|
120
|
+
}
|
|
121
|
+
else if (t.value < 0 &&
|
|
122
|
+
(t.source.startsWith('norm:') || t.source.startsWith('goal:')) &&
|
|
123
|
+
(!opposed || t.value < opposed.value))
|
|
124
|
+
opposed = t;
|
|
125
|
+
}
|
|
126
|
+
if (!top || !opposed || pull < PROMPTINGS_DEFAULTS.noticeAt)
|
|
127
|
+
return undefined;
|
|
128
|
+
return { source: tagged && !isPull(top.source, false) ? 'host' : top.source, opposedBy: opposed.source };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Extra terms for one option (cognition calls this before summing): the echo pull under `fault`, then the
|
|
132
|
+
* turning-away term when the option carries a prompt. Pure apart from reads; empty without the slice.
|
|
133
|
+
*/
|
|
134
|
+
export function promptingTerms(p, action, terms, tags, now, desperation) {
|
|
135
|
+
const s = p.promptings;
|
|
136
|
+
if (!s)
|
|
137
|
+
return [];
|
|
138
|
+
const out = [];
|
|
139
|
+
const e = Object.hasOwn(s.echo, action) ? s.echo[action] : undefined;
|
|
140
|
+
if (e && s.reading === 'fault') {
|
|
141
|
+
const level = halve(e.level, now - e.at, PROMPTINGS_DEFAULTS.echoHalfLifeDays);
|
|
142
|
+
const v = round(PROMPTINGS_DEFAULTS.echoScale * level);
|
|
143
|
+
if (v > 0)
|
|
144
|
+
out.push({ source: 'promptings:echo', value: v });
|
|
145
|
+
}
|
|
146
|
+
if (desperation >= PROMPTINGS_DEFAULTS.survivalDesperation)
|
|
147
|
+
return out;
|
|
148
|
+
const best = bestResponse(s, now);
|
|
149
|
+
if (!best || best.practice <= 0)
|
|
150
|
+
return out;
|
|
151
|
+
if (!promptIn([...terms, ...out], tags))
|
|
152
|
+
return out;
|
|
153
|
+
const v = round(-PROMPTINGS_DEFAULTS.turnMax * best.practice);
|
|
154
|
+
if (v < 0)
|
|
155
|
+
out.push({ source: 'promptings:turn', value: v });
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Record the prompts of one decision point and learn from them. `chosenId` is the chosen affordance (null when
|
|
160
|
+
* nothing was chosen). The only writer of `p.promptings` after `enablePromptings`.
|
|
161
|
+
*/
|
|
162
|
+
export function notePromptings(p, seen, chosenId, now) {
|
|
163
|
+
const s = p.promptings;
|
|
164
|
+
const noted = { records: [], guilt: 0 };
|
|
165
|
+
if (!s || seen.length === 0)
|
|
166
|
+
return noted;
|
|
167
|
+
const D = PROMPTINGS_DEFAULTS;
|
|
168
|
+
const best = bestResponse(s, now);
|
|
169
|
+
// One practice gain per decision point, however many options carried a prompt: the larger one if any held.
|
|
170
|
+
let gain = 0;
|
|
171
|
+
for (const x of seen) {
|
|
172
|
+
const assented = x.affordanceId === chosenId;
|
|
173
|
+
const used = x.turnApplied && best !== undefined;
|
|
174
|
+
const rec = {
|
|
175
|
+
at: now,
|
|
176
|
+
action: x.action,
|
|
177
|
+
affordanceId: x.affordanceId,
|
|
178
|
+
source: x.source,
|
|
179
|
+
opposedBy: x.opposedBy,
|
|
180
|
+
outcome: assented ? 'assented' : used ? 'turned' : 'passed',
|
|
181
|
+
};
|
|
182
|
+
if (used && best) {
|
|
183
|
+
rec.responseId = best.r.id;
|
|
184
|
+
gain = Math.max(gain, assented ? D.gainAttempt : D.gainTurned);
|
|
185
|
+
}
|
|
186
|
+
s.recent.push(rec);
|
|
187
|
+
noted.records.push(rec);
|
|
188
|
+
if (s.reading === 'fault') {
|
|
189
|
+
noted.guilt = Math.max(noted.guilt, D.faultGuilt);
|
|
190
|
+
if (s.echoRate > 0) {
|
|
191
|
+
const prev = Object.hasOwn(s.echo, x.action) ? s.echo[x.action] : undefined;
|
|
192
|
+
const level = prev ? halve(prev.level, now - prev.at, D.echoHalfLifeDays) : 0;
|
|
193
|
+
s.echo[x.action] = { level: round(clamp01(level + s.echoRate * (1 - level))), at: now };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (best && gain > 0) {
|
|
198
|
+
const practice = practiceAt(best.r, now);
|
|
199
|
+
best.r.practice = round(clamp01(practice + gain * (1 - practice)));
|
|
200
|
+
best.r.lastUsedAt = now;
|
|
201
|
+
}
|
|
202
|
+
if (s.recent.length > D.maxRecent)
|
|
203
|
+
s.recent.splice(0, s.recent.length - D.maxRecent);
|
|
204
|
+
const keys = Object.keys(s.echo);
|
|
205
|
+
if (keys.length > D.maxEcho) {
|
|
206
|
+
keys.sort((a, b) => (s.echo[a]?.at ?? 0) - (s.echo[b]?.at ?? 0) || (a < b ? -1 : 1));
|
|
207
|
+
for (const k of keys.slice(0, keys.length - D.maxEcho))
|
|
208
|
+
delete s.echo[k];
|
|
209
|
+
}
|
|
210
|
+
return noted;
|
|
211
|
+
}
|
|
212
|
+
/** Vocabulary for one prompt under a held reading (narration only; it changes nothing). */
|
|
213
|
+
export function promptingLine(reading, outcome, kind) {
|
|
214
|
+
if (outcome === 'assented')
|
|
215
|
+
return reading === 'whisper'
|
|
216
|
+
? 'a whisper came and was followed'
|
|
217
|
+
: reading === 'inclination'
|
|
218
|
+
? 'the pull of the self won'
|
|
219
|
+
: 'an urge came and was acted on';
|
|
220
|
+
const how = outcome === 'turned'
|
|
221
|
+
? kind === 'refuge'
|
|
222
|
+
? 'sought refuge and '
|
|
223
|
+
: kind === 'redirect'
|
|
224
|
+
? 'turned to something else and '
|
|
225
|
+
: ''
|
|
226
|
+
: '';
|
|
227
|
+
switch (reading) {
|
|
228
|
+
case 'whisper':
|
|
229
|
+
return `a whisper came; ${how}let it pass`;
|
|
230
|
+
case 'inclination':
|
|
231
|
+
return `the self pulled; ${how}held back`;
|
|
232
|
+
case 'intrusion':
|
|
233
|
+
return `a thought came, not who I am; ${how}let it go`;
|
|
234
|
+
case 'fault':
|
|
235
|
+
return `a thought came and felt like wrongdoing; ${how}let it pass`;
|
|
236
|
+
default:
|
|
237
|
+
return `an urge came; ${how}it passed`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export function promptingsReading(p, now = p.now) {
|
|
241
|
+
const s = p.promptings;
|
|
242
|
+
if (!s)
|
|
243
|
+
return undefined;
|
|
244
|
+
const count = (o) => s.recent.filter((r) => r.outcome === o).length;
|
|
245
|
+
const kindOf = (id) => s.responses.find((r) => r.id === id)?.kind;
|
|
246
|
+
const view = {
|
|
247
|
+
reading: s.reading,
|
|
248
|
+
prompts: s.recent.length,
|
|
249
|
+
turned: count('turned'),
|
|
250
|
+
passed: count('passed'),
|
|
251
|
+
assented: count('assented'),
|
|
252
|
+
responses: s.responses.map((r) => ({ id: r.id, kind: r.kind, practice: round(practiceAt(r, now)) })),
|
|
253
|
+
lines: s.recent.slice(-5).map((r) => promptingLine(s.reading, r.outcome, kindOf(r.responseId))),
|
|
254
|
+
};
|
|
255
|
+
if (s.readingSource !== undefined)
|
|
256
|
+
view.readingSource = s.readingSource;
|
|
257
|
+
return view;
|
|
258
|
+
}
|
|
259
|
+
export function nafsPatterns(p, inputs) {
|
|
260
|
+
const recent = p.promptings?.recent ?? [];
|
|
261
|
+
const assentedAgainstNorm = recent.filter((r) => r.outcome === 'assented' && r.opposedBy.startsWith('norm:'));
|
|
262
|
+
const letPass = recent.filter((r) => r.outcome !== 'assented').length;
|
|
263
|
+
const reproach = inputs.guiltLoad > 0 || inputs.guilt > 0.05;
|
|
264
|
+
return [
|
|
265
|
+
{
|
|
266
|
+
name: 'urging',
|
|
267
|
+
ref: 'Qur’an 12:53',
|
|
268
|
+
evident: assentedAgainstNorm.length > 0,
|
|
269
|
+
evidence: `${assentedAgainstNorm.length} recent prompts followed against a held norm`,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'reproaching',
|
|
273
|
+
ref: 'Qur’an 75:2',
|
|
274
|
+
evident: reproach,
|
|
275
|
+
evidence: `open-breach weight ${round(inputs.guiltLoad, 2)}, felt guilt ${round(inputs.guilt, 2)}`,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'at rest',
|
|
279
|
+
ref: 'Qur’an 89:27',
|
|
280
|
+
evident: letPass > 0 && assentedAgainstNorm.length === 0 && !reproach,
|
|
281
|
+
evidence: `${letPass} recent prompts let pass, none followed against a norm, no felt reproach`,
|
|
282
|
+
},
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
const unit = (x) => typeof x === 'number' && Number.isFinite(x) && x >= 0 && x <= 1;
|
|
286
|
+
const minute = (x) => typeof x === 'number' && Number.isFinite(x);
|
|
287
|
+
const str = (x) => typeof x === 'string' && x !== '' && x.length <= 200;
|
|
288
|
+
/** Validate a saved slice; a malformed slice is dropped (undefined), malformed entries are skipped. */
|
|
289
|
+
export function sanitizePromptings(input, now) {
|
|
290
|
+
if (!isObj(input) || !Array.isArray(input.responses) || !Array.isArray(input.recent) || !isObj(input.echo))
|
|
291
|
+
return undefined;
|
|
292
|
+
if (typeof input.reading !== 'string' || !READINGS.includes(input.reading))
|
|
293
|
+
return undefined;
|
|
294
|
+
const D = PROMPTINGS_DEFAULTS;
|
|
295
|
+
const responses = [];
|
|
296
|
+
for (const r of input.responses.slice(0, D.maxResponses)) {
|
|
297
|
+
if (!isObj(r) || !str(r.id) || !KINDS.includes(r.kind) || !unit(r.practice))
|
|
298
|
+
continue;
|
|
299
|
+
if (!minute(r.lastUsedAt) || r.lastUsedAt > now || responses.some((x) => x.id === r.id))
|
|
300
|
+
continue;
|
|
301
|
+
responses.push({
|
|
302
|
+
id: r.id,
|
|
303
|
+
kind: r.kind,
|
|
304
|
+
practice: r.practice,
|
|
305
|
+
lastUsedAt: r.lastUsedAt,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
const recent = [];
|
|
309
|
+
for (const r of input.recent.slice(-D.maxRecent)) {
|
|
310
|
+
if (!isObj(r) || !minute(r.at) || r.at > now || !str(r.action) || !str(r.affordanceId))
|
|
311
|
+
continue;
|
|
312
|
+
if (!str(r.source) || !str(r.opposedBy))
|
|
313
|
+
continue;
|
|
314
|
+
if (r.outcome !== 'passed' && r.outcome !== 'turned' && r.outcome !== 'assented')
|
|
315
|
+
continue;
|
|
316
|
+
const rec = {
|
|
317
|
+
at: r.at,
|
|
318
|
+
action: r.action,
|
|
319
|
+
affordanceId: r.affordanceId,
|
|
320
|
+
source: r.source,
|
|
321
|
+
opposedBy: r.opposedBy,
|
|
322
|
+
outcome: r.outcome,
|
|
323
|
+
};
|
|
324
|
+
if (str(r.responseId))
|
|
325
|
+
rec.responseId = r.responseId;
|
|
326
|
+
recent.push(rec);
|
|
327
|
+
}
|
|
328
|
+
const echo = {};
|
|
329
|
+
for (const k of Object.keys(input.echo).slice(0, D.maxEcho)) {
|
|
330
|
+
if (k === '__proto__' || !str(k))
|
|
331
|
+
continue;
|
|
332
|
+
const e = input.echo[k];
|
|
333
|
+
if (!isObj(e) || !unit(e.level) || !minute(e.at) || e.at > now)
|
|
334
|
+
continue;
|
|
335
|
+
echo[k] = { level: e.level, at: e.at };
|
|
336
|
+
}
|
|
337
|
+
const rate = input.echoRate;
|
|
338
|
+
const out = {
|
|
339
|
+
reading: input.reading,
|
|
340
|
+
responses,
|
|
341
|
+
recent,
|
|
342
|
+
echo,
|
|
343
|
+
echoRate: typeof rate === 'number' && Number.isFinite(rate) && rate >= 0 ? Math.min(1, rate) : 0,
|
|
344
|
+
};
|
|
345
|
+
if (str(input.readingSource))
|
|
346
|
+
out.readingSource = input.readingSource;
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE (bodily forecast, 2.5.0): a person anticipates their own body during exertion they know must follow an
|
|
3
|
+
* act ("I can reach the top, but I'll be spent for the descent"). The host declares the follow-on spans on the
|
|
4
|
+
* offer (`Affordance.followedBy`); the person simulates a copy of their body through the act and those spans with
|
|
5
|
+
* the ordinary body model, reads it as they perceive it, and weighs the worst perceived fatigue, sleepiness,
|
|
6
|
+
* hunger or thirst during the follow-on spans. One term, `forecast:body` = -forecastScale × smoothstep(comfortAt,
|
|
7
|
+
* spentAt, worst), joins the offer's utility; it is pure and draws no randomness. Offers without `followedBy`
|
|
8
|
+
* get no forecast, so existing hosts continue exactly.
|
|
9
|
+
*
|
|
10
|
+
* The forecast inherits the body model's limits and the person's misperceptions: HF exertion relaxes toward the
|
|
11
|
+
* current effort (no cumulative glycogen or muscle-damage store), so a long climb does not by itself leave a
|
|
12
|
+
* person more spent for the descent unless sleepiness, hunger, thirst or the descent's own effort do; unfelt
|
|
13
|
+
* sleep debt stays unfelt in the forecast; a fast's damped hunger and thirst are not applied (so a fasting
|
|
14
|
+
* person forecasts them undamped); host surroundings are not applied (the person's own body rates and life
|
|
15
|
+
* modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
|
|
16
|
+
* open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
|
|
17
|
+
* (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
|
|
18
|
+
* predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
|
|
19
|
+
* default.
|
|
20
|
+
*/
|
|
21
|
+
import { type BodyForecastSpan } from '../body/index.ts';
|
|
22
|
+
import type { Affordance, LifeModifiers, Person, Term } from '../types.ts';
|
|
23
|
+
export declare const FORECAST_DEFAULTS: {
|
|
24
|
+
/** Worst perceived readout during a follow-on from which the forecast starts to weigh. */
|
|
25
|
+
readonly comfortAt: 0.8;
|
|
26
|
+
/** Worst perceived readout at which the person expects to be spent (full weight). */
|
|
27
|
+
readonly spentAt: 0.97;
|
|
28
|
+
/** Weight at full expected spending (utility units; the effort term is on the order of 0.1–1). */
|
|
29
|
+
readonly forecastScale: 1.2;
|
|
30
|
+
/** At most this many follow-on spans are read, and at most this many minutes in total. */
|
|
31
|
+
readonly maxSpans: 4;
|
|
32
|
+
readonly maxMinutes: number;
|
|
33
|
+
};
|
|
34
|
+
/** The person's forecast for an offer: the act, then its follow-on spans. Empty without `followedBy`. Pure. */
|
|
35
|
+
export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeModifiers): BodyForecastSpan[];
|
|
36
|
+
/**
|
|
37
|
+
* The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
|
|
38
|
+
* design: it weighs being in that state *while doing* the follow-on, so a person already sleepy or thirsty is
|
|
39
|
+
* charged for exerting so, while an offer with no declared follow-on is not (a night hike with a declared descent
|
|
40
|
+
* costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
|
|
41
|
+
* too, which cancelled the very case the forecast exists for. Pure.
|
|
42
|
+
*/
|
|
43
|
+
export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers): Term | undefined;
|
|
44
|
+
/** A first-person line for the forecast (narration only). */
|
|
45
|
+
export declare function forecastLine(spans: readonly BodyForecastSpan[]): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE (bodily forecast, 2.5.0): a person anticipates their own body during exertion they know must follow an
|
|
3
|
+
* act ("I can reach the top, but I'll be spent for the descent"). The host declares the follow-on spans on the
|
|
4
|
+
* offer (`Affordance.followedBy`); the person simulates a copy of their body through the act and those spans with
|
|
5
|
+
* the ordinary body model, reads it as they perceive it, and weighs the worst perceived fatigue, sleepiness,
|
|
6
|
+
* hunger or thirst during the follow-on spans. One term, `forecast:body` = -forecastScale × smoothstep(comfortAt,
|
|
7
|
+
* spentAt, worst), joins the offer's utility; it is pure and draws no randomness. Offers without `followedBy`
|
|
8
|
+
* get no forecast, so existing hosts continue exactly.
|
|
9
|
+
*
|
|
10
|
+
* The forecast inherits the body model's limits and the person's misperceptions: HF exertion relaxes toward the
|
|
11
|
+
* current effort (no cumulative glycogen or muscle-damage store), so a long climb does not by itself leave a
|
|
12
|
+
* person more spent for the descent unless sleepiness, hunger, thirst or the descent's own effort do; unfelt
|
|
13
|
+
* sleep debt stays unfelt in the forecast; a fast's damped hunger and thirst are not applied (so a fasting
|
|
14
|
+
* person forecasts them undamped); host surroundings are not applied (the person's own body rates and life
|
|
15
|
+
* modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
|
|
16
|
+
* open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
|
|
17
|
+
* (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
|
|
18
|
+
* predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
|
|
19
|
+
* default.
|
|
20
|
+
*/
|
|
21
|
+
import { forecastBody } from '../body/index.js';
|
|
22
|
+
import { clamp01, round } from '../core/index.js';
|
|
23
|
+
export const FORECAST_DEFAULTS = {
|
|
24
|
+
/** Worst perceived readout during a follow-on from which the forecast starts to weigh. */
|
|
25
|
+
comfortAt: 0.8,
|
|
26
|
+
/** Worst perceived readout at which the person expects to be spent (full weight). */
|
|
27
|
+
spentAt: 0.97,
|
|
28
|
+
/** Weight at full expected spending (utility units; the effort term is on the order of 0.1–1). */
|
|
29
|
+
forecastScale: 1.2,
|
|
30
|
+
/** At most this many follow-on spans are read, and at most this many minutes in total. */
|
|
31
|
+
maxSpans: 4,
|
|
32
|
+
maxMinutes: 24 * 60,
|
|
33
|
+
};
|
|
34
|
+
const smooth = (a, b, x) => {
|
|
35
|
+
const t = clamp01((x - a) / (b - a));
|
|
36
|
+
return t * t * (3 - 2 * t);
|
|
37
|
+
};
|
|
38
|
+
/** The person's forecast for an offer: the act, then its follow-on spans. Empty without `followedBy`. Pure. */
|
|
39
|
+
export function bodyForecastFor(p, aff, mods) {
|
|
40
|
+
const follow = aff.followedBy;
|
|
41
|
+
if (!Array.isArray(follow) || follow.length === 0 || !p.body.alive)
|
|
42
|
+
return [];
|
|
43
|
+
const D = FORECAST_DEFAULTS;
|
|
44
|
+
const d0 = Number.isFinite(aff.duration) ? Math.min(D.maxMinutes, Math.max(0, aff.duration)) : 0;
|
|
45
|
+
const spans = [
|
|
46
|
+
{
|
|
47
|
+
label: aff.label,
|
|
48
|
+
duration: d0,
|
|
49
|
+
load: {
|
|
50
|
+
effort: clamp01(aff.effort),
|
|
51
|
+
focus: clamp01(aff.focus ?? 0),
|
|
52
|
+
mode: aff.mode ?? 'awake',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
let total = spans[0]?.duration ?? 0;
|
|
57
|
+
for (const f of follow.slice(0, D.maxSpans)) {
|
|
58
|
+
if (!f || typeof f !== 'object')
|
|
59
|
+
continue;
|
|
60
|
+
const d = Number.isFinite(f.duration) ? Math.max(0, f.duration) : 0;
|
|
61
|
+
if (total + d > D.maxMinutes)
|
|
62
|
+
break;
|
|
63
|
+
total += d;
|
|
64
|
+
spans.push({
|
|
65
|
+
label: f.label ?? 'after',
|
|
66
|
+
duration: d,
|
|
67
|
+
load: { effort: clamp01(f.effort), focus: clamp01(f.focus ?? 0), mode: 'awake' },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return forecastBody(p, spans, mods);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
|
|
74
|
+
* design: it weighs being in that state *while doing* the follow-on, so a person already sleepy or thirsty is
|
|
75
|
+
* charged for exerting so, while an offer with no declared follow-on is not (a night hike with a declared descent
|
|
76
|
+
* costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
|
|
77
|
+
* too, which cancelled the very case the forecast exists for. Pure.
|
|
78
|
+
*/
|
|
79
|
+
export function bodyForecastTerm(p, aff, mods) {
|
|
80
|
+
const spans = bodyForecastFor(p, aff, mods).slice(1);
|
|
81
|
+
if (spans.length === 0)
|
|
82
|
+
return undefined;
|
|
83
|
+
const D = FORECAST_DEFAULTS;
|
|
84
|
+
const worst = Math.max(...spans.map((s) => s.worst));
|
|
85
|
+
const v = round(-D.forecastScale * smooth(D.comfortAt, D.spentAt, worst));
|
|
86
|
+
return v < 0 ? { source: 'forecast:body', value: v } : undefined;
|
|
87
|
+
}
|
|
88
|
+
/** A first-person line for the forecast (narration only). */
|
|
89
|
+
export function forecastLine(spans) {
|
|
90
|
+
const after = spans.slice(1);
|
|
91
|
+
if (after.length === 0)
|
|
92
|
+
return '';
|
|
93
|
+
let w = after[0];
|
|
94
|
+
for (const s of after)
|
|
95
|
+
if (s.worst > w.worst)
|
|
96
|
+
w = s;
|
|
97
|
+
const what = { fatigue: 'spent', sleepiness: 'too sleepy', hunger: 'too hungry', thirst: 'too thirsty' }[w.worstOf];
|
|
98
|
+
if (w.worst >= FORECAST_DEFAULTS.spentAt)
|
|
99
|
+
return `I can do it, but I'll be ${what} for ${w.label}.`;
|
|
100
|
+
if (w.worst >= FORECAST_DEFAULTS.comfortAt)
|
|
101
|
+
return `I can do it, though ${w.label} will be hard.`;
|
|
102
|
+
return `I'll still have enough for ${w.label}.`;
|
|
103
|
+
}
|
package/dist/restore.js
CHANGED
|
@@ -20,6 +20,7 @@ import { sanitizeMethods } from './methods/index.js';
|
|
|
20
20
|
import { migrate } from './migrate.js';
|
|
21
21
|
import { sanitizeBonds } from './partnering/index.js';
|
|
22
22
|
import { cleanRetention, createPerson, traceLimit } from './person.js';
|
|
23
|
+
import { sanitizePromptings } from './promptings/index.js';
|
|
23
24
|
import { sanitizeProspection } from './prospection/index.js';
|
|
24
25
|
import { sanitizeSkillRetention } from './skills/index.js';
|
|
25
26
|
import { sanitizeGroups, sanitizeImpressions } from './social/index.js';
|
|
@@ -61,6 +62,7 @@ const OPTIONAL_KEYS = new Set([
|
|
|
61
62
|
'retention',
|
|
62
63
|
'methods',
|
|
63
64
|
'prospection',
|
|
65
|
+
'promptings',
|
|
64
66
|
]);
|
|
65
67
|
/** Kind of a JSON value for the per-field type check in `restore`. */
|
|
66
68
|
const kindOf = (x) => (Array.isArray(x) ? 'array' : x === null ? 'null' : typeof x);
|
|
@@ -174,6 +176,7 @@ export function restore(input) {
|
|
|
174
176
|
keepValid(out, 'retention', cleanRetention);
|
|
175
177
|
keepValid(out, 'methods', sanitizeMethods);
|
|
176
178
|
keepValid(out, 'prospection', (x) => sanitizeProspection(x, out.now, out.id));
|
|
179
|
+
keepValid(out, 'promptings', (x) => sanitizePromptings(x, out.now));
|
|
177
180
|
boundLists(out);
|
|
178
181
|
return out;
|
|
179
182
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -827,7 +827,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
|
|
|
827
827
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
828
828
|
* `dutyReviewAt`). Person shape unchanged.
|
|
829
829
|
*/
|
|
830
|
-
export declare const ENGINE_VERSION = "2.
|
|
830
|
+
export declare const ENGINE_VERSION = "2.5.0";
|
|
831
831
|
/** A pregnancy carried by this person (host-driven; see `family/`). */
|
|
832
832
|
export interface Pregnancy {
|
|
833
833
|
fatherId: PersonId;
|
|
@@ -992,6 +992,8 @@ export interface Person {
|
|
|
992
992
|
methods?: MethodsState;
|
|
993
993
|
/** Received future consequences, absent until attended testimony supplies them. */
|
|
994
994
|
prospection?: ProspectionState;
|
|
995
|
+
/** Noticed promptings and practised turnings-away (2.4.0, opt-in via `enablePromptings`). Owned by `promptings/`. */
|
|
996
|
+
promptings?: PromptingsState;
|
|
995
997
|
/** Practice consolidation (1.8.0, opt-in via `enableSkillRetention`). Owned by `skills/`. */
|
|
996
998
|
skillRetention?: {
|
|
997
999
|
consolidationHours: number;
|
|
@@ -1090,6 +1092,12 @@ export interface Affordance {
|
|
|
1090
1092
|
severity: Unit;
|
|
1091
1093
|
kind: string;
|
|
1092
1094
|
};
|
|
1095
|
+
/**
|
|
1096
|
+
* Exertion the person knows must follow this act (2.5.0): the way back down, carrying the load home. The
|
|
1097
|
+
* person forecasts their own body through the act and these spans (`prospection/` forecast) and weighs being
|
|
1098
|
+
* spent during them. Absent = no forecast, exactly as before.
|
|
1099
|
+
*/
|
|
1100
|
+
followedBy?: FollowOn[];
|
|
1093
1101
|
/** Set on a partner's mirror offer by the joint protocol in `sim/` (the proposal id). */
|
|
1094
1102
|
jointId?: string;
|
|
1095
1103
|
/** Material gain/cost in host units (e.g. coins). Valued via security/achievement/power values. */
|
|
@@ -1295,7 +1303,9 @@ export interface SuggestionResolution {
|
|
|
1295
1303
|
export interface Term {
|
|
1296
1304
|
/** 'need:food', 'norm:salah', 'commitment:c3', 'commitment' (promise inertia of the running activity),
|
|
1297
1305
|
* 'goal:g1', 'habit', 'emotion:fear:risky' (emotion id, then the tag it acts through), 'effort', 'risk',
|
|
1298
|
-
* 'suggestion:player', 'social:p2', 'expectation', 'autonomy' (reactance to a pushing voice)
|
|
1306
|
+
* 'suggestion:player', 'social:p2', 'expectation', 'autonomy' (reactance to a pushing voice),
|
|
1307
|
+
* 'promptings:turn' (a practised turning-away), 'forecast:body' (expected
|
|
1308
|
+
* bodily state during `followedBy` spans), 'promptings:echo' (recurrence under the `fault` reading) */
|
|
1299
1309
|
source: string;
|
|
1300
1310
|
value: number;
|
|
1301
1311
|
}
|
|
@@ -1383,8 +1393,71 @@ export interface PersonSpec {
|
|
|
1383
1393
|
yearbook?: boolean;
|
|
1384
1394
|
character?: boolean;
|
|
1385
1395
|
skillRetention?: boolean;
|
|
1396
|
+
/** Promptings and turning away (2.4.0): the same as `enablePromptings(p, spec)` after creation. */
|
|
1397
|
+
promptings?: PromptingsSpec;
|
|
1386
1398
|
};
|
|
1387
1399
|
}
|
|
1400
|
+
/** A known span of exertion after an act (see `Affordance.followedBy`). */
|
|
1401
|
+
export interface FollowOn {
|
|
1402
|
+
label?: string;
|
|
1403
|
+
/** Minutes. */
|
|
1404
|
+
duration: number;
|
|
1405
|
+
effort: Unit;
|
|
1406
|
+
focus?: Unit;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* How a person understands their own unwanted inner pulls (a held reading, set by the host, with provenance).
|
|
1410
|
+
* `whisper`, `inclination` and `intrusion` share mechanics and differ only in vocabulary; `fault` (the scrupulous
|
|
1411
|
+
* reading) adds guilt at the prompt itself; `none` is neutral vocabulary. See research/reconciliation.md §5.
|
|
1412
|
+
*/
|
|
1413
|
+
export type PromptingReading = 'whisper' | 'inclination' | 'intrusion' | 'fault' | 'none';
|
|
1414
|
+
/** A practised turning-away: seeking refuge, redirecting attention, or simply leaving the thought. */
|
|
1415
|
+
export type PromptingResponseKind = 'refuge' | 'redirect' | 'leave';
|
|
1416
|
+
export interface PromptingResponse {
|
|
1417
|
+
id: string;
|
|
1418
|
+
kind: PromptingResponseKind;
|
|
1419
|
+
/** Skill-like practice 0..1 as of `lastUsedAt`; it decays slowly with disuse. */
|
|
1420
|
+
practice: Unit;
|
|
1421
|
+
lastUsedAt: Minute;
|
|
1422
|
+
}
|
|
1423
|
+
export interface PromptingRecord {
|
|
1424
|
+
at: Minute;
|
|
1425
|
+
action: string;
|
|
1426
|
+
affordanceId: string;
|
|
1427
|
+
/** The strongest pull: 'habit', 'emotion:*', 'need:*', 'promptings:echo' or 'host' (a `temptation` tag). */
|
|
1428
|
+
source: string;
|
|
1429
|
+
/** The opposing term: 'norm:x' or 'goal:g'. */
|
|
1430
|
+
opposedBy: string;
|
|
1431
|
+
outcome: 'passed' | 'turned' | 'assented';
|
|
1432
|
+
/** The response that was practised at this prompt, if any. */
|
|
1433
|
+
responseId?: string;
|
|
1434
|
+
}
|
|
1435
|
+
export interface PromptingsState {
|
|
1436
|
+
reading: PromptingReading;
|
|
1437
|
+
/** Provenance of the reading (upbringing, a teacher, a book id), as for held norms. */
|
|
1438
|
+
readingSource?: string;
|
|
1439
|
+
responses: PromptingResponse[];
|
|
1440
|
+
/** Recent prompts, oldest first, bounded (`PROMPTINGS_DEFAULTS.maxRecent`). */
|
|
1441
|
+
recent: PromptingRecord[];
|
|
1442
|
+
/** Recurrence per action under the `fault` reading (an off-by-default assumption; see `echoRate`). */
|
|
1443
|
+
echo: Record<string, {
|
|
1444
|
+
level: Unit;
|
|
1445
|
+
at: Minute;
|
|
1446
|
+
}>;
|
|
1447
|
+
/** Echo gained per prompt under `fault`. Default 0: the recurrence term is a host-set assumption (P3 contested). */
|
|
1448
|
+
echoRate: number;
|
|
1449
|
+
}
|
|
1450
|
+
/** What a host hands `enablePromptings`. */
|
|
1451
|
+
export interface PromptingsSpec {
|
|
1452
|
+
reading?: PromptingReading;
|
|
1453
|
+
readingSource?: string;
|
|
1454
|
+
responses?: {
|
|
1455
|
+
id: string;
|
|
1456
|
+
kind: PromptingResponseKind;
|
|
1457
|
+
practice?: Unit;
|
|
1458
|
+
}[];
|
|
1459
|
+
echoRate?: number;
|
|
1460
|
+
}
|
|
1388
1461
|
/** What the body is doing over an interval. */
|
|
1389
1462
|
export interface BodyLoad {
|
|
1390
1463
|
effort: Unit;
|
package/dist/types.js
CHANGED
|
@@ -120,4 +120,4 @@ export const PERSON_SCHEMA = 'human/person@1';
|
|
|
120
120
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
121
121
|
* `dutyReviewAt`). Person shape unchanged.
|
|
122
122
|
*/
|
|
123
|
-
export const ENGINE_VERSION = '2.
|
|
123
|
+
export const ENGINE_VERSION = '2.5.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Deterministic, explainable simulated people: body, needs, affect, memory, beliefs, skills, habits, relationships, values and a will that can refuse a suggestion",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"simulation",
|