@adam0white/human-framework 2.12.0 → 2.13.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.js +4 -1
- package/dist/conscience/conscience.js +10 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/migrate.js +6 -1
- package/dist/person.js +3 -1
- package/dist/types.d.ts +13 -1
- package/dist/types.js +1 -1
- package/dist/will/will.d.ts +7 -0
- package/dist/will/will.js +47 -1
- package/package.json +1 -1
package/dist/agenda/agenda.js
CHANGED
|
@@ -154,8 +154,11 @@ function sanitizeGoalTemporal(g) {
|
|
|
154
154
|
else if (g.temporal)
|
|
155
155
|
g.temporal = { halfLifeMinutes: g.temporal.halfLifeMinutes };
|
|
156
156
|
}
|
|
157
|
-
/** Restore-time check of
|
|
157
|
+
/** Restore-time check of optional lapse and firm flags; malformed values are dropped. @internal */
|
|
158
158
|
export function sanitizeAgenda(a) {
|
|
159
|
+
for (const c of a.commitments)
|
|
160
|
+
if (isObj(c) && (c.firm !== true || c.kind !== 'abstain'))
|
|
161
|
+
delete c.firm;
|
|
159
162
|
for (const g of a.goals)
|
|
160
163
|
if (isObj(g))
|
|
161
164
|
sanitizeGoalTemporal(g);
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* desperation threshold, and only for acts meeting a bodily need; this is not the fast's own exemption for
|
|
23
23
|
* illness and travel (2:184-185, applied in the agenda) and not the 2:173 necessity exception for forbidden
|
|
24
24
|
* food. Below the threshold the abstention acts only as a negative term (agenda), so a weakly convinced person
|
|
25
|
-
* can be tempted.
|
|
25
|
+
* can be tempted. An opt-in `firm: true` self abstention without a linked obligatory norm instead vetoes
|
|
26
|
+
* as `rule:<label>` (id fallback). It uses the same open-window, exemption and bodily survival checks,
|
|
27
|
+
* including for self choice and insistence. It models a held rule, not a new religious ruling.
|
|
28
|
+
* It does not model the person's later judgment of a broken fast beyond the recorded breach.
|
|
26
29
|
*/
|
|
27
30
|
import { clamp01 } from '../core/index.js';
|
|
28
31
|
import { DEFAULT_NORMS, NORM_SCOPE } from './catalog.js';
|
|
@@ -251,11 +254,15 @@ export function abstentionVeto(p, aff, now, capacityLifted = false) {
|
|
|
251
254
|
return undefined;
|
|
252
255
|
const end = now + Math.max(0, aff.duration);
|
|
253
256
|
for (const c of p.agenda?.commitments ?? []) {
|
|
254
|
-
if (c.kind !== 'abstain' || c.status !== 'pending' || c.exempt !== undefined
|
|
257
|
+
if (c.kind !== 'abstain' || c.status !== 'pending' || c.exempt !== undefined)
|
|
255
258
|
continue;
|
|
256
259
|
if (!(c.violatedBy?.includes(aff.action) ?? false) || now >= c.until || end <= c.from)
|
|
257
260
|
continue;
|
|
258
|
-
const held = heldNorm(p, c.normId);
|
|
261
|
+
const held = c.normId === undefined ? undefined : heldNorm(p, c.normId);
|
|
262
|
+
if (c.firm === true &&
|
|
263
|
+
(c.toId === undefined || c.toId === 'self' || c.toId === p.id) &&
|
|
264
|
+
held?.standing !== 'obligatory')
|
|
265
|
+
return { kind: 'willNot', reason: `rule:${c.label ?? c.id}` };
|
|
259
266
|
if (held?.standing !== 'obligatory' || held.conviction < CONSCIENCE_DEFAULTS.abstainVetoConviction)
|
|
260
267
|
continue;
|
|
261
268
|
return { kind: 'willNot', reason: `duty:${c.normId}` };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework public surface. */
|
|
2
|
-
export declare const FRAMEWORK_VERSION = "2.
|
|
2
|
+
export declare const FRAMEWORK_VERSION = "2.13.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';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework public surface. */
|
|
2
|
-
export const FRAMEWORK_VERSION = '2.
|
|
2
|
+
export const FRAMEWORK_VERSION = '2.13.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';
|
package/dist/migrate.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* version; fullness stays absent unless enabled, and alongside partners and reconsideration are opt-in.
|
|
19
19
|
* 2.8.0 to 2.9.0 stamps the version; prompting cues and practice half-lives stay absent unless supplied,
|
|
20
20
|
* review terms may carry refs, and an outcome naming several commitments now keeps each rather than only
|
|
21
|
-
* the earliest-ending one. 2.11.0 to 2.12.0 stamps the version (planning starts absent). 2.10.0 to 2.11.0 stamps the version (curiosity starts absent). 2.9.0 to 2.10.0 stamps the version; belief provenance, noted contradictions and
|
|
21
|
+
* the earliest-ending one. 2.12.0 to 2.13.0 stamps the version (suggestion reasons and firm rules start absent). 2.11.0 to 2.12.0 stamps the version (planning starts absent). 2.10.0 to 2.11.0 stamps the version (curiosity starts absent). 2.9.0 to 2.10.0 stamps the version; belief provenance, noted contradictions and
|
|
22
22
|
* believed regard stay absent unless a host supplies them;
|
|
23
23
|
* `regard` becomes a reserved impression cue key. A migrated save continues under the current engine's rules: it restores and runs,
|
|
24
24
|
* but it does not reproduce what the old engine would have done next where the rules changed (1.4.0 to 1.5.0
|
|
@@ -70,6 +70,11 @@ export const MIGRATIONS = {
|
|
|
70
70
|
note: 'the surroundings field `ambient.now` is renamed `ambient.percept`; per-person retention is optional and starts absent; behaviour unchanged',
|
|
71
71
|
apply: renameAmbient,
|
|
72
72
|
},
|
|
73
|
+
'2.12.0': {
|
|
74
|
+
to: '2.13.0',
|
|
75
|
+
note: 'suggestion reasons (Person.will.suggestionReasons) and firm self-rules (Commitment.firm) are optional and start absent; existing hosts retain exact behavior',
|
|
76
|
+
apply: stamp,
|
|
77
|
+
},
|
|
73
78
|
'2.11.0': {
|
|
74
79
|
to: '2.12.0',
|
|
75
80
|
note: 'the planning slice (Person.planning) is optional and starts absent; retained consequences may carry channel known only when seeded; existing hosts retain exact behavior',
|
package/dist/person.js
CHANGED
|
@@ -43,7 +43,7 @@ import { enableReview, reviewIfDue } from './review/index.js';
|
|
|
43
43
|
import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
|
|
44
44
|
import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, noteKnownBy, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
|
|
45
45
|
import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA, PHYSIOLOGICAL_NEEDS, PSYCHOLOGICAL_NEEDS, TRAIT_KEYS, VALUE_KEYS, } from './types.js';
|
|
46
|
-
import { advanceWill, answerSuggestion, chargeCommand, commandTargets, createWill, creditedVoices, dischargeAdvice, dutyReviewAt, endCommand, learnFromVoice, noteCommandOutcome, predictResponse, rememberAdvice, resolveCommand, takeCommand, voicesIn, WILL_DEFAULTS, wakeReviewAt, } from './will/index.js';
|
|
46
|
+
import { advanceWill, answerSuggestion, chargeCommand, commandTargets, createWill, creditedVoices, dischargeAdvice, dutyReviewAt, enableSuggestionReasons, endCommand, learnFromVoice, noteCommandOutcome, predictResponse, rememberAdvice, resolveCommand, takeCommand, voicesIn, WILL_DEFAULTS, wakeReviewAt, } from './will/index.js';
|
|
47
47
|
export const PERSON_DEFAULTS = {
|
|
48
48
|
/** Body load while idle and awake. */
|
|
49
49
|
idleLoad: { effort: 0.05, focus: 0.05, mode: 'awake' },
|
|
@@ -148,6 +148,8 @@ export function createPerson(spec) {
|
|
|
148
148
|
if (family)
|
|
149
149
|
p.family = family;
|
|
150
150
|
const on = spec.enable;
|
|
151
|
+
if (on?.suggestionReasons)
|
|
152
|
+
enableSuggestionReasons(p);
|
|
151
153
|
if (on?.gists)
|
|
152
154
|
enableGists(p);
|
|
153
155
|
if (on?.yearbook)
|
package/dist/types.d.ts
CHANGED
|
@@ -563,6 +563,8 @@ export interface Commitment {
|
|
|
563
563
|
label?: string;
|
|
564
564
|
/** For kind 'abstain': action ids whose completion inside the window breaks it. */
|
|
565
565
|
violatedBy?: string[];
|
|
566
|
+
/** Firm self abstention: a will-not, lifted for bodily survival like a norm abstention. */
|
|
567
|
+
firm?: true;
|
|
566
568
|
/** For kind 'abstain': catalogued conditions under which the person may not hold it that day. */
|
|
567
569
|
exemptWhen?: ('illness' | 'travel')[];
|
|
568
570
|
/**
|
|
@@ -700,6 +702,8 @@ export interface VoiceTrustEvent {
|
|
|
700
702
|
* impulse emerges from competing terms, real fatigue costs, habits and precommitments.
|
|
701
703
|
*/
|
|
702
704
|
export interface WillState {
|
|
705
|
+
/** Opt-in score explanations on suggestion resolutions; absent preserves legacy JSON. */
|
|
706
|
+
suggestionReasons?: true;
|
|
703
707
|
voices: VoiceRelation[];
|
|
704
708
|
/**
|
|
705
709
|
* Precommitments the person made for themselves (e.g. "no work after Isha", "walk instead of eat when bored"):
|
|
@@ -885,7 +889,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
|
|
|
885
889
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
886
890
|
* `dutyReviewAt`). Person shape unchanged.
|
|
887
891
|
*/
|
|
888
|
-
export declare const ENGINE_VERSION = "2.
|
|
892
|
+
export declare const ENGINE_VERSION = "2.13.0";
|
|
889
893
|
/** A pregnancy carried by this person (host-driven; see `family/`). */
|
|
890
894
|
export interface Pregnancy {
|
|
891
895
|
fatherId: PersonId;
|
|
@@ -1374,6 +1378,12 @@ export type SuggestionVerdict = 'assented' | 'complied' | 'deferred' | 'modified
|
|
|
1374
1378
|
/** Refusal class for UI: grey = cannot, amber = notNow, red = willNot. */
|
|
1375
1379
|
export type RefusalKind = 'cannot' | 'notNow' | 'willNot';
|
|
1376
1380
|
export interface SuggestionResolution {
|
|
1381
|
+
/** Read-only score explanation, only with `enableSuggestionReasons`; margin includes activity inertia. */
|
|
1382
|
+
decidedBy?: {
|
|
1383
|
+
kind: 'inertia' | 'term';
|
|
1384
|
+
source?: string;
|
|
1385
|
+
margin: number;
|
|
1386
|
+
};
|
|
1377
1387
|
voiceId: EntityId;
|
|
1378
1388
|
verdict: SuggestionVerdict;
|
|
1379
1389
|
kind?: RefusalKind;
|
|
@@ -1501,6 +1511,8 @@ export interface PersonSpec {
|
|
|
1501
1511
|
* `enableCharacterChange` and `enableSkillRetention` right after `createPerson`. Each is off when absent.
|
|
1502
1512
|
*/
|
|
1503
1513
|
enable?: {
|
|
1514
|
+
/** Read-only suggestion score explanations, as with `enableSuggestionReasons(p)`. */
|
|
1515
|
+
suggestionReasons?: boolean;
|
|
1504
1516
|
gists?: boolean;
|
|
1505
1517
|
yearbook?: boolean;
|
|
1506
1518
|
character?: boolean;
|
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.13.0';
|
package/dist/will/will.d.ts
CHANGED
|
@@ -266,6 +266,13 @@ export declare function dominantTerm(c: Considered | undefined): string;
|
|
|
266
266
|
* sorted by voice id and bounded by `maxVoices`, so the outcome never depends on the host's argument order.
|
|
267
267
|
*/
|
|
268
268
|
export declare function voicesIn(single: Suggestion | undefined, list: readonly Suggestion[] | undefined): Suggestion[];
|
|
269
|
+
/**
|
|
270
|
+
* SCOPE (suggestion reasons): opt-in read-only explanations of score-based deferrals and modifications.
|
|
271
|
+
* The gap includes running-activity inertia; source differences aggregate like-labelled terms and tie by
|
|
272
|
+
* source string. This does not change selection, voice wording, counters or RNG, explain vetoes, or claim
|
|
273
|
+
* a score caused a lower-scoring stochastic choice. Legacy resolutions have no additional keys.
|
|
274
|
+
*/
|
|
275
|
+
export declare function enableSuggestionReasons(p: Person): void;
|
|
269
276
|
/** Whether a command names offer `aff` (by affordance id, or by action class when it names no affordance). */
|
|
270
277
|
export declare const commandTargets: (cmd: Command, aff: Affordance) => boolean;
|
|
271
278
|
/**
|
package/dist/will/will.js
CHANGED
|
@@ -36,7 +36,7 @@ import { breakAllows, inBreak } from '../affect/index.js';
|
|
|
36
36
|
import { commitmentPressure, isUnderWay, pressureReachedAt } from '../agenda/index.js';
|
|
37
37
|
import { downedAllows, readCapacities } from '../body/index.js';
|
|
38
38
|
import { CONSCIENCE_DEFAULTS, normVeto } from '../conscience/index.js';
|
|
39
|
-
import { clamp01, cmpStr, decay, dexp, dpow, hasNumbers, isObj, random } from '../core/index.js';
|
|
39
|
+
import { clamp01, cmpStr, decay, dexp, dpow, hasNumbers, isObj, random, round } from '../core/index.js';
|
|
40
40
|
import { skillLevel } from '../skills/index.js';
|
|
41
41
|
import { MINUTES_PER_DAY, MINUTES_PER_HOUR } from '../types.js';
|
|
42
42
|
export const WILL_DEFAULTS = {
|
|
@@ -160,6 +160,8 @@ export const WILL_DEFAULTS = {
|
|
|
160
160
|
* or `lastCommand` (1.6.0, 1.7.0) are dropped (absent means none). A save the engine wrote is unchanged. @internal
|
|
161
161
|
*/
|
|
162
162
|
export function sanitizeWill(w) {
|
|
163
|
+
if (w.suggestionReasons !== true)
|
|
164
|
+
delete w.suggestionReasons;
|
|
163
165
|
if (w.advice !== undefined && !Array.isArray(w.advice))
|
|
164
166
|
delete w.advice;
|
|
165
167
|
const cmd = w.command;
|
|
@@ -721,6 +723,43 @@ function arbitrateCommand(p, ctx, cmd, suggestions, prepared, selection, ev) {
|
|
|
721
723
|
}
|
|
722
724
|
return false;
|
|
723
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* SCOPE (suggestion reasons): opt-in read-only explanations of score-based deferrals and modifications.
|
|
728
|
+
* The gap includes running-activity inertia; source differences aggregate like-labelled terms and tie by
|
|
729
|
+
* source string. This does not change selection, voice wording, counters or RNG, explain vetoes, or claim
|
|
730
|
+
* a score caused a lower-scoring stochastic choice. Legacy resolutions have no additional keys.
|
|
731
|
+
*/
|
|
732
|
+
export function enableSuggestionReasons(p) {
|
|
733
|
+
p.will.suggestionReasons = true;
|
|
734
|
+
}
|
|
735
|
+
function scoreReason(selection, live, targetIds) {
|
|
736
|
+
const { winner, ranked, score } = selection;
|
|
737
|
+
if (!winner)
|
|
738
|
+
return undefined;
|
|
739
|
+
// A stochastic draw below the best live score was not decided by the scores.
|
|
740
|
+
if (ranked.some((c) => !c.vetoed && score(c) > score(winner)))
|
|
741
|
+
return undefined;
|
|
742
|
+
// Compare against the closest suggested option by the same adjusted score the winner was chosen on.
|
|
743
|
+
const suggested = [...live].sort((a, b) => score(b) - score(a) || cmpStr(a.affordanceId, b.affordanceId))[0];
|
|
744
|
+
if (!suggested)
|
|
745
|
+
return undefined;
|
|
746
|
+
const gap = score(winner) - score(suggested);
|
|
747
|
+
if (!Number.isFinite(gap) || gap < 0)
|
|
748
|
+
return undefined;
|
|
749
|
+
const margin = round(gap);
|
|
750
|
+
const withoutInertia = [...ranked].sort(byUtilityThenId)[0];
|
|
751
|
+
if (score(winner) > winner.utility && withoutInertia && targetIds.has(withoutInertia.affordanceId))
|
|
752
|
+
return { kind: 'inertia', margin };
|
|
753
|
+
const differences = new Map();
|
|
754
|
+
for (const t of winner.terms)
|
|
755
|
+
differences.set(t.source, (differences.get(t.source) ?? 0) + t.value);
|
|
756
|
+
for (const t of suggested.terms)
|
|
757
|
+
differences.set(t.source, (differences.get(t.source) ?? 0) - t.value);
|
|
758
|
+
const term = [...differences]
|
|
759
|
+
.filter(([, value]) => value > 0)
|
|
760
|
+
.sort((a, b) => b[1] - a[1] || cmpStr(a[0], b[0]))[0];
|
|
761
|
+
return { kind: 'term', ...(term ? { source: term[0] } : {}), margin };
|
|
762
|
+
}
|
|
724
763
|
/** Judge each voice against the same selected option before resolving competing insistence. */
|
|
725
764
|
function judgeVoices(p, ctx, suggestions, prepared, selection) {
|
|
726
765
|
const W = WILL_DEFAULTS;
|
|
@@ -829,17 +868,21 @@ function judgeVoices(p, ctx, suggestions, prepared, selection) {
|
|
|
829
868
|
}
|
|
830
869
|
side.pressure = W.pressurePush * clamp01(s.strength);
|
|
831
870
|
side.refused = 1;
|
|
871
|
+
const decidedBy = p.will.suggestionReasons ? scoreReason(selection, liveSuggested, targetIds) : undefined;
|
|
872
|
+
const explanation = decidedBy ? { decidedBy } : {};
|
|
832
873
|
const near = winnerAff !== undefined &&
|
|
833
874
|
suggestedAff !== undefined &&
|
|
834
875
|
(winnerAff.action === suggestedAff.action ||
|
|
835
876
|
(mainAim(suggestedAff) !== undefined && mainAim(suggestedAff) === mainAim(winnerAff)));
|
|
836
877
|
if (near && winnerAff)
|
|
837
878
|
return out(resolution('modified', reason, {
|
|
879
|
+
...explanation,
|
|
838
880
|
kind: 'notNow',
|
|
839
881
|
insteadAffordanceId: winnerAff.id,
|
|
840
882
|
counterOffer: { affordanceId: winnerAff.id, label: winnerAff.label },
|
|
841
883
|
}));
|
|
842
884
|
return out(resolution('deferred', reason, {
|
|
885
|
+
...explanation,
|
|
843
886
|
kind: 'notNow',
|
|
844
887
|
insteadAffordanceId: winnerAff?.id,
|
|
845
888
|
counterOffer: winnerAff
|
|
@@ -865,6 +908,9 @@ function arbitrateInsistence(p, judged, considered, affById, ev) {
|
|
|
865
908
|
forcer = j;
|
|
866
909
|
}
|
|
867
910
|
if (forcer?.comply) {
|
|
911
|
+
// The final option is imposed by insistence, not selected by scores.
|
|
912
|
+
for (const j of judged)
|
|
913
|
+
delete j.res.decidedBy;
|
|
868
914
|
const forced = forcer.comply.target.affordanceId;
|
|
869
915
|
const forcedAff = affById.get(forced);
|
|
870
916
|
ev.chosenId = forced;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.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",
|