@adam0white/human-framework 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/agenda/agenda.d.ts +2 -0
- package/dist/agenda/agenda.js +7 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/persona-calendar.d.ts +39 -0
- package/dist/persona-calendar.js +411 -0
- package/dist/persona.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -440,3 +440,22 @@ commitment history prunes it. It removes only records matched to an existing, cl
|
|
|
440
440
|
recursively frozen, detached `MakeUpReceipt` objects. Persist those receipts in the source replay. Pending,
|
|
441
441
|
broken, released, exempt and missing matches remain unresolved. The normal actor loop never calls retirement;
|
|
442
442
|
existing games and saves keep their previous behavior until a host opts in.
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
### Calendar passports (2.16.0, opt-in)
|
|
446
|
+
|
|
447
|
+
Use `projectCalendarPersona(source, { version, skills, episodes, calendar })` when declared obligations must
|
|
448
|
+
travel. `calendar` has version `hf-persona-calendar-map/1`, one exact clock offset, explicit injective maps
|
|
449
|
+
for commitment, chain, action, entity and norm references, and the source-backed norm catalog.
|
|
450
|
+
The result is `hf-persona-calendar/1`, wrapping the original approved Person projection and a separate
|
|
451
|
+
calendar. It preserves continuing commitments, every owed record, actual retained receipts, held norm
|
|
452
|
+
understandings and lapse state. A missing scheduled receipt remains unresolved and is disclosed; it never
|
|
453
|
+
becomes a paid duty. `isContinuingCommitment(agenda, commitment)` lets a host construct the required maps
|
|
454
|
+
using the owning agenda's existing recurrence semantics.
|
|
455
|
+
|
|
456
|
+
`validateCalendarPersona` strictly validates portable data. `admitCalendarPersona(payload, opening,
|
|
457
|
+
{ mappingVersion, sourceId, targetId, targetNorms })` checks the offset and exact catalog semantics before
|
|
458
|
+
returning a detached Person. The receiving calendar must be empty; authored role goals may remain. After
|
|
459
|
+
admission, the host uses ordinary agenda APIs and its declared calendar. No duty is waived, no new ruling
|
|
460
|
+
is invented, and no obligation is converted to a score. Activities, projects and incompatible identity or
|
|
461
|
+
health remain unsupported. The original passport format and no-call continuation remain unchanged.
|
package/dist/agenda/agenda.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export declare function createAgenda(spec: {
|
|
|
56
56
|
export declare function promise(p: Person, c: Omit<Commitment, 'id' | 'status'>): Commitment;
|
|
57
57
|
/** Release a pending commitment (the other party let it go). Released commitments do not recur. */
|
|
58
58
|
export declare function release(p: Person, id: string): boolean;
|
|
59
|
+
/** Whether an instance still owns a pending duty or a recurrence that can produce its next window. Read-only. */
|
|
60
|
+
export declare const isContinuingCommitment: (state: AgendaState, c: Commitment) => boolean;
|
|
59
61
|
/**
|
|
60
62
|
* Whether an activity that would keep `c` is under way at `now`: begun no later than the window's end
|
|
61
63
|
* (`spanMeetsWindow`), not yet ended, not sleep, and serving `c` (listed in `fulfills` or matching its action and
|
package/dist/agenda/agenda.js
CHANGED
|
@@ -246,6 +246,13 @@ const sameShape = (a, b) => a.kind === b.kind &&
|
|
|
246
246
|
const hasSuccessor = (state, c) => state.commitments.some((x) => x !== c && x.from > c.from && sameChain(x, c));
|
|
247
247
|
/** Closed instances that carry their chain on: kept, broken, or released by an exemption (not by the other party). */
|
|
248
248
|
const chainLink = (c) => c.status === 'kept' || c.status === 'broken' || (c.status === 'released' && c.exempt !== undefined);
|
|
249
|
+
/** Whether an instance still owns a pending duty or a recurrence that can produce its next window. Read-only. */
|
|
250
|
+
export const isContinuingCommitment = (state, c) => c.status === 'pending' ||
|
|
251
|
+
(c.recurEvery !== undefined &&
|
|
252
|
+
c.recurEvery > 0 &&
|
|
253
|
+
chainLink(c) &&
|
|
254
|
+
(c.recurUntil === undefined || c.from + c.recurEvery <= c.recurUntil) &&
|
|
255
|
+
!hasSuccessor(state, c));
|
|
249
256
|
const needsSpawn = (state, c, now) => c.recurEvery !== undefined &&
|
|
250
257
|
c.recurEvery > 0 &&
|
|
251
258
|
chainLink(c) &&
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Human Framework public surface. */
|
|
2
|
-
export declare const FRAMEWORK_VERSION = "2.
|
|
2
|
+
export declare const FRAMEWORK_VERSION = "2.16.0";
|
|
3
3
|
export type { CrisisEvent, PracticeKind } from './affect/index.ts';
|
|
4
4
|
export { AFFECT_DEFAULTS, actionTendencies, advanceAffect, appraise, breakAllows, breakBehaviour, breakHazard, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, enableBreaks, feel, inBreak, readAffect, regulate, release as releaseEmotion, skipCrisis, strain, tendencyEmotions, } from './affect/index.ts';
|
|
5
5
|
export type { GoalTemplate, MakeUpReceipt, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
|
|
6
|
-
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, retirePaidMakeUps, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.ts';
|
|
6
|
+
export { AGENDA_DEFAULTS, abandonGoal, adoptGoal, advanceAgenda, agendaTerms, applyExemptions, CARE_DEFAULTS, calendarRetimer, careDuty, commitmentPressure, createAgenda, DEFAULT_PRAYER_TIMES, EID_PRAYER_IMPORTANCE, EXEMPTION_DEFAULTS, eidPrayer, eidWindow, fastWindow, iftarWindow, inMakruhTime, isContinuingCommitment, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, retirePaidMakeUps, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.ts';
|
|
7
7
|
export * from './beliefs/index.ts';
|
|
8
8
|
export * from './body/index.ts';
|
|
9
9
|
export * from './character/index.ts';
|
|
@@ -31,6 +31,7 @@ export * from './partnering/index.ts';
|
|
|
31
31
|
export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistence/index.ts';
|
|
32
32
|
export * from './person.ts';
|
|
33
33
|
export * from './persona.ts';
|
|
34
|
+
export * from './persona-calendar.ts';
|
|
34
35
|
export { considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal } from './projects/index.ts';
|
|
35
36
|
export * from './promptings/index.ts';
|
|
36
37
|
export type { ConsequenceEvidence, PlanRoute, PlanStep } from './prospection/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Human Framework public surface. */
|
|
2
|
-
export const FRAMEWORK_VERSION = '2.
|
|
2
|
+
export const FRAMEWORK_VERSION = '2.16.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
|
-
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, retirePaidMakeUps, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
|
|
5
|
+
export { AGENDA_DEFAULTS, abandonGoal, adoptGoal, advanceAgenda, agendaTerms, applyExemptions, CARE_DEFAULTS, calendarRetimer, careDuty, commitmentPressure, createAgenda, DEFAULT_PRAYER_TIMES, EID_PRAYER_IMPORTANCE, EXEMPTION_DEFAULTS, eidPrayer, eidWindow, fastWindow, iftarWindow, inMakruhTime, isContinuingCommitment, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, retirePaidMakeUps, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
|
|
6
6
|
export * from './beliefs/index.js';
|
|
7
7
|
export * from './body/index.js';
|
|
8
8
|
export * from './character/index.js';
|
|
@@ -28,6 +28,7 @@ export * from './partnering/index.js';
|
|
|
28
28
|
export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistence/index.js';
|
|
29
29
|
export * from './person.js';
|
|
30
30
|
export * from './persona.js';
|
|
31
|
+
export * from './persona-calendar.js';
|
|
31
32
|
export { considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal } from './projects/index.js';
|
|
32
33
|
export * from './promptings/index.js';
|
|
33
34
|
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type PersonaAdmission, type PersonaAdmissionResult, type PersonaProjection, type PersonaProjectionSnapshot } from './persona.ts';
|
|
2
|
+
import { type Commitment, type HeldNorm, type NormDefinition, type OwedMakeUp, type Person } from './types.ts';
|
|
3
|
+
export interface PersonaCalendarMap {
|
|
4
|
+
version: 'hf-persona-calendar-map/1';
|
|
5
|
+
/** Exactly target opening minus source departure. All retained calendar timestamps use this offset. */
|
|
6
|
+
offset: number;
|
|
7
|
+
references: {
|
|
8
|
+
commitments: Record<string, string>;
|
|
9
|
+
chains: Record<string, string>;
|
|
10
|
+
actions: Record<string, string>;
|
|
11
|
+
entities: Record<string, string>;
|
|
12
|
+
norms: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
sourceNorms: NormDefinition[];
|
|
15
|
+
}
|
|
16
|
+
export interface CalendarPersonaProjection extends Omit<PersonaProjection, 'commitments'> {
|
|
17
|
+
calendar: PersonaCalendarMap;
|
|
18
|
+
}
|
|
19
|
+
export interface CalendarPersonaSnapshot {
|
|
20
|
+
format: 'hf-persona-calendar/1';
|
|
21
|
+
person: PersonaProjectionSnapshot;
|
|
22
|
+
calendar: {
|
|
23
|
+
offset: number;
|
|
24
|
+
commitments: Commitment[];
|
|
25
|
+
owed: OwedMakeUp[];
|
|
26
|
+
lapse: Person['agenda']['lapse'] | null;
|
|
27
|
+
heldNorms: HeldNorm[];
|
|
28
|
+
norms: NormDefinition[];
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface CalendarPersonaAdmission extends PersonaAdmission {
|
|
32
|
+
targetNorms: NormDefinition[];
|
|
33
|
+
}
|
|
34
|
+
/** Validate portable calendar data strictly; no restore defaults or debt settlement. */
|
|
35
|
+
export declare function validateCalendarPersona(input: unknown): CalendarPersonaSnapshot;
|
|
36
|
+
/** Source declares a complete semantic ID map, a clock offset and source-backed norm catalog. */
|
|
37
|
+
export declare function projectCalendarPersona(source: Person, mapping: CalendarPersonaProjection): CalendarPersonaSnapshot;
|
|
38
|
+
/** Receiver declares identical norm meanings and a clean calendar at the exact declared opening clock. */
|
|
39
|
+
export declare function admitCalendarPersona(input: unknown, target: Person, policy: CalendarPersonaAdmission): PersonaAdmissionResult;
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE: opt-in calendar transport between declared chapter clocks. Hosts map every retained reference and
|
|
3
|
+
* supply identical source-backed norm meanings. Carries pending/continuing duties, owed records and their
|
|
4
|
+
* receipts, held understanding and lapse timestamps. Does not choose dates, waive debt, simulate travel,
|
|
5
|
+
* create rulings, migrate activities/projects or change the v1 persona path. Neither input is mutated.
|
|
6
|
+
*/
|
|
7
|
+
import { isContinuingCommitment } from './agenda/agenda.js';
|
|
8
|
+
import { createPerson } from './person.js';
|
|
9
|
+
import { admitPersona, projectPersona, validatePersonaProjection, } from './persona.js';
|
|
10
|
+
import { restore, snapshot } from './restore.js';
|
|
11
|
+
import { MAX_MINUTE, } from './types.js';
|
|
12
|
+
function fail(message) {
|
|
13
|
+
throw new Error(`persona calendar: ${message}`);
|
|
14
|
+
}
|
|
15
|
+
function object(x) {
|
|
16
|
+
if (!x || typeof x !== 'object' || Array.isArray(x))
|
|
17
|
+
fail('expected object');
|
|
18
|
+
}
|
|
19
|
+
function keys(x, allowed, required = allowed) {
|
|
20
|
+
object(x);
|
|
21
|
+
if (Object.keys(x).some((k) => !allowed.includes(k)) || required.some((k) => !Object.hasOwn(x, k)))
|
|
22
|
+
fail('unsupported or missing field');
|
|
23
|
+
}
|
|
24
|
+
function text(x) {
|
|
25
|
+
if (typeof x !== 'string' ||
|
|
26
|
+
!x.length ||
|
|
27
|
+
x.length > 1024 ||
|
|
28
|
+
['__proto__', 'constructor', 'prototype'].includes(x))
|
|
29
|
+
fail('invalid reference');
|
|
30
|
+
}
|
|
31
|
+
function num(x, min = -MAX_MINUTE, max = MAX_MINUTE) {
|
|
32
|
+
if (typeof x !== 'number' || !Number.isFinite(x) || x < min || x > max)
|
|
33
|
+
fail('invalid timestamp/number');
|
|
34
|
+
}
|
|
35
|
+
function list(x) {
|
|
36
|
+
if (!Array.isArray(x) || x.length > 128)
|
|
37
|
+
fail('invalid bounded list');
|
|
38
|
+
}
|
|
39
|
+
function strings(x) {
|
|
40
|
+
list(x);
|
|
41
|
+
for (const v of x)
|
|
42
|
+
text(v);
|
|
43
|
+
}
|
|
44
|
+
function same(a, b) {
|
|
45
|
+
const canonical = (x) => JSON.stringify(x, (_k, v) => v && typeof v === 'object' && !Array.isArray(v)
|
|
46
|
+
? Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
47
|
+
: v);
|
|
48
|
+
return canonical(a) === canonical(b);
|
|
49
|
+
}
|
|
50
|
+
function choice(value, allowed) {
|
|
51
|
+
return typeof value === 'string' && allowed.includes(value);
|
|
52
|
+
}
|
|
53
|
+
const standings = ['obligatory', 'recommended', 'permitted', 'disliked', 'forbidden'];
|
|
54
|
+
const kinds = ['promise', 'duty', 'appointment', 'worship', 'job', 'abstain'];
|
|
55
|
+
const reasons = ['illness', 'travel', 'necessity', 'sleep', 'unconscious', 'missed', 'broken'];
|
|
56
|
+
function sources(x) {
|
|
57
|
+
list(x);
|
|
58
|
+
if (!x.length)
|
|
59
|
+
fail('norm provenance required');
|
|
60
|
+
for (const v of x) {
|
|
61
|
+
keys(v, ['kind', 'ref']);
|
|
62
|
+
text(v.ref);
|
|
63
|
+
if (!choice(v.kind, ['revelation', 'hadith', 'interpretation', 'empirical', 'assumption']))
|
|
64
|
+
fail('invalid provenance');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function catalog(x) {
|
|
68
|
+
list(x);
|
|
69
|
+
const ids = new Set();
|
|
70
|
+
for (const v of x) {
|
|
71
|
+
keys(v, ['id', 'label', 'standing', 'sources', 'exemptions'], ['id', 'label', 'standing', 'sources']);
|
|
72
|
+
text(v.id);
|
|
73
|
+
text(v.label);
|
|
74
|
+
sources(v.sources);
|
|
75
|
+
if (ids.has(v.id) || !choice(v.standing, standings))
|
|
76
|
+
fail('invalid norm catalog');
|
|
77
|
+
ids.add(v.id);
|
|
78
|
+
if (v.exemptions !== undefined) {
|
|
79
|
+
list(v.exemptions);
|
|
80
|
+
const conditions = new Set();
|
|
81
|
+
for (const e of v.exemptions) {
|
|
82
|
+
keys(e, ['when', 'makeUp', 'sources']);
|
|
83
|
+
if (!choice(e.when, ['illness', 'travel']) || typeof e.makeUp !== 'boolean' || conditions.has(e.when))
|
|
84
|
+
fail('invalid exemption');
|
|
85
|
+
conditions.add(e.when);
|
|
86
|
+
sources(e.sources);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function calendarCommitment(x) {
|
|
92
|
+
keys(x, [
|
|
93
|
+
'id',
|
|
94
|
+
'kind',
|
|
95
|
+
'toId',
|
|
96
|
+
'actions',
|
|
97
|
+
'targetId',
|
|
98
|
+
'from',
|
|
99
|
+
'until',
|
|
100
|
+
'normId',
|
|
101
|
+
'importance',
|
|
102
|
+
'status',
|
|
103
|
+
'recurEvery',
|
|
104
|
+
'label',
|
|
105
|
+
'violatedBy',
|
|
106
|
+
'firm',
|
|
107
|
+
'exemptWhen',
|
|
108
|
+
'exempt',
|
|
109
|
+
'makeUpOf',
|
|
110
|
+
'chain',
|
|
111
|
+
'recurUntil',
|
|
112
|
+
], ['id', 'kind', 'actions', 'from', 'until', 'importance', 'status']);
|
|
113
|
+
for (const field of ['id', 'toId', 'targetId', 'normId', 'label', 'makeUpOf', 'chain'])
|
|
114
|
+
if (x[field] !== undefined)
|
|
115
|
+
text(x[field]);
|
|
116
|
+
if (!choice(x.kind, kinds) || !choice(x.status, ['pending', 'kept', 'broken', 'released']))
|
|
117
|
+
fail('invalid commitment');
|
|
118
|
+
strings(x.actions);
|
|
119
|
+
num(x.from);
|
|
120
|
+
num(x.until);
|
|
121
|
+
num(x.importance, 0, 1);
|
|
122
|
+
if (x.until < x.from || (x.kind !== 'abstain' && !x.actions.length))
|
|
123
|
+
fail('invalid commitment window/actions');
|
|
124
|
+
if (x.violatedBy !== undefined)
|
|
125
|
+
strings(x.violatedBy);
|
|
126
|
+
if (x.recurEvery !== undefined)
|
|
127
|
+
num(x.recurEvery, 1);
|
|
128
|
+
if (x.recurUntil !== undefined)
|
|
129
|
+
num(x.recurUntil);
|
|
130
|
+
if (x.firm !== undefined && x.firm !== true)
|
|
131
|
+
fail('invalid firm');
|
|
132
|
+
if (x.exemptWhen !== undefined) {
|
|
133
|
+
strings(x.exemptWhen);
|
|
134
|
+
if (x.exemptWhen.some((v) => !['illness', 'travel'].includes(v)))
|
|
135
|
+
fail('invalid exemption condition');
|
|
136
|
+
}
|
|
137
|
+
if (x.exempt !== undefined) {
|
|
138
|
+
keys(x.exempt, ['reason', 'at']);
|
|
139
|
+
num(x.exempt.at);
|
|
140
|
+
if (!choice(x.exempt.reason, ['illness', 'travel', 'necessity', 'sleep', 'unconscious']))
|
|
141
|
+
fail('invalid exemption reason');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function owed(x) {
|
|
145
|
+
keys(x, ['ofId', 'normId', 'kind', 'violatedBy', 'actions', 'reason', 'lapseSince', 'at', 'label', 'scheduledAs'], ['ofId', 'kind', 'actions', 'reason', 'at']);
|
|
146
|
+
for (const field of ['ofId', 'normId', 'label', 'scheduledAs'])
|
|
147
|
+
if (x[field] !== undefined)
|
|
148
|
+
text(x[field]);
|
|
149
|
+
if (!choice(x.kind, kinds) || !choice(x.reason, reasons))
|
|
150
|
+
fail('invalid owed record');
|
|
151
|
+
strings(x.actions);
|
|
152
|
+
num(x.at);
|
|
153
|
+
if (x.violatedBy !== undefined)
|
|
154
|
+
strings(x.violatedBy);
|
|
155
|
+
if (x.lapseSince !== undefined)
|
|
156
|
+
num(x.lapseSince);
|
|
157
|
+
}
|
|
158
|
+
function boundedJson(input) {
|
|
159
|
+
const pending = [{ value: input, depth: 0 }];
|
|
160
|
+
const seen = new Set();
|
|
161
|
+
let nodes = 0;
|
|
162
|
+
while (pending.length) {
|
|
163
|
+
const item = pending.pop();
|
|
164
|
+
if (!item)
|
|
165
|
+
break;
|
|
166
|
+
if (++nodes > 16384 || item.depth > 16)
|
|
167
|
+
fail('JSON bounds exceeded');
|
|
168
|
+
const { value, depth } = item;
|
|
169
|
+
if (value === null || typeof value === 'boolean')
|
|
170
|
+
continue;
|
|
171
|
+
if (typeof value === 'string') {
|
|
172
|
+
if (value.length > 1024)
|
|
173
|
+
fail('text bounds exceeded');
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (typeof value === 'number') {
|
|
177
|
+
num(value);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (typeof value !== 'object' || seen.has(value))
|
|
181
|
+
fail('non-JSON or cyclic input');
|
|
182
|
+
seen.add(value);
|
|
183
|
+
if (!Array.isArray(value) &&
|
|
184
|
+
Object.getPrototypeOf(value) !== Object.prototype &&
|
|
185
|
+
Object.getPrototypeOf(value) !== null)
|
|
186
|
+
fail('non-JSON object');
|
|
187
|
+
for (const [key, child] of Object.entries(value)) {
|
|
188
|
+
if (['__proto__', 'constructor', 'prototype'].includes(key))
|
|
189
|
+
fail('unsafe reference');
|
|
190
|
+
pending.push({ value: child, depth: depth + 1 });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Validate portable calendar data strictly; no restore defaults or debt settlement. */
|
|
195
|
+
export function validateCalendarPersona(input) {
|
|
196
|
+
boundedJson(input);
|
|
197
|
+
keys(input, ['format', 'person', 'calendar']);
|
|
198
|
+
if (input.format !== 'hf-persona-calendar/1')
|
|
199
|
+
fail('unsupported format');
|
|
200
|
+
const person = validatePersonaProjection(input.person);
|
|
201
|
+
if (person.commitments.length)
|
|
202
|
+
fail('calendar owns portable commitments');
|
|
203
|
+
const c = input.calendar;
|
|
204
|
+
keys(c, ['offset', 'commitments', 'owed', 'lapse', 'heldNorms', 'norms']);
|
|
205
|
+
num(c.offset);
|
|
206
|
+
list(c.commitments);
|
|
207
|
+
for (const v of c.commitments)
|
|
208
|
+
calendarCommitment(v);
|
|
209
|
+
list(c.owed);
|
|
210
|
+
for (const v of c.owed)
|
|
211
|
+
owed(v);
|
|
212
|
+
const commitments = c.commitments;
|
|
213
|
+
const debts = c.owed;
|
|
214
|
+
if (new Set(commitments.map((v) => v.id)).size !== c.commitments.length ||
|
|
215
|
+
new Set(debts.map((v) => v.ofId)).size !== c.owed.length)
|
|
216
|
+
fail('duplicate obligation id');
|
|
217
|
+
if (c.lapse !== null) {
|
|
218
|
+
keys(c.lapse, ['since', 'missed']);
|
|
219
|
+
num(c.lapse.since);
|
|
220
|
+
num(c.lapse.missed, 0);
|
|
221
|
+
if (!Number.isInteger(c.lapse.missed))
|
|
222
|
+
fail('invalid lapse');
|
|
223
|
+
}
|
|
224
|
+
catalog(c.norms);
|
|
225
|
+
list(c.heldNorms);
|
|
226
|
+
for (const v of c.heldNorms) {
|
|
227
|
+
keys(v, ['normId', 'standing', 'conviction']);
|
|
228
|
+
text(v.normId);
|
|
229
|
+
num(v.conviction, 0, 1);
|
|
230
|
+
if (!choice(v.standing, standings))
|
|
231
|
+
fail('invalid held understanding');
|
|
232
|
+
}
|
|
233
|
+
const heldNorms = c.heldNorms;
|
|
234
|
+
if (new Set(heldNorms.map((v) => v.normId)).size !== c.heldNorms.length)
|
|
235
|
+
fail('duplicate held norm');
|
|
236
|
+
const normIds = new Set(c.norms.map((n) => n.id));
|
|
237
|
+
for (const record of [...commitments, ...debts, ...heldNorms])
|
|
238
|
+
if (record.normId !== undefined && !normIds.has(record.normId))
|
|
239
|
+
fail('missing norm definition');
|
|
240
|
+
const arrival = person.source.now + c.offset;
|
|
241
|
+
num(arrival);
|
|
242
|
+
for (const debt of debts) {
|
|
243
|
+
const scheduled = commitments.find((v) => v.id === debt.scheduledAs);
|
|
244
|
+
// Pruned source history stays absent and unresolved; never invent a paid receipt.
|
|
245
|
+
if (scheduled && scheduled.makeUpOf !== debt.ofId)
|
|
246
|
+
fail('conflicting scheduled make-up reference');
|
|
247
|
+
if (debt.at > arrival || (debt.lapseSince !== undefined && debt.lapseSince > arrival))
|
|
248
|
+
fail('future owed evidence');
|
|
249
|
+
}
|
|
250
|
+
if (c.lapse !== null && c.lapse.since > arrival)
|
|
251
|
+
fail('future lapse evidence');
|
|
252
|
+
for (const duty of commitments)
|
|
253
|
+
if (duty.exempt && duty.exempt.at > arrival)
|
|
254
|
+
fail('future exemption evidence');
|
|
255
|
+
// Match the engine's owning-slice bounds exactly. A rejected import cannot be silently truncated.
|
|
256
|
+
const probe = createPerson({
|
|
257
|
+
id: 'calendar-validation',
|
|
258
|
+
name: 'validation',
|
|
259
|
+
seed: 0,
|
|
260
|
+
bornAt: 0,
|
|
261
|
+
sex: 'female',
|
|
262
|
+
});
|
|
263
|
+
probe.agenda.commitments = structuredClone(c.commitments);
|
|
264
|
+
probe.agenda.owed = structuredClone(c.owed);
|
|
265
|
+
if (c.lapse !== null)
|
|
266
|
+
probe.agenda.lapse = structuredClone(c.lapse);
|
|
267
|
+
probe.conscience.norms = structuredClone(c.heldNorms);
|
|
268
|
+
const checked = restore(probe);
|
|
269
|
+
if (!same(probe.agenda, checked.agenda) || !same(probe.conscience.norms, checked.conscience.norms))
|
|
270
|
+
fail('calendar exceeds supported engine bounds');
|
|
271
|
+
return { format: 'hf-persona-calendar/1', person, calendar: structuredClone(c) };
|
|
272
|
+
}
|
|
273
|
+
/** Source declares a complete semantic ID map, a clock offset and source-backed norm catalog. */
|
|
274
|
+
export function projectCalendarPersona(source, mapping) {
|
|
275
|
+
const p = restore(snapshot(source));
|
|
276
|
+
const m = mapping.calendar;
|
|
277
|
+
keys(m, ['version', 'offset', 'references', 'sourceNorms']);
|
|
278
|
+
if (m.version !== 'hf-persona-calendar-map/1')
|
|
279
|
+
fail('unsupported calendar mapping');
|
|
280
|
+
num(m.offset);
|
|
281
|
+
keys(m.references, ['commitments', 'chains', 'actions', 'entities', 'norms']);
|
|
282
|
+
for (const record of Object.values(m.references)) {
|
|
283
|
+
object(record);
|
|
284
|
+
if (Object.keys(record).length > 512)
|
|
285
|
+
fail('too many references');
|
|
286
|
+
for (const [key, value] of Object.entries(record)) {
|
|
287
|
+
text(key);
|
|
288
|
+
text(value);
|
|
289
|
+
}
|
|
290
|
+
if (new Set(Object.values(record)).size !== Object.values(record).length)
|
|
291
|
+
fail('duplicate mapped reference');
|
|
292
|
+
}
|
|
293
|
+
catalog(m.sourceNorms);
|
|
294
|
+
const ref = (kind, id) => {
|
|
295
|
+
const value = Object.hasOwn(m.references[kind], id) ? m.references[kind][id] : undefined;
|
|
296
|
+
if (value === undefined)
|
|
297
|
+
fail(`unmapped ${kind} reference ${id}`);
|
|
298
|
+
return value;
|
|
299
|
+
};
|
|
300
|
+
const shift = (at) => {
|
|
301
|
+
const value = at + m.offset;
|
|
302
|
+
num(value);
|
|
303
|
+
return value;
|
|
304
|
+
};
|
|
305
|
+
const debts = p.agenda.owed ?? [];
|
|
306
|
+
const selected = p.agenda.commitments.filter((c) => isContinuingCommitment(p.agenda, c) || debts.some((d) => d.ofId === c.id || d.scheduledAs === c.id));
|
|
307
|
+
const commitments = selected.map((old) => {
|
|
308
|
+
const c = structuredClone(old);
|
|
309
|
+
c.id = ref('commitments', old.id);
|
|
310
|
+
c.actions = old.actions.map((a) => ref('actions', a));
|
|
311
|
+
c.from = shift(old.from);
|
|
312
|
+
c.until = shift(old.until);
|
|
313
|
+
for (const [field, kind] of [
|
|
314
|
+
['toId', 'entities'],
|
|
315
|
+
['targetId', 'entities'],
|
|
316
|
+
['normId', 'norms'],
|
|
317
|
+
['chain', 'chains'],
|
|
318
|
+
['makeUpOf', 'commitments'],
|
|
319
|
+
]) {
|
|
320
|
+
const value = old[field];
|
|
321
|
+
if (value !== undefined)
|
|
322
|
+
c[field] = ref(kind, value);
|
|
323
|
+
}
|
|
324
|
+
if (old.violatedBy !== undefined)
|
|
325
|
+
c.violatedBy = old.violatedBy.map((a) => ref('actions', a));
|
|
326
|
+
if (old.recurUntil !== undefined)
|
|
327
|
+
c.recurUntil = shift(old.recurUntil);
|
|
328
|
+
if (c.exempt !== undefined)
|
|
329
|
+
c.exempt.at = shift(c.exempt.at);
|
|
330
|
+
return c;
|
|
331
|
+
});
|
|
332
|
+
const mappedOwed = debts.map((old) => {
|
|
333
|
+
const d = structuredClone(old);
|
|
334
|
+
d.ofId = ref('commitments', old.ofId);
|
|
335
|
+
d.at = shift(old.at);
|
|
336
|
+
d.actions = old.actions.map((a) => ref('actions', a));
|
|
337
|
+
if (old.normId !== undefined)
|
|
338
|
+
d.normId = ref('norms', old.normId);
|
|
339
|
+
if (old.scheduledAs !== undefined)
|
|
340
|
+
d.scheduledAs = ref('commitments', old.scheduledAs);
|
|
341
|
+
if (old.violatedBy !== undefined)
|
|
342
|
+
d.violatedBy = old.violatedBy.map((a) => ref('actions', a));
|
|
343
|
+
if (old.lapseSince !== undefined)
|
|
344
|
+
d.lapseSince = shift(old.lapseSince);
|
|
345
|
+
return d;
|
|
346
|
+
});
|
|
347
|
+
const heldNorms = p.conscience.norms.map((n) => ({ ...n, normId: ref('norms', n.normId) }));
|
|
348
|
+
const norms = m.sourceNorms.map((n) => ({ ...structuredClone(n), id: ref('norms', n.id) }));
|
|
349
|
+
const stripped = snapshot(p);
|
|
350
|
+
stripped.agenda.commitments = [];
|
|
351
|
+
delete stripped.agenda.owed;
|
|
352
|
+
delete stripped.agenda.lapse;
|
|
353
|
+
const person = projectPersona(stripped, {
|
|
354
|
+
version: mapping.version,
|
|
355
|
+
skills: mapping.skills,
|
|
356
|
+
...(mapping.episodes === undefined ? {} : { episodes: mapping.episodes }),
|
|
357
|
+
});
|
|
358
|
+
person.limitations = person.limitations.map((line) => line === 'source habits/methods/beliefs/social/conscience omitted; target authored state'
|
|
359
|
+
? 'source habits/methods/beliefs/social and conscience breaches omitted; target authored state; held norms transported'
|
|
360
|
+
: line);
|
|
361
|
+
for (const debt of mappedOwed) {
|
|
362
|
+
if (debt.scheduledAs !== undefined && !commitments.some((c) => c.id === debt.scheduledAs))
|
|
363
|
+
person.limitations.push(`owed:${debt.ofId}:scheduled history absent; payment unproven`);
|
|
364
|
+
}
|
|
365
|
+
return validateCalendarPersona({
|
|
366
|
+
format: 'hf-persona-calendar/1',
|
|
367
|
+
person,
|
|
368
|
+
calendar: {
|
|
369
|
+
offset: m.offset,
|
|
370
|
+
commitments,
|
|
371
|
+
owed: mappedOwed,
|
|
372
|
+
lapse: p.agenda.lapse ? { ...p.agenda.lapse, since: shift(p.agenda.lapse.since) } : null,
|
|
373
|
+
heldNorms,
|
|
374
|
+
norms,
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/** Receiver declares identical norm meanings and a clean calendar at the exact declared opening clock. */
|
|
379
|
+
export function admitCalendarPersona(input, target, policy) {
|
|
380
|
+
const projection = validateCalendarPersona(input);
|
|
381
|
+
catalog(policy.targetNorms);
|
|
382
|
+
const c = projection.calendar;
|
|
383
|
+
if (target.now - projection.person.source.now !== c.offset)
|
|
384
|
+
fail('calendar clock offset conflict');
|
|
385
|
+
const ordered = (ns) => [...ns].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
386
|
+
if (!same(ordered(c.norms), ordered(policy.targetNorms)))
|
|
387
|
+
fail('norm semantic compatibility conflict');
|
|
388
|
+
if (target.agenda.commitments.length || target.agenda.owed?.length || target.agenda.lapse)
|
|
389
|
+
fail('target calendar must be empty');
|
|
390
|
+
const result = admitPersona(projection.person, target, policy);
|
|
391
|
+
const p = result.person;
|
|
392
|
+
p.agenda.commitments = c.commitments;
|
|
393
|
+
if (c.owed.length)
|
|
394
|
+
p.agenda.owed = c.owed;
|
|
395
|
+
if (c.lapse !== null)
|
|
396
|
+
p.agenda.lapse = c.lapse;
|
|
397
|
+
p.conscience.norms = c.heldNorms;
|
|
398
|
+
p.agenda.nextId = Math.max(p.agenda.nextId, p.agenda.commitments.length + 1);
|
|
399
|
+
// Agenda-generated ids are c<N>; reserve imported ids before normal host scheduling resumes.
|
|
400
|
+
for (const record of [
|
|
401
|
+
...c.commitments.map((x) => x.id),
|
|
402
|
+
...c.owed.flatMap((x) => [x.ofId, ...(x.scheduledAs === undefined ? [] : [x.scheduledAs])]),
|
|
403
|
+
]) {
|
|
404
|
+
if (/^c\d+$/.test(record))
|
|
405
|
+
p.agenda.nextId = Math.max(p.agenda.nextId, Number(record.slice(1)) + 1);
|
|
406
|
+
}
|
|
407
|
+
const checked = restore(p);
|
|
408
|
+
if (!same(p, checked))
|
|
409
|
+
fail('admitted calendar exceeds supported engine bounds');
|
|
410
|
+
return { ...result, person: checked };
|
|
411
|
+
}
|
package/dist/persona.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.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",
|