@adam0white/human-framework 2.6.0 → 2.7.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/dist/cognition/cognition.js +5 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/migrate.js +6 -1
- package/dist/person.js +9 -0
- package/dist/restore.js +3 -0
- package/dist/review/index.d.ts +1 -0
- package/dist/review/index.js +1 -0
- package/dist/review/review.d.ts +47 -0
- package/dist/review/review.js +239 -0
- package/dist/types.d.ts +46 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
|
@@ -36,6 +36,7 @@ import { urgencyOf } from '../needs/index.js';
|
|
|
36
36
|
import { partneringTerms } from '../partnering/index.js';
|
|
37
37
|
import { promptIn, promptingTerms } from '../promptings/index.js';
|
|
38
38
|
import { bodyForecastTerm, receivedGoalRoute } from '../prospection/index.js';
|
|
39
|
+
import { reviewTerms } from '../review/index.js';
|
|
39
40
|
import { companionSteadiness, socialTerms } from '../social/index.js';
|
|
40
41
|
import { PHYSIOLOGICAL_NEEDS, VALUE_KEYS } from '../types.js';
|
|
41
42
|
import { adviceWeight, resolveChoice, safeUtility, standingAdvice, suggestionTargets, voicesIn, } from '../will/index.js';
|
|
@@ -409,6 +410,10 @@ export function consider(p, aff, ctx) {
|
|
|
409
410
|
if (p.promptings)
|
|
410
411
|
for (const t of promptingTerms(p, aff.action, terms, aff.tags, ctx.now, ctx.desperation))
|
|
411
412
|
push(t.source, t.value);
|
|
413
|
+
// Evening review (2.7.0, opt-in): a resolve formed after a lapse leans on that commitment's own term.
|
|
414
|
+
if (p.review)
|
|
415
|
+
for (const t of reviewTerms(p, terms))
|
|
416
|
+
push(t.source, t.value);
|
|
412
417
|
let utility = 0;
|
|
413
418
|
for (const t of terms)
|
|
414
419
|
utility += t.value;
|
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.7.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';
|
|
@@ -32,6 +32,7 @@ export * from './promptings/index.ts';
|
|
|
32
32
|
export type { ConsequenceEvidence } from './prospection/index.ts';
|
|
33
33
|
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.ts';
|
|
34
34
|
export * from './restore.ts';
|
|
35
|
+
export * from './review/index.ts';
|
|
35
36
|
export * from './scenarios/index.ts';
|
|
36
37
|
export * from './sim/index.ts';
|
|
37
38
|
export * from './skills/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.7.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';
|
|
@@ -28,6 +28,7 @@ export * from './person.js';
|
|
|
28
28
|
export * from './promptings/index.js';
|
|
29
29
|
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.js';
|
|
30
30
|
export * from './restore.js';
|
|
31
|
+
export * from './review/index.js';
|
|
31
32
|
export * from './scenarios/index.js';
|
|
32
33
|
export * from './sim/index.js';
|
|
33
34
|
export * from './skills/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. 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`. 2.5.0 to 2.6.0 stamps the version; the fatigue store stays absent unless enabled. 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`. 2.5.0 to 2.6.0 stamps the version; the fatigue store stays absent unless enabled. 2.6.0 to 2.7.0 stamps the version; the evening review stays absent unless enabled. 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,11 @@ 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.6.0': {
|
|
62
|
+
to: '2.7.0',
|
|
63
|
+
note: 'the evening review (review) is optional and starts absent; existing hosts retain exact behavior',
|
|
64
|
+
apply: stamp,
|
|
65
|
+
},
|
|
61
66
|
'2.5.0': {
|
|
62
67
|
to: '2.6.0',
|
|
63
68
|
note: "the fatigue store (body.depletion) is optional and starts absent; the bodily forecast now applies an open fast's hunger damping, which changes only offers that declare followedBy",
|
package/dist/person.js
CHANGED
|
@@ -38,6 +38,7 @@ import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisf
|
|
|
38
38
|
import { widow } from './partnering/index.js';
|
|
39
39
|
import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
|
|
40
40
|
import { bodyForecastFor, consumeConsequences, forecastLine, pruneConsequences, receiveConsequences, } from './prospection/index.js';
|
|
41
|
+
import { enableReview, reviewIfDue } from './review/index.js';
|
|
41
42
|
import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
|
|
42
43
|
import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
|
|
43
44
|
import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA, PHYSIOLOGICAL_NEEDS, PSYCHOLOGICAL_NEEDS, TRAIT_KEYS, VALUE_KEYS, } from './types.js';
|
|
@@ -158,6 +159,8 @@ export function createPerson(spec) {
|
|
|
158
159
|
enableFatigueStore(p);
|
|
159
160
|
if (on?.promptings)
|
|
160
161
|
enablePromptings(p, on.promptings);
|
|
162
|
+
if (on?.review)
|
|
163
|
+
enableReview(p, on.review);
|
|
161
164
|
return p;
|
|
162
165
|
}
|
|
163
166
|
/**
|
|
@@ -418,6 +421,12 @@ export function tick(p, now) {
|
|
|
418
421
|
closed = closeDay(p);
|
|
419
422
|
applyRecall(p, recallByCue(p, { at: end }));
|
|
420
423
|
}
|
|
424
|
+
// Evening review (2.7.0, opt-in): from its hour, or at the close of a day that passed without one.
|
|
425
|
+
if (p.review) {
|
|
426
|
+
const done = reviewIfDue(p, end, (p.chronicle ?? []).slice(-2), closed);
|
|
427
|
+
if (done && done.distress > 0)
|
|
428
|
+
feel(p, 'distress', done.distress, 'review', end);
|
|
429
|
+
}
|
|
421
430
|
if (minuteOfDay(end) === 0) {
|
|
422
431
|
// Lasting gists (1.8.0, opt-in): at midnight, episodes past the horizon fold into gists.
|
|
423
432
|
if (p.memory.gists)
|
package/dist/restore.js
CHANGED
|
@@ -22,6 +22,7 @@ import { sanitizeBonds } from './partnering/index.js';
|
|
|
22
22
|
import { cleanRetention, createPerson, traceLimit } from './person.js';
|
|
23
23
|
import { sanitizePromptings } from './promptings/index.js';
|
|
24
24
|
import { sanitizeProspection } from './prospection/index.js';
|
|
25
|
+
import { sanitizeReview } from './review/index.js';
|
|
25
26
|
import { sanitizeSkillRetention } from './skills/index.js';
|
|
26
27
|
import { sanitizeGroups, sanitizeImpressions } from './social/index.js';
|
|
27
28
|
import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA } from './types.js';
|
|
@@ -63,6 +64,7 @@ const OPTIONAL_KEYS = new Set([
|
|
|
63
64
|
'methods',
|
|
64
65
|
'prospection',
|
|
65
66
|
'promptings',
|
|
67
|
+
'review',
|
|
66
68
|
]);
|
|
67
69
|
/** Kind of a JSON value for the per-field type check in `restore`. */
|
|
68
70
|
const kindOf = (x) => (Array.isArray(x) ? 'array' : x === null ? 'null' : typeof x);
|
|
@@ -177,6 +179,7 @@ export function restore(input) {
|
|
|
177
179
|
keepValid(out, 'methods', sanitizeMethods);
|
|
178
180
|
keepValid(out, 'prospection', (x) => sanitizeProspection(x, out.now, out.id));
|
|
179
181
|
keepValid(out, 'promptings', (x) => sanitizePromptings(x, out.now));
|
|
182
|
+
keepValid(out, 'review', (x) => sanitizeReview(x, out.now));
|
|
180
183
|
boundLists(out);
|
|
181
184
|
return out;
|
|
182
185
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './review.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './review.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DayRecord, Minute, Person, ReviewRecord, ReviewSpec, ReviewState, ReviewStyle, Term } from '../types.ts';
|
|
2
|
+
export declare const REVIEW_DEFAULTS: {
|
|
3
|
+
/** Default evening review time, minute of day. */
|
|
4
|
+
at: number;
|
|
5
|
+
/** Largest `review:resolve` term at full strength. */
|
|
6
|
+
resolveScale: number;
|
|
7
|
+
/** Share of the remaining headroom a lapse adds, by style. */
|
|
8
|
+
gain: Record<ReviewStyle, number>;
|
|
9
|
+
/** Strength kept after the commitment is kept. */
|
|
10
|
+
keptKeep: number;
|
|
11
|
+
halfLifeDays: number;
|
|
12
|
+
/** Resolves weaker than this are dropped. */
|
|
13
|
+
dropBelow: number;
|
|
14
|
+
maxResolves: number;
|
|
15
|
+
maxSeen: number;
|
|
16
|
+
/** Distress felt per lapse by a ruminating reviewer, capped at `ruminateMax`. */
|
|
17
|
+
ruminatePer: number;
|
|
18
|
+
ruminateMax: number;
|
|
19
|
+
};
|
|
20
|
+
/** Turn on the evening review. Idempotent; a second call updates style, source and time but keeps resolves. */
|
|
21
|
+
export declare function enableReview(p: Person, spec?: ReviewSpec): ReviewState;
|
|
22
|
+
/** The key a commitment and its chronicle note share: kind, owed-to, and label (else first action). */
|
|
23
|
+
export declare const resolveKey: (c: {
|
|
24
|
+
label?: string | undefined;
|
|
25
|
+
kind: string;
|
|
26
|
+
action?: string | undefined;
|
|
27
|
+
actions?: readonly string[];
|
|
28
|
+
toId?: string | undefined;
|
|
29
|
+
}) => string;
|
|
30
|
+
/**
|
|
31
|
+
* The `review:resolve` terms for an option, read from its already-traced `commitment:*` and `abstain:*` terms.
|
|
32
|
+
* Pure; empty when review is off or no resolve matches.
|
|
33
|
+
*/
|
|
34
|
+
export declare function reviewTerms(p: Person, terms: readonly Term[]): Term[];
|
|
35
|
+
/**
|
|
36
|
+
* Review now if it is due: at or after `at` on a day not yet reviewed, or when `closed` (the day just closed)
|
|
37
|
+
* was never reviewed. Reads the notes of `recent` (the latest closed day records) and the open day. Returns the
|
|
38
|
+
* record, or undefined when nothing ran. Distress for a ruminating reviewer is returned for the composite to feel.
|
|
39
|
+
*/
|
|
40
|
+
export declare function reviewIfDue(p: Person, now: Minute, recent: readonly DayRecord[], closed?: DayRecord): {
|
|
41
|
+
record: ReviewRecord;
|
|
42
|
+
distress: number;
|
|
43
|
+
} | undefined;
|
|
44
|
+
/** The person's own words for their latest review, or undefined when they hold no review. */
|
|
45
|
+
export declare function reviewLine(p: Person): string | undefined;
|
|
46
|
+
/** Restore guard: drop malformed fields, keep valid resolves. */
|
|
47
|
+
export declare function sanitizeReview(x: unknown, now?: Minute): ReviewState | undefined;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE (evening self-review, muhasaba, 2.7.0): once a day, from `at` (default 21:00), a person looks back over
|
|
3
|
+
* the commitments the chronicle recorded as kept or broken since their last review, and for each one they broke
|
|
4
|
+
* forms or renews a *resolve*: an if–then intention for that commitment's next occasion (Gollwitzer & Sheeran
|
|
5
|
+
* 2006, research/reconciliation.md E1). While a matching commitment is pending, the resolve adds one bounded term,
|
|
6
|
+
* `review:resolve` (each resolve counted once per option, the sum clamped to ±`resolveScale`), with the sign of the commitment's own term (toward a duty, away from an abstention's breaking
|
|
7
|
+
* act), scaled by its strength. Strength grows with diminishing gains at each further lapse, relaxes once the
|
|
8
|
+
* commitment is kept, and fades over `halfLifeDays` of disuse. Released and excused commitments (exemptions,
|
|
9
|
+
* sleep, necessity) form no resolve: what was beyond capacity is not held against the person (2:286). The review
|
|
10
|
+
* also runs when the day closes, if the evening passed without it or a lapse came after it.
|
|
11
|
+
*
|
|
12
|
+
* Held readings (`ReviewStyle`): `accounting` is self-accounting as the tradition describes it, naming the lapse
|
|
13
|
+
* with a view to tomorrow (Q 59:18, whose "tomorrow" most exegetes read as the hereafter, so a daily review is an
|
|
14
|
+
* inference; ʿUmar's saying reported by Tirmidhi after 2459, an athar, not a prophetic
|
|
15
|
+
* hadith) and turning back without despair (3:135, 39:53); it forms full resolves and adds no emotion. Reading accounting as
|
|
16
|
+
* non-punishing, like self-compassion after failure raising the motivation to repair (Breines & Chen 2012, E3),
|
|
17
|
+
* is an engineering mapping, not a finding about muhasaba. `ruminating` goes over
|
|
18
|
+
* the lapse abstractly: weaker resolves and a small distress emotion (Watkins 2008, E4: abstract, unconstructive
|
|
19
|
+
* repetitive thought). `none` never reviews. The review reads only chronicle notes and changes no other module's
|
|
20
|
+
* state except that one emotion.
|
|
21
|
+
*
|
|
22
|
+
* Every number is an engineering default, not fitted. Does not claim: divine reckoning or acceptance, a measure of
|
|
23
|
+
* piety, repentance as a ruling (tawba conditions are not modelled), effects on sleep onset (Harvey's model, E6, is
|
|
24
|
+
* not evidence enough), or that review improves outcomes beyond the borrowed if–then shape. It draws no randomness;
|
|
25
|
+
* without `enableReview` nothing here runs and existing runs replay byte for byte.
|
|
26
|
+
*/
|
|
27
|
+
import { clamp01, dexp, isObj, minuteOfDay, round } from '../core/index.js';
|
|
28
|
+
export const REVIEW_DEFAULTS = {
|
|
29
|
+
/** Default evening review time, minute of day. */
|
|
30
|
+
at: 21 * 60,
|
|
31
|
+
/** Largest `review:resolve` term at full strength. */
|
|
32
|
+
resolveScale: 0.3,
|
|
33
|
+
/** Share of the remaining headroom a lapse adds, by style. */
|
|
34
|
+
gain: { accounting: 0.5, ruminating: 0.15, none: 0 },
|
|
35
|
+
/** Strength kept after the commitment is kept. */
|
|
36
|
+
keptKeep: 0.6,
|
|
37
|
+
halfLifeDays: 7,
|
|
38
|
+
/** Resolves weaker than this are dropped. */
|
|
39
|
+
dropBelow: 0.05,
|
|
40
|
+
maxResolves: 8,
|
|
41
|
+
maxSeen: 64,
|
|
42
|
+
/** Distress felt per lapse by a ruminating reviewer, capped at `ruminateMax`. */
|
|
43
|
+
ruminatePer: 0.15,
|
|
44
|
+
ruminateMax: 0.45,
|
|
45
|
+
};
|
|
46
|
+
const STYLES = ['accounting', 'ruminating', 'none'];
|
|
47
|
+
/** Turn on the evening review. Idempotent; a second call updates style, source and time but keeps resolves. */
|
|
48
|
+
export function enableReview(p, spec = {}) {
|
|
49
|
+
const at = spec.at !== undefined && Number.isInteger(spec.at) && spec.at >= 0 && spec.at < 1440
|
|
50
|
+
? spec.at
|
|
51
|
+
: undefined;
|
|
52
|
+
const cur = p.review;
|
|
53
|
+
const st = cur ?? { style: 'accounting', at: REVIEW_DEFAULTS.at, resolves: [] };
|
|
54
|
+
if (spec.style && STYLES.includes(spec.style))
|
|
55
|
+
st.style = spec.style;
|
|
56
|
+
if (spec.styleSource !== undefined)
|
|
57
|
+
st.styleSource = spec.styleSource.slice(0, 96);
|
|
58
|
+
if (at !== undefined)
|
|
59
|
+
st.at = at;
|
|
60
|
+
p.review = st;
|
|
61
|
+
return st;
|
|
62
|
+
}
|
|
63
|
+
/** The key a commitment and its chronicle note share: kind, owed-to, and label (else first action). */
|
|
64
|
+
export const resolveKey = (c) => `${c.kind}:${c.toId ?? ''}:${c.label ?? c.action ?? c.actions?.[0] ?? ''}`;
|
|
65
|
+
/**
|
|
66
|
+
* The `review:resolve` terms for an option, read from its already-traced `commitment:*` and `abstain:*` terms.
|
|
67
|
+
* Pure; empty when review is off or no resolve matches.
|
|
68
|
+
*/
|
|
69
|
+
export function reviewTerms(p, terms) {
|
|
70
|
+
const st = p.review;
|
|
71
|
+
if (!st || st.resolves.length === 0)
|
|
72
|
+
return [];
|
|
73
|
+
let value = 0;
|
|
74
|
+
const used = new Set();
|
|
75
|
+
for (const t of terms) {
|
|
76
|
+
const m = /^(commitment|abstain):(.+)$/.exec(t.source);
|
|
77
|
+
if (!m || t.value === 0)
|
|
78
|
+
continue;
|
|
79
|
+
const c = p.agenda.commitments.find((x) => x.id === m[2]);
|
|
80
|
+
if (!c)
|
|
81
|
+
continue;
|
|
82
|
+
const r = st.resolves.find((x) => x.key === resolveKey(c));
|
|
83
|
+
if (!r || used.has(r.key))
|
|
84
|
+
continue;
|
|
85
|
+
used.add(r.key);
|
|
86
|
+
value += Math.sign(t.value) * REVIEW_DEFAULTS.resolveScale * r.strength;
|
|
87
|
+
}
|
|
88
|
+
const max = REVIEW_DEFAULTS.resolveScale;
|
|
89
|
+
value = Math.max(-max, Math.min(max, value));
|
|
90
|
+
return value === 0 ? [] : [{ source: 'review:resolve', value: round(value) }];
|
|
91
|
+
}
|
|
92
|
+
const fade = (s, days) => days <= 0 ? s : s * dexp((-Math.LN2 * days) / REVIEW_DEFAULTS.halfLifeDays);
|
|
93
|
+
/**
|
|
94
|
+
* Review now if it is due: at or after `at` on a day not yet reviewed, or when `closed` (the day just closed)
|
|
95
|
+
* was never reviewed. Reads the notes of `recent` (the latest closed day records) and the open day. Returns the
|
|
96
|
+
* record, or undefined when nothing ran. Distress for a ruminating reviewer is returned for the composite to feel.
|
|
97
|
+
*/
|
|
98
|
+
export function reviewIfDue(p, now, recent, closed) {
|
|
99
|
+
const st = p.review;
|
|
100
|
+
if (!st || st.style === 'none')
|
|
101
|
+
return undefined;
|
|
102
|
+
const today = Math.floor(now / 1440);
|
|
103
|
+
const seenNow = new Set(st.seen ?? []);
|
|
104
|
+
const lateLapse = closed?.broken.some((n) => !seenNow.has(n.id)) ?? false;
|
|
105
|
+
let day;
|
|
106
|
+
if (closed && (st.lastDay === undefined || st.lastDay < closed.day || lateLapse))
|
|
107
|
+
day = Math.max(closed.day, st.lastDay ?? closed.day);
|
|
108
|
+
else if (minuteOfDay(now) >= st.at && (st.lastDay === undefined || st.lastDay < today))
|
|
109
|
+
day = today;
|
|
110
|
+
else
|
|
111
|
+
return undefined;
|
|
112
|
+
const seen = new Set(st.seen ?? []);
|
|
113
|
+
const notes = (kind) => {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const r of closed && !recent.includes(closed) ? [...recent, closed] : recent)
|
|
116
|
+
for (const n of r[kind])
|
|
117
|
+
if (!seen.has(n.id))
|
|
118
|
+
out.push(n);
|
|
119
|
+
const open = p.chronicleDay;
|
|
120
|
+
if (open && open.day <= day)
|
|
121
|
+
for (const n of open[kind])
|
|
122
|
+
if (!seen.has(n.id))
|
|
123
|
+
out.push(n);
|
|
124
|
+
return out;
|
|
125
|
+
};
|
|
126
|
+
const broken = notes('broken');
|
|
127
|
+
const kept = notes('kept');
|
|
128
|
+
const elapsed = st.lastDay === undefined ? 0 : day - st.lastDay;
|
|
129
|
+
for (const r of st.resolves)
|
|
130
|
+
r.strength = round(fade(r.strength, elapsed));
|
|
131
|
+
const gain = REVIEW_DEFAULTS.gain[st.style];
|
|
132
|
+
const missed = [];
|
|
133
|
+
const resolved = [];
|
|
134
|
+
for (const n of broken) {
|
|
135
|
+
const key = resolveKey(n);
|
|
136
|
+
const name = n.label ?? n.action ?? n.kind;
|
|
137
|
+
if (!missed.includes(name))
|
|
138
|
+
missed.push(name);
|
|
139
|
+
let r = st.resolves.find((x) => x.key === key);
|
|
140
|
+
if (!r) {
|
|
141
|
+
r = { key, kind: n.kind, strength: 0, day };
|
|
142
|
+
if (n.label !== undefined)
|
|
143
|
+
r.label = n.label;
|
|
144
|
+
st.resolves.push(r);
|
|
145
|
+
}
|
|
146
|
+
r.strength = round(clamp01(r.strength + gain * (1 - r.strength)));
|
|
147
|
+
r.day = day;
|
|
148
|
+
if (!resolved.includes(name))
|
|
149
|
+
resolved.push(name);
|
|
150
|
+
}
|
|
151
|
+
const keptNames = [];
|
|
152
|
+
for (const n of kept) {
|
|
153
|
+
const key = resolveKey(n);
|
|
154
|
+
const name = n.label ?? n.action ?? n.kind;
|
|
155
|
+
if (!keptNames.includes(name))
|
|
156
|
+
keptNames.push(name);
|
|
157
|
+
const r = st.resolves.find((x) => x.key === key);
|
|
158
|
+
if (r && !broken.some((b) => resolveKey(b) === key))
|
|
159
|
+
r.strength = round(r.strength * REVIEW_DEFAULTS.keptKeep);
|
|
160
|
+
}
|
|
161
|
+
st.resolves = st.resolves
|
|
162
|
+
.filter((r) => r.strength >= REVIEW_DEFAULTS.dropBelow)
|
|
163
|
+
.sort((a, b) => b.strength - a.strength || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
|
|
164
|
+
.slice(0, REVIEW_DEFAULTS.maxResolves);
|
|
165
|
+
const ids = [...(st.seen ?? []), ...broken.map((n) => n.id), ...kept.map((n) => n.id)];
|
|
166
|
+
st.seen = ids.slice(-REVIEW_DEFAULTS.maxSeen);
|
|
167
|
+
st.lastDay = day;
|
|
168
|
+
const record = {
|
|
169
|
+
day,
|
|
170
|
+
missed,
|
|
171
|
+
kept: keptNames,
|
|
172
|
+
resolved: st.style === 'accounting' ? resolved : [],
|
|
173
|
+
};
|
|
174
|
+
st.last = record;
|
|
175
|
+
const distress = st.style === 'ruminating'
|
|
176
|
+
? Math.min(REVIEW_DEFAULTS.ruminateMax, REVIEW_DEFAULTS.ruminatePer * missed.length)
|
|
177
|
+
: 0;
|
|
178
|
+
return { record, distress };
|
|
179
|
+
}
|
|
180
|
+
/** The person's own words for their latest review, or undefined when they hold no review. */
|
|
181
|
+
export function reviewLine(p) {
|
|
182
|
+
const st = p.review;
|
|
183
|
+
const r = st?.last;
|
|
184
|
+
if (!st || st.style === 'none' || !r)
|
|
185
|
+
return undefined;
|
|
186
|
+
const list = (xs) => xs.length <= 1 ? (xs[0] ?? '') : `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}`;
|
|
187
|
+
if (r.missed.length === 0)
|
|
188
|
+
return r.kept.length > 0 ? `Today I kept ${list(r.kept)}.` : undefined;
|
|
189
|
+
if (st.style === 'ruminating')
|
|
190
|
+
return `I keep going back over ${list(r.missed)}.`;
|
|
191
|
+
return `I didn't keep ${list(r.missed)}. Next time I'll be ready when it comes.`;
|
|
192
|
+
}
|
|
193
|
+
/** Restore guard: drop malformed fields, keep valid resolves. */
|
|
194
|
+
export function sanitizeReview(x, now) {
|
|
195
|
+
if (!isObj(x))
|
|
196
|
+
return undefined;
|
|
197
|
+
const style = STYLES.includes(x.style) ? x.style : 'accounting';
|
|
198
|
+
const at = Number.isInteger(x.at) && x.at >= 0 && x.at < 1440
|
|
199
|
+
? x.at
|
|
200
|
+
: REVIEW_DEFAULTS.at;
|
|
201
|
+
const resolves = [];
|
|
202
|
+
if (Array.isArray(x.resolves))
|
|
203
|
+
for (const r of x.resolves) {
|
|
204
|
+
if (!isObj(r) || typeof r.key !== 'string' || typeof r.kind !== 'string')
|
|
205
|
+
continue;
|
|
206
|
+
if (typeof r.strength !== 'number' || !Number.isFinite(r.strength) || !Number.isInteger(r.day))
|
|
207
|
+
continue;
|
|
208
|
+
const out = {
|
|
209
|
+
key: r.key.slice(0, 96),
|
|
210
|
+
kind: r.kind.slice(0, 32),
|
|
211
|
+
strength: clamp01(r.strength),
|
|
212
|
+
day: r.day,
|
|
213
|
+
};
|
|
214
|
+
if (typeof r.label === 'string')
|
|
215
|
+
out.label = r.label.slice(0, 96);
|
|
216
|
+
if (!resolves.some((y) => y.key === out.key))
|
|
217
|
+
resolves.push(out);
|
|
218
|
+
if (resolves.length >= REVIEW_DEFAULTS.maxResolves)
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
const st = { style, at, resolves };
|
|
222
|
+
if (typeof x.styleSource === 'string')
|
|
223
|
+
st.styleSource = x.styleSource.slice(0, 96);
|
|
224
|
+
if (Number.isInteger(x.lastDay) && (now === undefined || x.lastDay <= Math.floor(now / 1440)))
|
|
225
|
+
st.lastDay = x.lastDay;
|
|
226
|
+
if (Array.isArray(x.seen))
|
|
227
|
+
st.seen = x.seen.filter((s) => typeof s === 'string').slice(-REVIEW_DEFAULTS.maxSeen);
|
|
228
|
+
const l = x.last;
|
|
229
|
+
if (isObj(l) && Number.isInteger(l.day)) {
|
|
230
|
+
const strs = (v) => Array.isArray(v) ? v.filter((s) => typeof s === 'string').slice(0, 24) : [];
|
|
231
|
+
st.last = {
|
|
232
|
+
day: l.day,
|
|
233
|
+
missed: strs(l.missed),
|
|
234
|
+
kept: strs(l.kept),
|
|
235
|
+
resolved: strs(l.resolved),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return st;
|
|
239
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -832,7 +832,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
|
|
|
832
832
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
833
833
|
* `dutyReviewAt`). Person shape unchanged.
|
|
834
834
|
*/
|
|
835
|
-
export declare const ENGINE_VERSION = "2.
|
|
835
|
+
export declare const ENGINE_VERSION = "2.7.0";
|
|
836
836
|
/** A pregnancy carried by this person (host-driven; see `family/`). */
|
|
837
837
|
export interface Pregnancy {
|
|
838
838
|
fatherId: PersonId;
|
|
@@ -999,6 +999,8 @@ export interface Person {
|
|
|
999
999
|
prospection?: ProspectionState;
|
|
1000
1000
|
/** Noticed promptings and practised turnings-away (2.4.0, opt-in via `enablePromptings`). Owned by `promptings/`. */
|
|
1001
1001
|
promptings?: PromptingsState;
|
|
1002
|
+
/** Evening self-review and its resolves (2.7.0, opt-in via `enableReview`). Owned by `review/`. */
|
|
1003
|
+
review?: ReviewState;
|
|
1002
1004
|
/** Practice consolidation (1.8.0, opt-in via `enableSkillRetention`). Owned by `skills/`. */
|
|
1003
1005
|
skillRetention?: {
|
|
1004
1006
|
consolidationHours: number;
|
|
@@ -1402,6 +1404,8 @@ export interface PersonSpec {
|
|
|
1402
1404
|
fatigueStore?: boolean;
|
|
1403
1405
|
/** Promptings and turning away (2.4.0): the same as `enablePromptings(p, spec)` after creation. */
|
|
1404
1406
|
promptings?: PromptingsSpec;
|
|
1407
|
+
/** Evening self-review (2.7.0): the same as `enableReview(p, spec)` after creation. */
|
|
1408
|
+
review?: ReviewSpec;
|
|
1405
1409
|
};
|
|
1406
1410
|
}
|
|
1407
1411
|
/** A known span of exertion after an act (see `Affordance.followedBy`). */
|
|
@@ -1454,6 +1458,47 @@ export interface PromptingsState {
|
|
|
1454
1458
|
/** Echo gained per prompt under `fault`. Default 0: the recurrence term is a host-set assumption (P3 contested). */
|
|
1455
1459
|
echoRate: number;
|
|
1456
1460
|
}
|
|
1461
|
+
/**
|
|
1462
|
+
* How a person reviews a day (2.7.0, a held reading): `accounting` (muhasaba: name the lapse and resolve for its
|
|
1463
|
+
* next occasion), `ruminating` (go over it abstractly: weaker resolve, some distress) or `none` (no review).
|
|
1464
|
+
*/
|
|
1465
|
+
export type ReviewStyle = 'accounting' | 'ruminating' | 'none';
|
|
1466
|
+
/** A resolve formed in review: an if-then intention for the next occasion of a missed commitment. */
|
|
1467
|
+
export interface Resolve {
|
|
1468
|
+
/** The commitment's label, else its first action, else its kind. */
|
|
1469
|
+
key: string;
|
|
1470
|
+
label?: string;
|
|
1471
|
+
kind: string;
|
|
1472
|
+
strength: Unit;
|
|
1473
|
+
/** Day the resolve was formed or last renewed. */
|
|
1474
|
+
day: number;
|
|
1475
|
+
}
|
|
1476
|
+
/** What the person concluded in their latest review. */
|
|
1477
|
+
export interface ReviewRecord {
|
|
1478
|
+
day: number;
|
|
1479
|
+
missed: string[];
|
|
1480
|
+
kept: string[];
|
|
1481
|
+
resolved: string[];
|
|
1482
|
+
}
|
|
1483
|
+
/** Owned by `review/`. */
|
|
1484
|
+
export interface ReviewState {
|
|
1485
|
+
style: ReviewStyle;
|
|
1486
|
+
styleSource?: string;
|
|
1487
|
+
/** Minute of the day from which the evening review can run (default 21:00). */
|
|
1488
|
+
at: number;
|
|
1489
|
+
/** Last day reviewed. */
|
|
1490
|
+
lastDay?: number;
|
|
1491
|
+
resolves: Resolve[];
|
|
1492
|
+
last?: ReviewRecord;
|
|
1493
|
+
/** Commitment ids already reviewed (bounded). */
|
|
1494
|
+
seen?: string[];
|
|
1495
|
+
}
|
|
1496
|
+
/** What a host hands `enableReview`. */
|
|
1497
|
+
export interface ReviewSpec {
|
|
1498
|
+
style?: ReviewStyle;
|
|
1499
|
+
styleSource?: string;
|
|
1500
|
+
at?: number;
|
|
1501
|
+
}
|
|
1457
1502
|
/** What a host hands `enablePromptings`. */
|
|
1458
1503
|
export interface PromptingsSpec {
|
|
1459
1504
|
reading?: PromptingReading;
|
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.7.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.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",
|