@adam0white/human-framework 2.7.0 → 2.9.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/agenda/agenda.d.ts +9 -1
- package/dist/agenda/agenda.js +36 -15
- package/dist/body/body.d.ts +5 -0
- package/dist/body/body.js +9 -0
- package/dist/cognition/cognition.d.ts +12 -0
- package/dist/cognition/cognition.js +58 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/migrate.js +10 -0
- package/dist/person.d.ts +6 -0
- package/dist/person.js +7 -3
- package/dist/promptings/promptings.d.ts +15 -2
- package/dist/promptings/promptings.js +82 -26
- package/dist/prospection/forecast.d.ts +15 -3
- package/dist/prospection/forecast.js +23 -6
- package/dist/review/review.js +16 -2
- package/dist/types.d.ts +66 -5
- package/dist/types.js +1 -1
- package/dist/will/will.d.ts +39 -0
- package/dist/will/will.js +67 -1
- package/package.json +1 -1
package/dist/agenda/agenda.d.ts
CHANGED
|
@@ -108,13 +108,21 @@ export declare const violatesAbstention: (c: Commitment, action: string, start:
|
|
|
108
108
|
* keeps by the same rule, so a pull never leads to an activity that cannot keep the commitment.
|
|
109
109
|
*/
|
|
110
110
|
export declare const spanMeetsWindow: (c: Commitment, start: Minute, end: Minute) => boolean;
|
|
111
|
+
/**
|
|
112
|
+
* The agenda's utility terms for one offer: `commitment:<id>` toward a pending commitment it serves,
|
|
113
|
+
* `abstain:<id>` against an act that would break an open abstention, and `goal:<id>` toward an active goal it
|
|
114
|
+
* advances. Goal terms are never negative, by design: a goal draws a person toward what advances it and is silent
|
|
115
|
+
* about everything else, so it cannot weigh against a temptation. A host that wants an act opposed links it to a
|
|
116
|
+
* held norm (`norms: violates`), an `abstain` commitment, or a commitment kept by the alternative.
|
|
117
|
+
*/
|
|
111
118
|
export declare function agendaTerms(p: Person, aff: Affordance, now: Minute): Term[];
|
|
112
119
|
/**
|
|
113
120
|
* Apply a finished activity. Only completed outcomes count. A completed violating action breaks every open
|
|
114
121
|
* abstention whose window its span overlaps (returned as `broken`; the composite routes them like missed
|
|
115
122
|
* commitments, with a breach when a norm is linked). One completion keeps the earliest-ending
|
|
116
123
|
* matching pending commitment whose window the activity's span [startedAt, outcome.at] overlaps (see
|
|
117
|
-
* `spanMeetsWindow`; `startedAt` defaults to `outcome.at`)
|
|
124
|
+
* `spanMeetsWindow`; `startedAt` defaults to `outcome.at`), except that when `outcome.fulfills` lists two or more
|
|
125
|
+
* ids, every listed commitment that qualifies is kept and no other (2.9.0); goals advance by their listed amount.
|
|
118
126
|
* Matching is by action (and target), or explicitly by `outcome.fulfills` / `outcome.advances` (the composite
|
|
119
127
|
* defaults these to the activity's `Affordance.fulfills` / `advances`), mirroring `agendaTerms`.
|
|
120
128
|
*/
|
package/dist/agenda/agenda.js
CHANGED
|
@@ -504,6 +504,13 @@ export function temporalGoalValue(p, g, aff, now, completion) {
|
|
|
504
504
|
dpow(0.5, Math.max(0, completion - now) / g.temporal.halfLifeMinutes)
|
|
505
505
|
: base;
|
|
506
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* The agenda's utility terms for one offer: `commitment:<id>` toward a pending commitment it serves,
|
|
509
|
+
* `abstain:<id>` against an act that would break an open abstention, and `goal:<id>` toward an active goal it
|
|
510
|
+
* advances. Goal terms are never negative, by design: a goal draws a person toward what advances it and is silent
|
|
511
|
+
* about everything else, so it cannot weigh against a temptation. A host that wants an act opposed links it to a
|
|
512
|
+
* held norm (`norms: violates`), an `abstain` commitment, or a commitment kept by the alternative.
|
|
513
|
+
*/
|
|
507
514
|
export function agendaTerms(p, aff, now) {
|
|
508
515
|
return agendaTermsWithGoalEstimates(p, aff, now);
|
|
509
516
|
}
|
|
@@ -599,7 +606,8 @@ function excuseUnderNecessity(p, c, at, catalog) {
|
|
|
599
606
|
* abstention whose window its span overlaps (returned as `broken`; the composite routes them like missed
|
|
600
607
|
* commitments, with a breach when a norm is linked). One completion keeps the earliest-ending
|
|
601
608
|
* matching pending commitment whose window the activity's span [startedAt, outcome.at] overlaps (see
|
|
602
|
-
* `spanMeetsWindow`; `startedAt` defaults to `outcome.at`)
|
|
609
|
+
* `spanMeetsWindow`; `startedAt` defaults to `outcome.at`), except that when `outcome.fulfills` lists two or more
|
|
610
|
+
* ids, every listed commitment that qualifies is kept and no other (2.9.0); goals advance by their listed amount.
|
|
603
611
|
* Matching is by action (and target), or explicitly by `outcome.fulfills` / `outcome.advances` (the composite
|
|
604
612
|
* defaults these to the activity's `Affordance.fulfills` / `advances`), mirroring `agendaTerms`.
|
|
605
613
|
*/
|
|
@@ -627,21 +635,34 @@ export function onFinished(p, outcome, startedAt = outcome.at, opts = {}) {
|
|
|
627
635
|
broken.push(c);
|
|
628
636
|
oweFor(p, c, 'broken', outcome.at);
|
|
629
637
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
638
|
+
// Commitments named explicitly (2.9.0): when `fulfills` lists two or more ids, every listed commitment that is
|
|
639
|
+
// eligible is kept and no other (one dinner keeps a word to a friend and the daily call). With one id listed, or
|
|
640
|
+
// none of several eligible, one completion keeps the earliest-ending match, explicit or by action, as before.
|
|
641
|
+
const eligible = (c) => c.status === 'pending' && c.kind !== 'abstain' && spanMeetsWindow(c, start, outcome.at);
|
|
642
|
+
const named = outcome.fulfills
|
|
643
|
+
? p.agenda.commitments.filter((c) => (outcome.fulfills?.includes(c.id) ?? false) && eligible(c))
|
|
644
|
+
: [];
|
|
645
|
+
if (new Set(outcome.fulfills ?? []).size > 1 && named.length > 0) {
|
|
646
|
+
for (const c of named) {
|
|
647
|
+
c.status = 'kept';
|
|
648
|
+
kept.push(c);
|
|
649
|
+
}
|
|
641
650
|
}
|
|
642
|
-
|
|
643
|
-
best
|
|
644
|
-
|
|
651
|
+
else {
|
|
652
|
+
let best;
|
|
653
|
+
for (const c of p.agenda.commitments) {
|
|
654
|
+
if (!eligible(c))
|
|
655
|
+
continue;
|
|
656
|
+
const explicit = outcome.fulfills?.includes(c.id) ?? false;
|
|
657
|
+
if (!explicit && !matchesCommitment(c, outcome.action, outcome.targetId))
|
|
658
|
+
continue;
|
|
659
|
+
if (!best || c.until < best.until)
|
|
660
|
+
best = c;
|
|
661
|
+
}
|
|
662
|
+
if (best) {
|
|
663
|
+
best.status = 'kept';
|
|
664
|
+
kept.push(best);
|
|
665
|
+
}
|
|
645
666
|
}
|
|
646
667
|
for (const g of p.agenda.goals) {
|
|
647
668
|
if (g.status !== 'active')
|
package/dist/body/body.d.ts
CHANGED
|
@@ -125,6 +125,11 @@ export interface BodyPerceptionContext {
|
|
|
125
125
|
fastingDays: number;
|
|
126
126
|
}
|
|
127
127
|
export type BodyParams = typeof BODY_DEFAULTS;
|
|
128
|
+
/**
|
|
129
|
+
* Turn on fullness (2.8.0): consumption for a need already past its comfort level carries a small cost in choice
|
|
130
|
+
* (see the fullness SCOPE in `cognition/`). Idempotent; off by default so existing hosts replay exactly.
|
|
131
|
+
*/
|
|
132
|
+
export declare function enableFullness(p: Person): void;
|
|
128
133
|
/** Turn on the fatigue store (2.6.0): accumulated exertion that recovers with rest and sleep. Idempotent. */
|
|
129
134
|
export declare function enableFatigueStore(p: Person): void;
|
|
130
135
|
export declare function createBody(init: PersonSpec['body'] | undefined, now: Minute): BodyState;
|
package/dist/body/body.js
CHANGED
|
@@ -238,9 +238,18 @@ export function sanitizeBody(b, now) {
|
|
|
238
238
|
delete b.lastSleep;
|
|
239
239
|
if (b.lastDowned !== undefined && !span(b.lastDowned))
|
|
240
240
|
delete b.lastDowned;
|
|
241
|
+
if (b.fullness !== undefined && b.fullness !== true)
|
|
242
|
+
delete b.fullness;
|
|
241
243
|
if (b.depletion !== undefined)
|
|
242
244
|
b.depletion = typeof b.depletion === 'number' && Number.isFinite(b.depletion) ? clamp01(b.depletion) : 0;
|
|
243
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Turn on fullness (2.8.0): consumption for a need already past its comfort level carries a small cost in choice
|
|
248
|
+
* (see the fullness SCOPE in `cognition/`). Idempotent; off by default so existing hosts replay exactly.
|
|
249
|
+
*/
|
|
250
|
+
export function enableFullness(p) {
|
|
251
|
+
p.body.fullness = true;
|
|
252
|
+
}
|
|
244
253
|
/** Turn on the fatigue store (2.6.0): accumulated exertion that recovers with rest and sleep. Idempotent. */
|
|
245
254
|
export function enableFatigueStore(p) {
|
|
246
255
|
if (p.body.depletion === undefined)
|
|
@@ -66,6 +66,16 @@ export declare const COGNITION_DEFAULTS: {
|
|
|
66
66
|
commitmentInertiaFade: number;
|
|
67
67
|
/** Social pull per partner on a joint offer: jointTrust × (relationship trust - 0.5). */
|
|
68
68
|
jointTrust: number;
|
|
69
|
+
/**
|
|
70
|
+
* Staying with a partner who is going (2.8.0, `Affordance.alongside`): 'together:<id>' = togetherScale ×
|
|
71
|
+
* closeness. It does not satiate with belonging: being together now does not remove the pull of not being left.
|
|
72
|
+
*/
|
|
73
|
+
togetherScale: number;
|
|
74
|
+
/**
|
|
75
|
+
* Fullness (2.8.0, opt-in `body.fullness`): 'full:<need>' = -fullnessScale × gain × over², over = how far the
|
|
76
|
+
* need's level is past its comfort threshold toward 1, for food, water and relief.
|
|
77
|
+
*/
|
|
78
|
+
fullnessScale: number;
|
|
69
79
|
/** Multiplier on standing advice terms relative to a live suggestion of the same strength (N9). */
|
|
70
80
|
rememberedScale: number;
|
|
71
81
|
/**
|
|
@@ -129,6 +139,8 @@ export interface DecideContext extends ConsiderContext {
|
|
|
129
139
|
quiet?: boolean;
|
|
130
140
|
/** A host command in force (1.6.0); passed to the will. */
|
|
131
141
|
command?: Command;
|
|
142
|
+
/** Drop the running activity's inertia bonus (2.8.0); passed to the will. */
|
|
143
|
+
reconsider?: boolean;
|
|
132
144
|
}
|
|
133
145
|
export interface Decision {
|
|
134
146
|
record: DecisionRecord;
|
|
@@ -23,6 +23,17 @@
|
|
|
23
23
|
* bias; and because losses are amplified too, a host must carry the benefit of paying a debt (a commitment,
|
|
24
24
|
* an advertised `safety` gain) or paying looks only like a loss. The spec's "security need" is read as the
|
|
25
25
|
* `safety` need: `security` is a value, not a need, and no need is added.
|
|
26
|
+
*
|
|
27
|
+
* SCOPE (2.8.0, both additive): an offer naming `alongside` partners (people already taking that course) gets one
|
|
28
|
+
* `together:<id>` term per known, living partner, `togetherScale` × closeness, without belonging satiation: the
|
|
29
|
+
* pull is not to be left behind, which stays when the pair is together and belonging is full (The Rope's resting
|
|
30
|
+
* partner, 2026-10-18 reproduction: belonging terms near 0.01 against inertia of 0.07-0.15). Shape only, after
|
|
31
|
+
* joint commitment and the sense of commitment in joint action (Gilbert 1990; Michael and Pacherie 2015; cited unaudited, research/empirical-models.md §6b); it does
|
|
32
|
+
* not model who leads, negotiation, or obligations between the partners. With opt-in `body.fullness`, an offer
|
|
33
|
+
* that advertises food, water or relief while that need is past its comfort threshold gets `full:<need>` =
|
|
34
|
+
* -fullnessScale × gain × over²: eating or drinking when full is mildly unpleasant (negative alliesthesia, unaudited citation, Cabanac
|
|
35
|
+
* 1971), so idle choice stops cycling short consumption at full hydration (The Rope, owner Kestrel export
|
|
36
|
+
* 2026-10-19). It models no bladder, sensory-specific satiety by food, or overeating for comfort.
|
|
26
37
|
*/
|
|
27
38
|
import { emotionLevel, inBreak } from '../affect/index.js';
|
|
28
39
|
import { agendaTermsWithGoalEstimates } from '../agenda/index.js';
|
|
@@ -32,12 +43,12 @@ import { clamp01, cmpStr, dexp, minuteOfDay, round } from '../core/index.js';
|
|
|
32
43
|
import { habitEase, habitPull } from '../habits/index.js';
|
|
33
44
|
import { expectedEffect } from '../memory/index.js';
|
|
34
45
|
import { methodEvidence } from '../methods/index.js';
|
|
35
|
-
import { urgencyOf } from '../needs/index.js';
|
|
46
|
+
import { levelOf, needThreshold, urgencyOf } from '../needs/index.js';
|
|
36
47
|
import { partneringTerms } from '../partnering/index.js';
|
|
37
48
|
import { promptIn, promptingTerms } from '../promptings/index.js';
|
|
38
49
|
import { bodyForecastTerm, receivedGoalRoute } from '../prospection/index.js';
|
|
39
50
|
import { reviewTerms } from '../review/index.js';
|
|
40
|
-
import { companionSteadiness, socialTerms } from '../social/index.js';
|
|
51
|
+
import { closeness, companionSteadiness, isDeceasedTie, relationshipWith, socialTerms, } from '../social/index.js';
|
|
41
52
|
import { PHYSIOLOGICAL_NEEDS, VALUE_KEYS } from '../types.js';
|
|
42
53
|
import { adviceWeight, resolveChoice, safeUtility, standingAdvice, suggestionTargets, voicesIn, } from '../will/index.js';
|
|
43
54
|
export const COGNITION_DEFAULTS = {
|
|
@@ -103,6 +114,16 @@ export const COGNITION_DEFAULTS = {
|
|
|
103
114
|
commitmentInertiaFade: 0.15,
|
|
104
115
|
/** Social pull per partner on a joint offer: jointTrust × (relationship trust - 0.5). */
|
|
105
116
|
jointTrust: 0.6,
|
|
117
|
+
/**
|
|
118
|
+
* Staying with a partner who is going (2.8.0, `Affordance.alongside`): 'together:<id>' = togetherScale ×
|
|
119
|
+
* closeness. It does not satiate with belonging: being together now does not remove the pull of not being left.
|
|
120
|
+
*/
|
|
121
|
+
togetherScale: 0.12,
|
|
122
|
+
/**
|
|
123
|
+
* Fullness (2.8.0, opt-in `body.fullness`): 'full:<need>' = -fullnessScale × gain × over², over = how far the
|
|
124
|
+
* need's level is past its comfort threshold toward 1, for food, water and relief.
|
|
125
|
+
*/
|
|
126
|
+
fullnessScale: 0.03,
|
|
106
127
|
// --- lane will+cognition ---
|
|
107
128
|
/** Multiplier on standing advice terms relative to a live suggestion of the same strength (N9). */
|
|
108
129
|
rememberedScale: 1,
|
|
@@ -226,16 +247,18 @@ export function materialValue(p, material, scarcity) {
|
|
|
226
247
|
const sat = Math.sign(material) * (1 - dexp(-Math.abs(material) / scale));
|
|
227
248
|
return K.materialWeight * sat * 2 * weight * (1 + K.scarcityMaterialGain * scarcity);
|
|
228
249
|
}
|
|
250
|
+
/** Needs whose consumption can overfill (2.8.0 fullness): what `body.consume` feeds. */
|
|
251
|
+
const FULL_NEEDS = ['food', 'water', 'relief'];
|
|
229
252
|
/** Score one option. Pure apart from reads; terms are rounded for stable snapshots. */
|
|
230
253
|
export function consider(p, aff, ctx) {
|
|
231
254
|
const K = COGNITION_DEFAULTS;
|
|
232
255
|
const scarcity = clamp01(Number.isFinite(ctx.scarcity) ? ctx.scarcity : 0);
|
|
233
256
|
const terms = [];
|
|
234
|
-
const push = (source, value) => {
|
|
257
|
+
const push = (source, value, ref) => {
|
|
235
258
|
const v = round(value);
|
|
236
259
|
// A non-finite term (bad host number) is dropped; the will vetoes the offer as invalid.
|
|
237
260
|
if (v !== 0 && Number.isFinite(v))
|
|
238
|
-
terms.push({ source, value: v });
|
|
261
|
+
terms.push(ref === undefined ? { source, value: v } : { source, value: v, ref });
|
|
239
262
|
};
|
|
240
263
|
// Needs: urgency × believed gain × believed success.
|
|
241
264
|
const expected = expectedEffect(p, aff);
|
|
@@ -248,6 +271,19 @@ export function consider(p, aff, ctx) {
|
|
|
248
271
|
const scarce = id === 'safety' ? 1 + K.scarcitySafetyGain * scarcity : 1;
|
|
249
272
|
push(`need:${id}`, K.needScale * u * gain * success * scarce);
|
|
250
273
|
}
|
|
274
|
+
// Fullness (2.8.0, opt-in): consuming for a need already past comfort is mildly unpleasant (alliesthesia).
|
|
275
|
+
if (p.body.fullness) {
|
|
276
|
+
for (const id of FULL_NEEDS) {
|
|
277
|
+
const gain = aff.advertises[id] ?? 0;
|
|
278
|
+
if (!(gain > 0))
|
|
279
|
+
continue;
|
|
280
|
+
const level = levelOf(ctx.needs, id);
|
|
281
|
+
const comfort = needThreshold(p, id);
|
|
282
|
+
const over = clamp01((level - comfort) / Math.max(1e-6, 1 - comfort));
|
|
283
|
+
if (over > 0)
|
|
284
|
+
push(`full:${id}`, -K.fullnessScale * Math.min(1, gain) * over * over);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
251
287
|
if (expected.samples > 0) {
|
|
252
288
|
let relevance = 1;
|
|
253
289
|
if (expected.valence > 0) {
|
|
@@ -324,6 +360,16 @@ export function consider(p, aff, ctx) {
|
|
|
324
360
|
// Social pulls. Per-person emotion tendencies enter at the same scale as tag tendencies.
|
|
325
361
|
for (const t of socialTerms(p, aff, social))
|
|
326
362
|
push(t.source, K.socialScale * (t.value > 0 ? social.satiation * refractory() : 1) * t.value);
|
|
363
|
+
// Going with partners who are already taking this course (2.8.0).
|
|
364
|
+
if (aff.alongside) {
|
|
365
|
+
for (const id of [...new Set(aff.alongside)].sort()) {
|
|
366
|
+
if (id === p.id || !p.social.relationships.some((r) => r.otherId === id))
|
|
367
|
+
continue;
|
|
368
|
+
if (isDeceasedTie(relationshipWith(p, id)))
|
|
369
|
+
continue;
|
|
370
|
+
push(`together:${id}`, K.togetherScale * closeness(p, id));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
327
373
|
// Joint offers: whether I trust the partners to do their share.
|
|
328
374
|
if (tags.includes('joint')) {
|
|
329
375
|
for (const id of [...(aff.with ?? [])].sort()) {
|
|
@@ -413,7 +459,7 @@ export function consider(p, aff, ctx) {
|
|
|
413
459
|
// Evening review (2.7.0, opt-in): a resolve formed after a lapse leans on that commitment's own term.
|
|
414
460
|
if (p.review)
|
|
415
461
|
for (const t of reviewTerms(p, terms))
|
|
416
|
-
push(t.source, t.value);
|
|
462
|
+
push(t.source, t.value, t.ref);
|
|
417
463
|
let utility = 0;
|
|
418
464
|
for (const t of terms)
|
|
419
465
|
utility += t.value;
|
|
@@ -453,6 +499,8 @@ export function scoreAll(p, affordances, ctx) {
|
|
|
453
499
|
};
|
|
454
500
|
if (ctx.command)
|
|
455
501
|
willCtx.command = ctx.command;
|
|
502
|
+
if (ctx.reconsider)
|
|
503
|
+
willCtx.reconsider = true;
|
|
456
504
|
return { sorted, considered, willCtx };
|
|
457
505
|
}
|
|
458
506
|
/**
|
|
@@ -479,6 +527,8 @@ export function decide(p, affordances, ctx) {
|
|
|
479
527
|
};
|
|
480
528
|
if (res.suggestion)
|
|
481
529
|
record.suggestion = res.suggestion;
|
|
530
|
+
if (ctx.reconsider)
|
|
531
|
+
record.reconsider = true;
|
|
482
532
|
if (res.suggestions && (res.suggestions.length > 1 || ctx.suggestions !== undefined))
|
|
483
533
|
record.suggestions = res.suggestions;
|
|
484
534
|
const needDeltas = {};
|
|
@@ -491,12 +541,14 @@ export function decide(p, affordances, ctx) {
|
|
|
491
541
|
const prompts = [];
|
|
492
542
|
const tagsOf = new Map(sorted.map((a) => [a.id, a.tags]));
|
|
493
543
|
for (const c of res.considered) {
|
|
494
|
-
const
|
|
544
|
+
const tags = tagsOf.get(c.affordanceId);
|
|
545
|
+
const seen = promptIn(c.terms, tags);
|
|
495
546
|
if (seen)
|
|
496
547
|
prompts.push({
|
|
497
548
|
affordanceId: c.affordanceId,
|
|
498
549
|
action: c.action,
|
|
499
550
|
...seen,
|
|
551
|
+
...(tags !== undefined ? { tags } : {}),
|
|
500
552
|
// A vetoed option was not turned from: the will blocked it, so the response earns no practice.
|
|
501
553
|
turnApplied: c.vetoed === undefined && c.terms.some((t) => t.source === 'promptings:turn'),
|
|
502
554
|
});
|
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.9.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';
|
|
@@ -30,7 +30,7 @@ export * from './partnering/index.ts';
|
|
|
30
30
|
export * from './person.ts';
|
|
31
31
|
export * from './promptings/index.ts';
|
|
32
32
|
export type { ConsequenceEvidence } from './prospection/index.ts';
|
|
33
|
-
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.ts';
|
|
33
|
+
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, } from './prospection/index.ts';
|
|
34
34
|
export * from './restore.ts';
|
|
35
35
|
export * from './review/index.ts';
|
|
36
36
|
export * from './scenarios/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.9.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';
|
|
@@ -26,7 +26,7 @@ export * from './needs/index.js';
|
|
|
26
26
|
export * from './partnering/index.js';
|
|
27
27
|
export * from './person.js';
|
|
28
28
|
export * from './promptings/index.js';
|
|
29
|
-
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.js';
|
|
29
|
+
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, } from './prospection/index.js';
|
|
30
30
|
export * from './restore.js';
|
|
31
31
|
export * from './review/index.js';
|
|
32
32
|
export * from './scenarios/index.js';
|
package/dist/migrate.js
CHANGED
|
@@ -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.8.0': {
|
|
62
|
+
to: '2.9.0',
|
|
63
|
+
note: 'prompting responses may carry a cue and the slice a practice half-life (both optional, absent as before); review terms may carry a ref; an outcome naming two or more commitments in fulfills now keeps each of them, where 2.8.0 kept the earliest-ending one',
|
|
64
|
+
apply: stamp,
|
|
65
|
+
},
|
|
66
|
+
'2.7.0': {
|
|
67
|
+
to: '2.8.0',
|
|
68
|
+
note: 'fullness (body.fullness) is optional and starts absent; offers may name alongside partners and decisions may reconsider; existing hosts retain exact behavior',
|
|
69
|
+
apply: stamp,
|
|
70
|
+
},
|
|
61
71
|
'2.6.0': {
|
|
62
72
|
to: '2.7.0',
|
|
63
73
|
note: 'the evening review (review) is optional and starts absent; existing hosts retain exact behavior',
|
package/dist/person.d.ts
CHANGED
|
@@ -103,6 +103,12 @@ export interface DecideOptions {
|
|
|
103
103
|
necessity?: boolean;
|
|
104
104
|
/** Why this decision was forced (recorded on the decision); defaults to the activity's pending interrupt. */
|
|
105
105
|
interrupt?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Reconsider on the merits (2.8.0): the running activity gets no inertia bonus in this decision, for a review the
|
|
108
|
+
* host forces because something changed (a partner leaving, a call). Promise inertia (`begin` with a promise) and
|
|
109
|
+
* every other term stay. Absent = the usual hysteresis.
|
|
110
|
+
*/
|
|
111
|
+
reconsider?: boolean;
|
|
106
112
|
}
|
|
107
113
|
/**
|
|
108
114
|
* How this person would answer a suggestion right now, without changing them or consuming randomness
|
package/dist/person.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { actionTendencies, advanceAffect, appraise, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, feel, readAffect, regulate, release, skipAffect, skipCrisis, strain, tendencyEmotions, } from './affect/index.js';
|
|
23
23
|
import { advanceAgenda, createAgenda, onFinished, promise, proposeGoals, revisePurposes, } from './agenda/index.js';
|
|
24
24
|
import { advanceBeliefs, attend, believe, confirm, credence } from './beliefs/index.js';
|
|
25
|
-
import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, enableFatigueStore, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
|
|
25
|
+
import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, enableFatigueStore, enableFullness, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
|
|
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';
|
|
@@ -37,7 +37,7 @@ 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
39
|
import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
|
|
40
|
-
import { bodyForecastFor, consumeConsequences, forecastLine, pruneConsequences, receiveConsequences, } from './prospection/index.js';
|
|
40
|
+
import { bodyForecastFor, consumeConsequences, forecastLine, forecastThresholds, pruneConsequences, receiveConsequences, } from './prospection/index.js';
|
|
41
41
|
import { enableReview, reviewIfDue } from './review/index.js';
|
|
42
42
|
import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
|
|
43
43
|
import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
|
|
@@ -157,6 +157,8 @@ export function createPerson(spec) {
|
|
|
157
157
|
enableSkillRetention(p);
|
|
158
158
|
if (on?.fatigueStore)
|
|
159
159
|
enableFatigueStore(p);
|
|
160
|
+
if (on?.fullness)
|
|
161
|
+
enableFullness(p);
|
|
160
162
|
if (on?.promptings)
|
|
161
163
|
enablePromptings(p, on.promptings);
|
|
162
164
|
if (on?.review)
|
|
@@ -756,6 +758,8 @@ function decisionInputs(p, affordances, id, opts) {
|
|
|
756
758
|
const cmd = opts.command ?? p.will.command;
|
|
757
759
|
if (cmd)
|
|
758
760
|
ctx.command = commandOf(cmd);
|
|
761
|
+
if (opts.reconsider)
|
|
762
|
+
ctx.reconsider = true;
|
|
759
763
|
return { list, ctx };
|
|
760
764
|
}
|
|
761
765
|
/** The `Command` part of a command in force (without the will's bookkeeping). */
|
|
@@ -1430,5 +1434,5 @@ export function nafsReading(p) {
|
|
|
1430
1434
|
*/
|
|
1431
1435
|
export function bodyForecast(p, aff) {
|
|
1432
1436
|
const spans = bodyForecastFor(p, aff, lifeModifiers(p), fastingCtx(p));
|
|
1433
|
-
return { spans, line: forecastLine(spans) };
|
|
1437
|
+
return { spans, line: forecastLine(spans, forecastThresholds(aff)) };
|
|
1434
1438
|
}
|
|
@@ -26,8 +26,17 @@ export declare const PROMPTINGS_DEFAULTS: {
|
|
|
26
26
|
* keeps its practice and last use (the spec's `practice` only seeds new responses). Returns the state.
|
|
27
27
|
*/
|
|
28
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;
|
|
29
|
+
/** Practice of a response at `now`, after disuse decay (`halfLifeDays` defaults to 60, as before 2.9.0). Pure. */
|
|
30
|
+
export declare function practiceAt(r: PromptingResponse, now: Minute, halfLifeDays?: number): Unit;
|
|
31
|
+
/** What a prompt offers a cue to match (2.9.0). */
|
|
32
|
+
export interface PromptContext {
|
|
33
|
+
action: string;
|
|
34
|
+
tags?: readonly string[] | undefined;
|
|
35
|
+
/** The opposing term's source: 'norm:x' or 'goal:g'. */
|
|
36
|
+
opposedBy: string;
|
|
37
|
+
}
|
|
38
|
+
/** Whether a response's cue matches a prompt: its action, one of its tags, or the opposing norm or goal. Pure. */
|
|
39
|
+
export declare function responseCueMatches(cue: string, at: PromptContext): boolean;
|
|
31
40
|
/** Whether an option's terms carry a noticed prompt: its strongest pull and the opposing term. Pure. */
|
|
32
41
|
export declare function promptIn(terms: readonly Term[], tags: readonly string[] | undefined): {
|
|
33
42
|
source: string;
|
|
@@ -46,6 +55,8 @@ export interface PromptingSeen {
|
|
|
46
55
|
opposedBy: string;
|
|
47
56
|
/** Whether a turning-away term was applied. */
|
|
48
57
|
turnApplied: boolean;
|
|
58
|
+
/** The offer's tags (2.9.0), for matching a response's cue. */
|
|
59
|
+
tags?: readonly string[] | undefined;
|
|
49
60
|
}
|
|
50
61
|
/** What `notePromptings` did, for the composite (the `fault` guilt goes through affect). */
|
|
51
62
|
export interface PromptingsNoted {
|
|
@@ -68,10 +79,12 @@ export interface PromptingsReadingView {
|
|
|
68
79
|
turned: number;
|
|
69
80
|
passed: number;
|
|
70
81
|
assented: number;
|
|
82
|
+
/** `cue` only when the response has one (2.9.0). */
|
|
71
83
|
responses: {
|
|
72
84
|
id: string;
|
|
73
85
|
kind: PromptingResponseKind;
|
|
74
86
|
practice: Unit;
|
|
87
|
+
cue?: string;
|
|
75
88
|
}[];
|
|
76
89
|
/** Vocabulary for the last few prompts, newest last. */
|
|
77
90
|
lines: string[];
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Detection reads an option's traced terms and changes none of them: the pull is the sum of positive `habit`,
|
|
10
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
|
|
11
|
+
* prompt when the pull reaches `noticeAt` while a `norm:*` or `goal:*` term on the same option is negative (goal
|
|
12
|
+
* terms are never negative, see `agendaTerms`, so in practice the opposition is a held norm).
|
|
12
13
|
* Abstention windows are not detected (they veto, and carry no term). A person with a response gets one extra
|
|
13
14
|
* term, `promptings:turn` = -turnMax × practice, on that option; it is bounded and absent at survival desperation,
|
|
14
15
|
* so a desperate bodily need still wins (2:286). Each prompt is recorded once per decision point (a review that
|
|
@@ -16,6 +17,16 @@
|
|
|
16
17
|
* or `passed` (not chosen, no response). Practice grows with diminishing gains once per decision point where the
|
|
17
18
|
* 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
|
*
|
|
20
|
+
* Cued responses and host time (2.9.0, both opt-in): a response may carry a `cue` (an action, an offer tag, or the
|
|
21
|
+
* opposing norm or goal), for a phrase learned at one pull. At a prompt the person uses the most practised response
|
|
22
|
+
* whose cue matches it; when none matches, the most practised response without a cue. A cued response never serves
|
|
23
|
+
* a prompt it does not match, so a phrase learned at the alarm does not also turn the person from gossip (the
|
|
24
|
+
* literal "global best" fallback would bring that back). Each response used at a decision point gains once. Without
|
|
25
|
+
* cues every response is uncued and this is the 2.4.0 rule exactly. `practiceHalfLifeDays` in `enablePromptings`
|
|
26
|
+
* replaces the 60-day half-life for the whole person (a one-week game can let a phrase fade); it is one setting per
|
|
27
|
+
* person, not per response, because the need is the host's time scale and no source gives phrases different decay.
|
|
28
|
+
* Practice is decayed lazily from `lastUsedAt`, so a new half-life also applies to disuse already elapsed.
|
|
29
|
+
*
|
|
19
30
|
* Held readings (reconciliation.md §5): `whisper`, `inclination`, `intrusion` and `none` give identical decisions
|
|
20
31
|
* and differ only in the vocabulary of `promptingLine`. `fault` (the scrupulous reading, which both the tradition
|
|
21
32
|
* and clinicians reject) adds a small guilt emotion at the prompt itself, from the person's own understanding;
|
|
@@ -71,7 +82,14 @@ export function enablePromptings(p, spec = {}) {
|
|
|
71
82
|
continue;
|
|
72
83
|
const held = prev?.responses.find((x) => x.id === r.id);
|
|
73
84
|
const seed = Number.isFinite(r.practice) ? clamp01(r.practice) : 0.1;
|
|
74
|
-
|
|
85
|
+
const next = held
|
|
86
|
+
? { ...held, kind: r.kind }
|
|
87
|
+
: { id: r.id, kind: r.kind, practice: seed, lastUsedAt: p.now };
|
|
88
|
+
// The spec's cue wins; a response re-enabled without one loses its old cue.
|
|
89
|
+
delete next.cue;
|
|
90
|
+
if (cueOk(r.cue))
|
|
91
|
+
next.cue = r.cue;
|
|
92
|
+
responses.push(next);
|
|
75
93
|
}
|
|
76
94
|
const state = {
|
|
77
95
|
reading: spec.reading && READINGS.includes(spec.reading) ? spec.reading : 'none',
|
|
@@ -82,22 +100,44 @@ export function enablePromptings(p, spec = {}) {
|
|
|
82
100
|
};
|
|
83
101
|
if (spec.readingSource !== undefined)
|
|
84
102
|
state.readingSource = spec.readingSource;
|
|
103
|
+
if (halfLifeOk(spec.practiceHalfLifeDays))
|
|
104
|
+
state.practiceHalfLifeDays = spec.practiceHalfLifeDays;
|
|
85
105
|
p.promptings = state;
|
|
86
106
|
return state;
|
|
87
107
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
const cueOk = (x) => typeof x === 'string' && x !== '' && x.length <= 200;
|
|
109
|
+
const halfLifeOk = (x) => typeof x === 'number' && Number.isFinite(x) && x > 0 && x <= 36500;
|
|
110
|
+
const halfLifeOf = (s) => s.practiceHalfLifeDays ?? PROMPTINGS_DEFAULTS.practiceHalfLifeDays;
|
|
111
|
+
/** Practice of a response at `now`, after disuse decay (`halfLifeDays` defaults to 60, as before 2.9.0). Pure. */
|
|
112
|
+
export function practiceAt(r, now, halfLifeDays = PROMPTINGS_DEFAULTS.practiceHalfLifeDays) {
|
|
113
|
+
return clamp01(halve(r.practice, now - r.lastUsedAt, halfLifeDays));
|
|
114
|
+
}
|
|
115
|
+
/** Whether a response's cue matches a prompt: its action, one of its tags, or the opposing norm or goal. Pure. */
|
|
116
|
+
export function responseCueMatches(cue, at) {
|
|
117
|
+
if (cue === at.action || cue === at.opposedBy || (at.tags?.includes(cue) ?? false))
|
|
118
|
+
return true;
|
|
119
|
+
const colon = at.opposedBy.indexOf(':');
|
|
120
|
+
return colon >= 0 && cue === at.opposedBy.slice(colon + 1);
|
|
91
121
|
}
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
122
|
+
/**
|
|
123
|
+
* The response a person would use now at prompt `at`: the most practised whose cue matches, else the most
|
|
124
|
+
* practised without a cue; ties by id. Without cues this is the most practised response (2.4.0).
|
|
125
|
+
*/
|
|
126
|
+
function bestResponse(s, now, at) {
|
|
127
|
+
let cued;
|
|
128
|
+
let plain;
|
|
129
|
+
const better = (b, r, x) => !b || x > b.practice || (x === b.practice && r.id < b.r.id);
|
|
130
|
+
const half = halfLifeOf(s);
|
|
95
131
|
for (const r of s.responses) {
|
|
96
|
-
const practice = practiceAt(r, now);
|
|
97
|
-
if (
|
|
98
|
-
|
|
132
|
+
const practice = practiceAt(r, now, half);
|
|
133
|
+
if (r.cue === undefined) {
|
|
134
|
+
if (better(plain, r, practice))
|
|
135
|
+
plain = { r, practice };
|
|
136
|
+
}
|
|
137
|
+
else if (responseCueMatches(r.cue, at) && better(cued, r, practice))
|
|
138
|
+
cued = { r, practice };
|
|
99
139
|
}
|
|
100
|
-
return
|
|
140
|
+
return cued ?? plain;
|
|
101
141
|
}
|
|
102
142
|
const isPull = (source, tagged) => tagged ||
|
|
103
143
|
source === 'habit' ||
|
|
@@ -145,10 +185,13 @@ export function promptingTerms(p, action, terms, tags, now, desperation) {
|
|
|
145
185
|
}
|
|
146
186
|
if (desperation >= PROMPTINGS_DEFAULTS.survivalDesperation)
|
|
147
187
|
return out;
|
|
148
|
-
|
|
149
|
-
if (!best || best.practice <= 0)
|
|
188
|
+
if (s.responses.length === 0)
|
|
150
189
|
return out;
|
|
151
|
-
|
|
190
|
+
const seen = promptIn([...terms, ...out], tags);
|
|
191
|
+
if (!seen)
|
|
192
|
+
return out;
|
|
193
|
+
const best = bestResponse(s, now, { action, tags, opposedBy: seen.opposedBy });
|
|
194
|
+
if (!best || best.practice <= 0)
|
|
152
195
|
return out;
|
|
153
196
|
const v = round(-PROMPTINGS_DEFAULTS.turnMax * best.practice);
|
|
154
197
|
if (v < 0)
|
|
@@ -165,11 +208,12 @@ export function notePromptings(p, seen, chosenId, now) {
|
|
|
165
208
|
if (!s || seen.length === 0)
|
|
166
209
|
return noted;
|
|
167
210
|
const D = PROMPTINGS_DEFAULTS;
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
211
|
+
// One practice gain per response per decision point, however many options it served: the larger one if any held.
|
|
212
|
+
// Responses are chosen before any practice moves, as `promptingTerms` chose them.
|
|
213
|
+
const gains = new Map();
|
|
171
214
|
for (const x of seen) {
|
|
172
215
|
const assented = x.affordanceId === chosenId;
|
|
216
|
+
const best = bestResponse(s, now, x);
|
|
173
217
|
const used = x.turnApplied && best !== undefined;
|
|
174
218
|
const rec = {
|
|
175
219
|
at: now,
|
|
@@ -181,7 +225,7 @@ export function notePromptings(p, seen, chosenId, now) {
|
|
|
181
225
|
};
|
|
182
226
|
if (used && best) {
|
|
183
227
|
rec.responseId = best.r.id;
|
|
184
|
-
|
|
228
|
+
gains.set(best.r, Math.max(gains.get(best.r) ?? 0, assented ? D.gainAttempt : D.gainTurned));
|
|
185
229
|
}
|
|
186
230
|
s.recent.push(rec);
|
|
187
231
|
noted.records.push(rec);
|
|
@@ -194,10 +238,12 @@ export function notePromptings(p, seen, chosenId, now) {
|
|
|
194
238
|
}
|
|
195
239
|
}
|
|
196
240
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
241
|
+
for (const [r, gain] of gains) {
|
|
242
|
+
if (gain <= 0)
|
|
243
|
+
continue;
|
|
244
|
+
const practice = practiceAt(r, now, halfLifeOf(s));
|
|
245
|
+
r.practice = round(clamp01(practice + gain * (1 - practice)));
|
|
246
|
+
r.lastUsedAt = now;
|
|
201
247
|
}
|
|
202
248
|
if (s.recent.length > D.maxRecent)
|
|
203
249
|
s.recent.splice(0, s.recent.length - D.maxRecent);
|
|
@@ -249,7 +295,12 @@ export function promptingsReading(p, now = p.now) {
|
|
|
249
295
|
turned: count('turned'),
|
|
250
296
|
passed: count('passed'),
|
|
251
297
|
assented: count('assented'),
|
|
252
|
-
responses: s.responses.map((r) => ({
|
|
298
|
+
responses: s.responses.map((r) => ({
|
|
299
|
+
id: r.id,
|
|
300
|
+
kind: r.kind,
|
|
301
|
+
practice: round(practiceAt(r, now, halfLifeOf(s))),
|
|
302
|
+
...(r.cue !== undefined ? { cue: r.cue } : {}),
|
|
303
|
+
})),
|
|
253
304
|
lines: s.recent.slice(-5).map((r) => promptingLine(s.reading, r.outcome, kindOf(r.responseId))),
|
|
254
305
|
};
|
|
255
306
|
if (s.readingSource !== undefined)
|
|
@@ -298,12 +349,15 @@ export function sanitizePromptings(input, now) {
|
|
|
298
349
|
continue;
|
|
299
350
|
if (!minute(r.lastUsedAt) || r.lastUsedAt > now || responses.some((x) => x.id === r.id))
|
|
300
351
|
continue;
|
|
301
|
-
|
|
352
|
+
const kept = {
|
|
302
353
|
id: r.id,
|
|
303
354
|
kind: r.kind,
|
|
304
355
|
practice: r.practice,
|
|
305
356
|
lastUsedAt: r.lastUsedAt,
|
|
306
|
-
}
|
|
357
|
+
};
|
|
358
|
+
if (cueOk(r.cue))
|
|
359
|
+
kept.cue = r.cue;
|
|
360
|
+
responses.push(kept);
|
|
307
361
|
}
|
|
308
362
|
const recent = [];
|
|
309
363
|
for (const r of input.recent.slice(-D.maxRecent)) {
|
|
@@ -344,5 +398,7 @@ export function sanitizePromptings(input, now) {
|
|
|
344
398
|
};
|
|
345
399
|
if (str(input.readingSource))
|
|
346
400
|
out.readingSource = input.readingSource;
|
|
401
|
+
if (halfLifeOk(input.practiceHalfLifeDays))
|
|
402
|
+
out.practiceHalfLifeDays = input.practiceHalfLifeDays;
|
|
347
403
|
return out;
|
|
348
404
|
}
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
|
|
17
17
|
* (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
|
|
18
18
|
* predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
|
|
19
|
-
* default.
|
|
19
|
+
* default. 2.8.0: an offer may carry its own thresholds (`Affordance.forecastAt`, from The Glance's night
|
|
20
|
+
* keepers); offers without it read exactly as before.
|
|
20
21
|
*/
|
|
21
22
|
import { type BodyForecastSpan, type BodyPerceptionContext } from '../body/index.ts';
|
|
22
23
|
import type { Affordance, LifeModifiers, Person, Term } from '../types.ts';
|
|
@@ -31,6 +32,14 @@ export declare const FORECAST_DEFAULTS: {
|
|
|
31
32
|
readonly maxSpans: 4;
|
|
32
33
|
readonly maxMinutes: number;
|
|
33
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The forecast thresholds for an offer (2.8.0): its `forecastAt` when it forms a valid pair, else the defaults.
|
|
37
|
+
* Pure.
|
|
38
|
+
*/
|
|
39
|
+
export declare function forecastThresholds(aff: Pick<Affordance, 'forecastAt'>): {
|
|
40
|
+
comfortAt: number;
|
|
41
|
+
spentAt: number;
|
|
42
|
+
};
|
|
34
43
|
/** The person's forecast for an offer: the act, then its follow-on spans. Empty without `followedBy`. Pure. */
|
|
35
44
|
export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): BodyForecastSpan[];
|
|
36
45
|
/**
|
|
@@ -41,5 +50,8 @@ export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeMo
|
|
|
41
50
|
* too, which cancelled the very case the forecast exists for. Pure.
|
|
42
51
|
*/
|
|
43
52
|
export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): Term | undefined;
|
|
44
|
-
/** A first-person line for the forecast (narration only). */
|
|
45
|
-
export declare function forecastLine(spans: readonly BodyForecastSpan[]
|
|
53
|
+
/** A first-person line for the forecast (narration only); `at` is the offer's thresholds (2.8.0, default as before). */
|
|
54
|
+
export declare function forecastLine(spans: readonly BodyForecastSpan[], at?: {
|
|
55
|
+
comfortAt: number;
|
|
56
|
+
spentAt: number;
|
|
57
|
+
}): string;
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
|
|
17
17
|
* (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
|
|
18
18
|
* predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
|
|
19
|
-
* default.
|
|
19
|
+
* default. 2.8.0: an offer may carry its own thresholds (`Affordance.forecastAt`, from The Glance's night
|
|
20
|
+
* keepers); offers without it read exactly as before.
|
|
20
21
|
*/
|
|
21
22
|
import { BODY_DEFAULTS, forecastBody, } from '../body/index.js';
|
|
22
23
|
import { clamp01, round } from '../core/index.js';
|
|
@@ -31,6 +32,21 @@ export const FORECAST_DEFAULTS = {
|
|
|
31
32
|
maxSpans: 4,
|
|
32
33
|
maxMinutes: 24 * 60,
|
|
33
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The forecast thresholds for an offer (2.8.0): its `forecastAt` when it forms a valid pair, else the defaults.
|
|
37
|
+
* Pure.
|
|
38
|
+
*/
|
|
39
|
+
export function forecastThresholds(aff) {
|
|
40
|
+
const D = FORECAST_DEFAULTS;
|
|
41
|
+
const at = aff.forecastAt;
|
|
42
|
+
if (!at || typeof at !== 'object')
|
|
43
|
+
return { comfortAt: D.comfortAt, spentAt: D.spentAt };
|
|
44
|
+
const c = Number.isFinite(at.comfortAt) ? at.comfortAt : D.comfortAt;
|
|
45
|
+
const s = Number.isFinite(at.spentAt) ? at.spentAt : D.spentAt;
|
|
46
|
+
return c >= 0 && c < s && s <= 1
|
|
47
|
+
? { comfortAt: c, spentAt: s }
|
|
48
|
+
: { comfortAt: D.comfortAt, spentAt: D.spentAt };
|
|
49
|
+
}
|
|
34
50
|
const smooth = (a, b, x) => {
|
|
35
51
|
const t = clamp01((x - a) / (b - a));
|
|
36
52
|
return t * t * (3 - 2 * t);
|
|
@@ -81,12 +97,13 @@ export function bodyForecastTerm(p, aff, mods, perception) {
|
|
|
81
97
|
if (spans.length === 0)
|
|
82
98
|
return undefined;
|
|
83
99
|
const D = FORECAST_DEFAULTS;
|
|
100
|
+
const at = forecastThresholds(aff);
|
|
84
101
|
const worst = Math.max(...spans.map((s) => s.worst));
|
|
85
|
-
const v = round(-D.forecastScale * smooth(
|
|
102
|
+
const v = round(-D.forecastScale * smooth(at.comfortAt, at.spentAt, worst));
|
|
86
103
|
return v < 0 ? { source: 'forecast:body', value: v } : undefined;
|
|
87
104
|
}
|
|
88
|
-
/** A first-person line for the forecast (narration only). */
|
|
89
|
-
export function forecastLine(spans) {
|
|
105
|
+
/** A first-person line for the forecast (narration only); `at` is the offer's thresholds (2.8.0, default as before). */
|
|
106
|
+
export function forecastLine(spans, at = FORECAST_DEFAULTS) {
|
|
90
107
|
const after = spans.slice(1);
|
|
91
108
|
if (after.length === 0)
|
|
92
109
|
return '';
|
|
@@ -95,9 +112,9 @@ export function forecastLine(spans) {
|
|
|
95
112
|
if (s.worst > w.worst)
|
|
96
113
|
w = s;
|
|
97
114
|
const what = { fatigue: 'spent', sleepiness: 'too sleepy', hunger: 'too hungry', thirst: 'too thirsty' }[w.worstOf];
|
|
98
|
-
if (w.worst >=
|
|
115
|
+
if (w.worst >= at.spentAt)
|
|
99
116
|
return `I can do it, but I'll be ${what} for ${w.label}.`;
|
|
100
|
-
if (w.worst >=
|
|
117
|
+
if (w.worst >= at.comfortAt)
|
|
101
118
|
return `I can do it, though ${w.label} will be hard.`;
|
|
102
119
|
return `I'll still have enough for ${w.label}.`;
|
|
103
120
|
}
|
package/dist/review/review.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* the commitments the chronicle recorded as kept or broken since their last review, and for each one they broke
|
|
4
4
|
* forms or renews a *resolve*: an if–then intention for that commitment's next occasion (Gollwitzer & Sheeran
|
|
5
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
|
|
6
|
+
* `review:resolve` (each resolve counted once per option, the sum clamped to ±`resolveScale`; 2.9.0: the term's
|
|
7
|
+
* `ref` is the resolve's key when exactly one resolve weighed in), with the sign of the commitment's own term (toward a duty, away from an abstention's breaking
|
|
7
8
|
* act), scaled by its strength. Strength grows with diminishing gains at each further lapse, relaxes once the
|
|
8
9
|
* commitment is kept, and fades over `halfLifeDays` of disuse. Released and excused commitments (exemptions,
|
|
9
10
|
* sleep, necessity) form no resolve: what was beyond capacity is not held against the person (2:286). The review
|
|
@@ -23,6 +24,11 @@
|
|
|
23
24
|
* piety, repentance as a ruling (tawba conditions are not modelled), effects on sleep onset (Harvey's model, E6, is
|
|
24
25
|
* not evidence enough), or that review improves outcomes beyond the borrowed if–then shape. It draws no randomness;
|
|
25
26
|
* without `enableReview` nothing here runs and existing runs replay byte for byte.
|
|
27
|
+
*
|
|
28
|
+
* Host notes (2.9.0, docs only). `reviewLine` sets commitment labels into "I didn't keep …" and "Today I kept …",
|
|
29
|
+
* so a label should be a noun phrase ("my call to Mum", "the evening prayer"), not a verb phrase ("calling Mum").
|
|
30
|
+
* A recurring commitment lapses on any day its window passes unlived: a host that skips a day (no service, a
|
|
31
|
+
* fast-forward) should still live, or release, that day's occasions, or the review counts each one as missed.
|
|
26
32
|
*/
|
|
27
33
|
import { clamp01, dexp, isObj, minuteOfDay, round } from '../core/index.js';
|
|
28
34
|
export const REVIEW_DEFAULTS = {
|
|
@@ -72,6 +78,7 @@ export function reviewTerms(p, terms) {
|
|
|
72
78
|
return [];
|
|
73
79
|
let value = 0;
|
|
74
80
|
const used = new Set();
|
|
81
|
+
let only;
|
|
75
82
|
for (const t of terms) {
|
|
76
83
|
const m = /^(commitment|abstain):(.+)$/.exec(t.source);
|
|
77
84
|
if (!m || t.value === 0)
|
|
@@ -83,11 +90,18 @@ export function reviewTerms(p, terms) {
|
|
|
83
90
|
if (!r || used.has(r.key))
|
|
84
91
|
continue;
|
|
85
92
|
used.add(r.key);
|
|
93
|
+
only = r.key;
|
|
86
94
|
value += Math.sign(t.value) * REVIEW_DEFAULTS.resolveScale * r.strength;
|
|
87
95
|
}
|
|
88
96
|
const max = REVIEW_DEFAULTS.resolveScale;
|
|
89
97
|
value = Math.max(-max, Math.min(max, value));
|
|
90
|
-
|
|
98
|
+
if (value === 0)
|
|
99
|
+
return [];
|
|
100
|
+
const term = { source: 'review:resolve', value: round(value) };
|
|
101
|
+
// Name the resolve when exactly one weighed in (2.9.0, The Pass): "he turned from the jar, as he said he would".
|
|
102
|
+
if (used.size === 1 && only !== undefined)
|
|
103
|
+
term.ref = only;
|
|
104
|
+
return [term];
|
|
91
105
|
}
|
|
92
106
|
const fade = (s, days) => days <= 0 ? s : s * dexp((-Math.LN2 * days) / REVIEW_DEFAULTS.halfLifeDays);
|
|
93
107
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -91,6 +91,11 @@ export interface BodyState {
|
|
|
91
91
|
* recovering with rest and faster with sleep. Raises felt fatigue and lowers capacity. Absent = no store.
|
|
92
92
|
*/
|
|
93
93
|
depletion?: Unit;
|
|
94
|
+
/**
|
|
95
|
+
* Fullness (2.8.0, opt-in via `enableFullness`): when true, eating, drinking or taking relief for a need already
|
|
96
|
+
* past its comfort level costs a little (`full:<need>` term) instead of being free. Absent = no term.
|
|
97
|
+
*/
|
|
98
|
+
fullness?: boolean;
|
|
94
99
|
asleep: boolean;
|
|
95
100
|
/** Minute the current sleep or wake period began. */
|
|
96
101
|
since: Minute;
|
|
@@ -506,7 +511,10 @@ export interface Commitment {
|
|
|
506
511
|
status: 'pending' | 'kept' | 'broken' | 'released';
|
|
507
512
|
/** Optional recurrence: re-create after it closes (e.g. daily prayer windows). */
|
|
508
513
|
recurEvery?: number;
|
|
509
|
-
/**
|
|
514
|
+
/**
|
|
515
|
+
* Optional display name, e.g. 'Maghrib'; used in counter-offers ("after I pray Maghrib") and in `reviewLine`
|
|
516
|
+
* ("I didn't keep my call to Mum"), so write it as a noun phrase.
|
|
517
|
+
*/
|
|
510
518
|
label?: string;
|
|
511
519
|
/** For kind 'abstain': action ids whose completion inside the window breaks it. */
|
|
512
520
|
violatedBy?: string[];
|
|
@@ -832,7 +840,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
|
|
|
832
840
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
833
841
|
* `dutyReviewAt`). Person shape unchanged.
|
|
834
842
|
*/
|
|
835
|
-
export declare const ENGINE_VERSION = "2.
|
|
843
|
+
export declare const ENGINE_VERSION = "2.9.0";
|
|
836
844
|
/** A pregnancy carried by this person (host-driven; see `family/`). */
|
|
837
845
|
export interface Pregnancy {
|
|
838
846
|
fatherId: PersonId;
|
|
@@ -1067,6 +1075,12 @@ export interface Affordance {
|
|
|
1067
1075
|
placeId?: EntityId;
|
|
1068
1076
|
/** Other people taking part. */
|
|
1069
1077
|
with?: PersonId[];
|
|
1078
|
+
/**
|
|
1079
|
+
* People already taking this course (2.8.0): a partner moving on, a companion leaving for home. Choosing it keeps
|
|
1080
|
+
* the person with them, a `together:<id>` term from closeness (`cognition` SCOPE). Unlike `with`, it says who is
|
|
1081
|
+
* going this way, not who would take part. Absent = no term, exactly as before.
|
|
1082
|
+
*/
|
|
1083
|
+
alongside?: PersonId[];
|
|
1070
1084
|
/** Minutes the activity occupies (including travel if the host folds it in). */
|
|
1071
1085
|
duration: number;
|
|
1072
1086
|
/** Physical exertion 0..1. */
|
|
@@ -1089,7 +1103,11 @@ export interface Affordance {
|
|
|
1089
1103
|
/** Free tags: 'work', 'leisure', 'social', 'worship', 'risky', 'outdoors', ... */
|
|
1090
1104
|
tags?: string[];
|
|
1091
1105
|
norms?: NormTag[];
|
|
1092
|
-
/**
|
|
1106
|
+
/**
|
|
1107
|
+
* Commitment ids this would fulfil if completed. Listing two or more keeps every listed one that is pending and
|
|
1108
|
+
* in window, and no other (2.9.0), not even an earlier-ending match by action; listing one keeps the
|
|
1109
|
+
* earliest-ending match, explicit or by action, as before.
|
|
1110
|
+
*/
|
|
1093
1111
|
fulfills?: string[];
|
|
1094
1112
|
/** Goal ids this advances. */
|
|
1095
1113
|
advances?: string[];
|
|
@@ -1105,6 +1123,16 @@ export interface Affordance {
|
|
|
1105
1123
|
* spent during them. Absent = no forecast, exactly as before.
|
|
1106
1124
|
*/
|
|
1107
1125
|
followedBy?: FollowOn[];
|
|
1126
|
+
/**
|
|
1127
|
+
* Readout thresholds for this offer's forecast (2.8.0), for hosts whose people live higher on a readout (a
|
|
1128
|
+
* night keeper at sleepiness 0.7). `comfortAt` is where the forecast starts to weigh and `spentAt` where it
|
|
1129
|
+
* weighs fully; either absent, or a pair that is not 0 ≤ comfortAt < spentAt ≤ 1, falls back to
|
|
1130
|
+
* `FORECAST_DEFAULTS`. Ignored without `followedBy`.
|
|
1131
|
+
*/
|
|
1132
|
+
forecastAt?: {
|
|
1133
|
+
comfortAt?: Unit;
|
|
1134
|
+
spentAt?: Unit;
|
|
1135
|
+
};
|
|
1108
1136
|
/** Set on a partner's mirror offer by the joint protocol in `sim/` (the proposal id). */
|
|
1109
1137
|
jointId?: string;
|
|
1110
1138
|
/** Material gain/cost in host units (e.g. coins). Valued via security/achievement/power values. */
|
|
@@ -1228,7 +1256,11 @@ export interface Outcome {
|
|
|
1228
1256
|
* under protest and 1 otherwise. Lower quality lowers felt valence and therefore learned expectations.
|
|
1229
1257
|
*/
|
|
1230
1258
|
quality?: Unit;
|
|
1231
|
-
/**
|
|
1259
|
+
/**
|
|
1260
|
+
* Commitment ids this outcome keeps (defaults to the activity's `Affordance.fulfills`). With two or more listed,
|
|
1261
|
+
* every listed one pending and in window is kept, and no other (2.9.0); with one, one completion keeps the
|
|
1262
|
+
* earliest-ending match.
|
|
1263
|
+
*/
|
|
1232
1264
|
fulfills?: string[];
|
|
1233
1265
|
/** Goal ids this outcome advances (defaults to the activity's `Affordance.advances`). */
|
|
1234
1266
|
advances?: string[];
|
|
@@ -1312,9 +1344,16 @@ export interface Term {
|
|
|
1312
1344
|
* 'goal:g1', 'habit', 'emotion:fear:risky' (emotion id, then the tag it acts through), 'effort', 'risk',
|
|
1313
1345
|
* 'suggestion:player', 'social:p2', 'expectation', 'autonomy' (reactance to a pushing voice),
|
|
1314
1346
|
* 'promptings:turn' (a practised turning-away), 'forecast:body' (expected
|
|
1315
|
-
* bodily state during `followedBy` spans), 'promptings:echo' (recurrence under the `fault` reading)
|
|
1347
|
+
* bodily state during `followedBy` spans), 'promptings:echo' (recurrence under the `fault` reading),
|
|
1348
|
+
* 'review:resolve' (a resolve from the evening review) */
|
|
1316
1349
|
source: string;
|
|
1317
1350
|
value: number;
|
|
1351
|
+
/**
|
|
1352
|
+
* What a generic source refers to (2.9.0), so `source` stays stable for hosts that match it exactly. Set on
|
|
1353
|
+
* `review:resolve` to the resolve's key (`Resolve.key`) when exactly one resolve weighed in; absent when several
|
|
1354
|
+
* did. Absent on every other term.
|
|
1355
|
+
*/
|
|
1356
|
+
ref?: string;
|
|
1318
1357
|
}
|
|
1319
1358
|
export interface Considered {
|
|
1320
1359
|
affordanceId: string;
|
|
@@ -1354,6 +1393,8 @@ export interface DecisionRecord {
|
|
|
1354
1393
|
review?: boolean;
|
|
1355
1394
|
/** Why this decision was brought forward, when `interrupt()` forced it (e.g. 'percept:injury'). */
|
|
1356
1395
|
interrupt?: string;
|
|
1396
|
+
/** Made with `DecideOptions.reconsider` (2.8.0): the running activity had no inertia bonus. */
|
|
1397
|
+
reconsider?: true;
|
|
1357
1398
|
/**
|
|
1358
1399
|
* One resolution per voice that suggested something in this decision, in voice-id order (N1). `suggestion` is
|
|
1359
1400
|
* the credited voice's resolution (same object), or the only one when a single voice spoke.
|
|
@@ -1402,6 +1443,8 @@ export interface PersonSpec {
|
|
|
1402
1443
|
skillRetention?: boolean;
|
|
1403
1444
|
/** Accumulated exertion (2.6.0): the same as `enableFatigueStore(p)` after creation. */
|
|
1404
1445
|
fatigueStore?: boolean;
|
|
1446
|
+
/** Fullness (2.8.0): the same as `enableFullness(p)` after creation. */
|
|
1447
|
+
fullness?: boolean;
|
|
1405
1448
|
/** Promptings and turning away (2.4.0): the same as `enablePromptings(p, spec)` after creation. */
|
|
1406
1449
|
promptings?: PromptingsSpec;
|
|
1407
1450
|
/** Evening self-review (2.7.0): the same as `enableReview(p, spec)` after creation. */
|
|
@@ -1430,6 +1473,13 @@ export interface PromptingResponse {
|
|
|
1430
1473
|
/** Skill-like practice 0..1 as of `lastUsedAt`; it decays slowly with disuse. */
|
|
1431
1474
|
practice: Unit;
|
|
1432
1475
|
lastUsedAt: Minute;
|
|
1476
|
+
/**
|
|
1477
|
+
* What this response was learned for (2.9.0): an action, an offer tag, or the opposing norm or goal (`n1`,
|
|
1478
|
+
* `norm:n1` or `goal:g1`). A cued response serves only prompts it matches; a response without a cue serves every
|
|
1479
|
+
* prompt that no cued response matches. A norm or goal cue matches only the prompt's strongest opposing term.
|
|
1480
|
+
* Cues longer than 200 characters are ignored.
|
|
1481
|
+
*/
|
|
1482
|
+
cue?: string;
|
|
1433
1483
|
}
|
|
1434
1484
|
export interface PromptingRecord {
|
|
1435
1485
|
at: Minute;
|
|
@@ -1457,6 +1507,8 @@ export interface PromptingsState {
|
|
|
1457
1507
|
}>;
|
|
1458
1508
|
/** Echo gained per prompt under `fault`. Default 0: the recurrence term is a host-set assumption (P3 contested). */
|
|
1459
1509
|
echoRate: number;
|
|
1510
|
+
/** Days over which disused practice halves (2.9.0, host-set); absent = `PROMPTINGS_DEFAULTS.practiceHalfLifeDays`. */
|
|
1511
|
+
practiceHalfLifeDays?: number;
|
|
1460
1512
|
}
|
|
1461
1513
|
/**
|
|
1462
1514
|
* How a person reviews a day (2.7.0, a held reading): `accounting` (muhasaba: name the lapse and resolve for its
|
|
@@ -1503,12 +1555,21 @@ export interface ReviewSpec {
|
|
|
1503
1555
|
export interface PromptingsSpec {
|
|
1504
1556
|
reading?: PromptingReading;
|
|
1505
1557
|
readingSource?: string;
|
|
1558
|
+
/** `cue` (2.9.0): what the response was learned for; see `PromptingResponse.cue`. */
|
|
1506
1559
|
responses?: {
|
|
1507
1560
|
id: string;
|
|
1508
1561
|
kind: PromptingResponseKind;
|
|
1509
1562
|
practice?: Unit;
|
|
1563
|
+
cue?: string;
|
|
1510
1564
|
}[];
|
|
1511
1565
|
echoRate?: number;
|
|
1566
|
+
/**
|
|
1567
|
+
* Days over which disused practice halves (2.9.0), for hosts whose time runs short (a one-week game). Applies to
|
|
1568
|
+
* every response of the person, including disuse since a response's last use. Absent = 60 days, as before.
|
|
1569
|
+
* Read on every `enablePromptings` call: re-enabling without it returns the person to the default. Values outside
|
|
1570
|
+
* (0, 36500] are ignored.
|
|
1571
|
+
*/
|
|
1572
|
+
practiceHalfLifeDays?: number;
|
|
1512
1573
|
}
|
|
1513
1574
|
/** What the body is doing over an interval. */
|
|
1514
1575
|
export interface BodyLoad {
|
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.9.0';
|
package/dist/will/will.d.ts
CHANGED
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
* when the answer leaves the person doing what they are doing. A host command is a separate input that yields the
|
|
29
29
|
* `commanded` verdict (see the commanded-control SCOPE on `commandOutcome`); `learnFromVoice` learns from assented,
|
|
30
30
|
* complied and commanded activities, and `adoptVoiceTrust` moves trust toward a target without a suggestion.
|
|
31
|
+
* 2.8.0: `seedVoice` sets an authored starting trust after creation; `noteVoicePromise` learns from whether the
|
|
32
|
+
* voice kept its own promise; a decision with `reconsider` (WillContext) gives the running activity no inertia
|
|
33
|
+
* bonus, for a review the host forces because something changed (promise inertia, a cognition term, stays).
|
|
31
34
|
*/
|
|
32
35
|
import type { Affordance, BodyReadout, Command, Considered, EntityId, Minute, Percept, Person, RefusalKind, Signed, StandingAdvice, Suggestion, SuggestionResolution, Unit, VoiceConflict, VoiceRelation, WillState } from '../types.ts';
|
|
33
36
|
export declare const WILL_DEFAULTS: {
|
|
@@ -108,6 +111,14 @@ export declare const WILL_DEFAULTS: {
|
|
|
108
111
|
maxCredited: number;
|
|
109
112
|
/** Share of a tie's inertia bonus that remains when the activity is about to end. */
|
|
110
113
|
inertiaFloor: number;
|
|
114
|
+
/**
|
|
115
|
+
* A voice's own promise (2.8.0, `noteVoicePromise`): a kept promise moves trust this share of the way toward 1, a
|
|
116
|
+
* broken one this share toward 0. Broken weighs more than kept (trust asymmetry, unaudited citations in research/empirical-models.md §6b: Slovic 1993; broken promises
|
|
117
|
+
* damage trust and kept ones restore it only gradually, Schweitzer, Hershey and Bradlow 2006: research/
|
|
118
|
+
* empirical-models.md §6b). Shape only; the numbers are engineering defaults equal to `trustGain`/`trustLoss`.
|
|
119
|
+
*/
|
|
120
|
+
promiseKeptGain: number;
|
|
121
|
+
promiseBrokenLoss: number;
|
|
111
122
|
/**
|
|
112
123
|
* Insisting cannot override survival: at this desperation (same scale as conscience's necessity threshold),
|
|
113
124
|
* or when the person's own choice is driven by a bodily need at `survivalNeedUrgency`, the verdict is
|
|
@@ -151,6 +162,8 @@ export interface WillContext {
|
|
|
151
162
|
}[];
|
|
152
163
|
/** A host command in force (1.6.0); see the command SCOPE on `commandOutcome`. */
|
|
153
164
|
command?: Command;
|
|
165
|
+
/** No inertia bonus for the running activity in this decision (2.8.0, `DecideOptions.reconsider`). */
|
|
166
|
+
reconsider?: boolean;
|
|
154
167
|
}
|
|
155
168
|
/**
|
|
156
169
|
* How a command fared in one decision. `holds`: the commanded option was chosen. Otherwise `ends` says whether
|
|
@@ -194,6 +207,32 @@ export declare function voiceOf(p: Person, voiceId: EntityId): VoiceRelation | u
|
|
|
194
207
|
* records no history event and no credit, since no suggestion of that voice was weighed.
|
|
195
208
|
*/
|
|
196
209
|
export declare function adoptVoiceTrust(p: Person, voiceId: EntityId, target: Unit, rate: Unit): VoiceRelation;
|
|
210
|
+
/**
|
|
211
|
+
* Give `p` an authored starting trust in a voice after creation (2.8.0), marked `seeded` as `PersonSpec.voices`
|
|
212
|
+
* would (a seeded voice is never evicted to make room). A no-op returning the existing relation when the voice is
|
|
213
|
+
* already known, unless `replace` is set; replacing keeps counters and history and marks the voice seeded. Records
|
|
214
|
+
* no history event: no suggestion was weighed. A non-finite trust falls back to `defaultVoiceTrust`.
|
|
215
|
+
*/
|
|
216
|
+
export declare function seedVoice(p: Person, voiceId: EntityId, trust: Unit, opts?: {
|
|
217
|
+
replace?: boolean;
|
|
218
|
+
}): VoiceRelation;
|
|
219
|
+
/**
|
|
220
|
+
* SCOPE (a voice's own promise, 2.8.0): the voice said it would do something ("I will carry food up by 18:00") and
|
|
221
|
+
* the host reports whether it did. This is evidence about the voice's word, separate from how following its advice
|
|
222
|
+
* felt (`learnFromVoice`), so it needs no suggestion resolution and works for a request that was only deferred.
|
|
223
|
+
* Kept moves trust `promiseKeptGain` of the way toward 1, divided by 1 + n where n is the decayed count of kept
|
|
224
|
+
* promises already credited for the same action (key `promise:<action>` in `credited`), so a string of trivial
|
|
225
|
+
* promises cannot farm trust (these keys share `credited` and its `maxCredited` bound with advice credits, so a
|
|
226
|
+
* host with many promise actions can evict older entries of either kind, which only shortens a discount);
|
|
227
|
+
* broken moves it `promiseBrokenLoss` toward 0, undiscounted. History reasons are
|
|
228
|
+
* `promise-kept` and `promise-broken`. The voice is added if absent (bounded like any new voice). Does not model
|
|
229
|
+
* excuses, partial keeping, apology, or the size of what was promised; the host judges kept or broken.
|
|
230
|
+
*/
|
|
231
|
+
export declare function noteVoicePromise(p: Person, voiceId: EntityId, event: {
|
|
232
|
+
kept: boolean;
|
|
233
|
+
at?: Minute;
|
|
234
|
+
action?: string;
|
|
235
|
+
}): VoiceRelation;
|
|
197
236
|
/**
|
|
198
237
|
* The next minute after `now` at which a pending commitment reaches `wakeCommitmentPressure`, the level at which a
|
|
199
238
|
* sleeper may wake to serve it (the asleep veto lifts). The composite caps a sleeper's review there so a duty whose
|
package/dist/will/will.js
CHANGED
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
* when the answer leaves the person doing what they are doing. A host command is a separate input that yields the
|
|
29
29
|
* `commanded` verdict (see the commanded-control SCOPE on `commandOutcome`); `learnFromVoice` learns from assented,
|
|
30
30
|
* complied and commanded activities, and `adoptVoiceTrust` moves trust toward a target without a suggestion.
|
|
31
|
+
* 2.8.0: `seedVoice` sets an authored starting trust after creation; `noteVoicePromise` learns from whether the
|
|
32
|
+
* voice kept its own promise; a decision with `reconsider` (WillContext) gives the running activity no inertia
|
|
33
|
+
* bonus, for a review the host forces because something changed (promise inertia, a cognition term, stays).
|
|
31
34
|
*/
|
|
32
35
|
import { breakAllows, inBreak } from '../affect/index.js';
|
|
33
36
|
import { commitmentPressure, isUnderWay, pressureReachedAt } from '../agenda/index.js';
|
|
@@ -114,6 +117,14 @@ export const WILL_DEFAULTS = {
|
|
|
114
117
|
maxCredited: 8,
|
|
115
118
|
/** Share of a tie's inertia bonus that remains when the activity is about to end. */
|
|
116
119
|
inertiaFloor: 0.5,
|
|
120
|
+
/**
|
|
121
|
+
* A voice's own promise (2.8.0, `noteVoicePromise`): a kept promise moves trust this share of the way toward 1, a
|
|
122
|
+
* broken one this share toward 0. Broken weighs more than kept (trust asymmetry, unaudited citations in research/empirical-models.md §6b: Slovic 1993; broken promises
|
|
123
|
+
* damage trust and kept ones restore it only gradually, Schweitzer, Hershey and Bradlow 2006: research/
|
|
124
|
+
* empirical-models.md §6b). Shape only; the numbers are engineering defaults equal to `trustGain`/`trustLoss`.
|
|
125
|
+
*/
|
|
126
|
+
promiseKeptGain: 0.1,
|
|
127
|
+
promiseBrokenLoss: 0.25,
|
|
117
128
|
/**
|
|
118
129
|
* Insisting cannot override survival: at this desperation (same scale as conscience's necessity threshold),
|
|
119
130
|
* or when the person's own choice is driven by a bodily need at `survivalNeedUrgency`, the verdict is
|
|
@@ -224,6 +235,61 @@ export function adoptVoiceTrust(p, voiceId, target, rate) {
|
|
|
224
235
|
v.trust = clamp01(v.trust + r * (t - v.trust));
|
|
225
236
|
return v;
|
|
226
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Give `p` an authored starting trust in a voice after creation (2.8.0), marked `seeded` as `PersonSpec.voices`
|
|
240
|
+
* would (a seeded voice is never evicted to make room). A no-op returning the existing relation when the voice is
|
|
241
|
+
* already known, unless `replace` is set; replacing keeps counters and history and marks the voice seeded. Records
|
|
242
|
+
* no history event: no suggestion was weighed. A non-finite trust falls back to `defaultVoiceTrust`.
|
|
243
|
+
*/
|
|
244
|
+
export function seedVoice(p, voiceId, trust, opts = {}) {
|
|
245
|
+
const existing = voiceOf(p, voiceId);
|
|
246
|
+
if (existing && !opts.replace)
|
|
247
|
+
return existing;
|
|
248
|
+
const v = existing ?? ensureVoice(p, voiceId);
|
|
249
|
+
v.trust = clamp01(Number.isFinite(trust) ? trust : WILL_DEFAULTS.defaultVoiceTrust);
|
|
250
|
+
v.seeded = true;
|
|
251
|
+
return v;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* SCOPE (a voice's own promise, 2.8.0): the voice said it would do something ("I will carry food up by 18:00") and
|
|
255
|
+
* the host reports whether it did. This is evidence about the voice's word, separate from how following its advice
|
|
256
|
+
* felt (`learnFromVoice`), so it needs no suggestion resolution and works for a request that was only deferred.
|
|
257
|
+
* Kept moves trust `promiseKeptGain` of the way toward 1, divided by 1 + n where n is the decayed count of kept
|
|
258
|
+
* promises already credited for the same action (key `promise:<action>` in `credited`), so a string of trivial
|
|
259
|
+
* promises cannot farm trust (these keys share `credited` and its `maxCredited` bound with advice credits, so a
|
|
260
|
+
* host with many promise actions can evict older entries of either kind, which only shortens a discount);
|
|
261
|
+
* broken moves it `promiseBrokenLoss` toward 0, undiscounted. History reasons are
|
|
262
|
+
* `promise-kept` and `promise-broken`. The voice is added if absent (bounded like any new voice). Does not model
|
|
263
|
+
* excuses, partial keeping, apology, or the size of what was promised; the host judges kept or broken.
|
|
264
|
+
*/
|
|
265
|
+
export function noteVoicePromise(p, voiceId, event) {
|
|
266
|
+
const W = WILL_DEFAULTS;
|
|
267
|
+
const v = ensureVoice(p, voiceId);
|
|
268
|
+
const at = event.at !== undefined && Number.isFinite(event.at) ? event.at : p.now;
|
|
269
|
+
const before = v.trust;
|
|
270
|
+
if (event.kept) {
|
|
271
|
+
const key = `promise:${event.action ?? ''}`;
|
|
272
|
+
v.credited ??= [];
|
|
273
|
+
const list = v.credited;
|
|
274
|
+
const prior = list.find((c) => c.action === key);
|
|
275
|
+
const n = prior ? decay(prior.n, Math.max(0, at - prior.at), W.creditHalfLife) : 0;
|
|
276
|
+
v.trust = clamp01(v.trust + (W.promiseKeptGain * (1 - v.trust)) / (1 + n));
|
|
277
|
+
if (prior) {
|
|
278
|
+
prior.n = n + 1;
|
|
279
|
+
prior.at = at;
|
|
280
|
+
}
|
|
281
|
+
else
|
|
282
|
+
list.push({ action: key, n: 1, at });
|
|
283
|
+
if (list.length > W.maxCredited) {
|
|
284
|
+
list.sort((a, b) => b.at - a.at);
|
|
285
|
+
list.length = W.maxCredited;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else
|
|
289
|
+
v.trust = clamp01(v.trust - W.promiseBrokenLoss * v.trust);
|
|
290
|
+
noteTrust(v, v.trust - before, event.kept ? 'promise-kept' : 'promise-broken', at, event.action);
|
|
291
|
+
return v;
|
|
292
|
+
}
|
|
227
293
|
const finite = (x) => x === undefined || Number.isFinite(x);
|
|
228
294
|
/** Host numbers the framework computes with must be finite; a NaN offer is vetoed, never chosen. */
|
|
229
295
|
function wellFormed(aff) {
|
|
@@ -547,7 +613,7 @@ function evaluate(p, input, ctx, given, draw) {
|
|
|
547
613
|
const live = considered.filter((c) => !c.vetoed);
|
|
548
614
|
const current = p.activity;
|
|
549
615
|
const score = (c) => {
|
|
550
|
-
if (!current || c.affordanceId !== current.affordanceId)
|
|
616
|
+
if (!current || c.affordanceId !== current.affordanceId || ctx.reconsider)
|
|
551
617
|
return safeUtility(c.utility);
|
|
552
618
|
const span = Math.max(1, current.endsAt - current.startedAt);
|
|
553
619
|
const remaining = clamp01((current.endsAt - ctx.now) / span);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.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",
|