@adam0white/human-framework 2.16.0 → 2.17.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 CHANGED
@@ -410,6 +410,7 @@ Knowledge lives in optional `Person.prospection` and survives snapshot/restore.
410
410
  `projectPersona(source, mapping)` produces `PersonaProjectionSnapshot` (`hf-persona-state/1`).
411
411
  `admitPersona(projection, targetOpening, { mappingVersion, sourceId, targetId })` returns a detached
412
412
  `{ person, report }`. `validatePersonaProjection(unknown)` validates portable data without filling defaults.
413
+ Portable v1 accepts the published 2.14.1 source-engine stamp and the current engine; it preserves that source provenance. Unknown source engines remain rejected.
413
414
  These functions never enter the actor loop and never mutate the source or the host opening.
414
415
 
415
416
  The host supplies a versioned map of skill ids, selected episode ids and every continuing obligation.
@@ -459,3 +460,19 @@ returning a detached Person. The receiving calendar must be empty; authored role
459
460
  admission, the host uses ordinary agenda APIs and its declared calendar. No duty is waived, no new ruling
460
461
  is invented, and no obligation is converted to a score. Activities, projects and incompatible identity or
461
462
  health remain unsupported. The original passport format and no-call continuation remain unchanged.
463
+
464
+ ### Project protection and narration options
465
+
466
+ Projects can opt into undo protection with `enableProjects(person, { protectProgress: true })`.
467
+ Declare `diminishes: [prospectId]` on an offer or its described successor, or let the person
468
+ learn the exact action context from a perceived drop after its own completed act.
469
+ `adoptProjectTask(person, prospectId, task)` reuses an active project and its net credit
470
+ for a host task; an absent match returns `undefined`.
471
+
472
+ Narration remains unchanged unless requested. Set `person.lexicon.variety`,
473
+ `nounActions` or `materialMeaning` for the corresponding host wording. Use
474
+ `promptingLine(reading, outcome, kind, { voice: 'third' })` for plain third-person lines.
475
+ `converse` adds per-call `loyalWarning`, `confidentMin` and `phrase` options;
476
+ `perceive` adds a third `{ reputationTraits }` argument. See the architecture's
477
+ [narration and social host options](../../docs/framework.md#narration-and-social-host-options-opt-in)
478
+ for cause-aware reviews and actor-addressed recall. These options change no default continuation.
@@ -49,6 +49,7 @@
49
49
  import { DEFAULT_NORMS } from '../conscience/catalog.js';
50
50
  import { clamp01, dayOf, dpow, hasNumbers, isNum, isObj } from '../core/index.js';
51
51
  import { expectedEffect } from '../memory/index.js';
52
+ import { diminishesProject } from '../projects/index.js';
52
53
  import { MINUTES_PER_DAY } from '../types.js';
53
54
  export const AGENDA_DEFAULTS = {
54
55
  /** Minutes before a window opens during which a commitment already pulls (to get there). */
@@ -527,6 +528,8 @@ function goalValue(g, aff, now) {
527
528
  }
528
529
  /** Opt-in completion-time and success estimate, shared by direct and received routes. @internal */
529
530
  export function temporalGoalValue(p, g, aff, now, completion) {
531
+ if (g.project && diminishesProject(p, aff, g.project.prospectId))
532
+ return 0;
530
533
  const base = goalValue(g, aff, now);
531
534
  return g.temporal
532
535
  ? base *
@@ -578,7 +581,7 @@ export function agendaTermsWithGoalEstimates(p, aff, now, goalOverrides) {
578
581
  terms.push({ source: `commitment:${c.id}`, value: d.commitmentScale * c.importance * pressure });
579
582
  }
580
583
  for (const g of p.agenda.goals) {
581
- if (g.status !== 'active')
584
+ if (g.status !== 'active' || (g.project && diminishesProject(p, aff, g.project.prospectId)))
582
585
  continue;
583
586
  const override = goalOverrides?.get(g.id);
584
587
  if (override !== undefined || g.temporal) {
@@ -55,6 +55,12 @@ export interface ConverseContext {
55
55
  traits?: Record<string, TraitEffect>;
56
56
  /** Carry belief origin and opaque lineage into told claims (off by default). */
57
57
  provenance?: boolean;
58
+ /** Relay beliefs this far from 0.5 (0..0.5); default 0.2. Invalid values use the default. */
59
+ confidentMin?: number;
60
+ /** Let a fond speaker warn the listener of an unfavourable character claim about them. */
61
+ loyalWarning?: boolean;
62
+ /** Presentation only. Receives a told claim or a withheld reason and its default summary; never changes selection. */
63
+ phrase?: (claim: Readonly<ToldClaim | WithheldClaim>, summary: string) => string;
58
64
  }
59
65
  /** Host-visible record of one claim. */
60
66
  export interface ToldClaim {
@@ -80,6 +86,12 @@ export interface AdviceGiven {
80
86
  /** For the listener's next decision; the voice is the speaker. */
81
87
  suggestion: Suggestion;
82
88
  }
89
+ export interface WithheldClaim {
90
+ prop: string;
91
+ reason: string;
92
+ /** Present only when a host phrasing callback rendered this reason. */
93
+ summary?: string;
94
+ }
83
95
  export interface ConversationResult {
84
96
  /** Testimony percepts for `perceive(listener, told)`. */
85
97
  told: Percept[];
@@ -87,10 +99,7 @@ export interface ConversationResult {
87
99
  advice: AdviceGiven[];
88
100
  claims: ToldClaim[];
89
101
  /** Candidate claims the speaker held back, and why (e.g. 'norm:backbiting', 'norm:lying'). */
90
- withheld: {
91
- prop: string;
92
- reason: string;
93
- }[];
102
+ withheld: WithheldClaim[];
94
103
  /** True when any told claim was a lie. */
95
104
  deceit: boolean;
96
105
  /** The speaker's own deed tags for conscience (e.g. backbiting/lying violated), deduplicated, sorted. */
@@ -21,6 +21,10 @@
21
21
  * share of conversation about absent third parties that spreads reputational information (Dunbar 2004), and
22
22
  * extraversion as talkativeness (how many claims are offered).
23
23
  *
24
+ * SCOPE (host options): `loyalWarning` admits only unfavourable listener claims when affection is at least
25
+ * 0.5 (an engineering threshold). `confidentMin` sets a per-call relay threshold. `phrase` renders selected
26
+ * testimony only; the host owns authored facts, delivery and subsequent disclosure. No hidden relay occurs.
27
+ *
24
28
  * It does NOT generate language, model turn-taking, persuasion beyond source trust, what the speaker believes
25
29
  * the listener already knows, lies of omission, white lies told to spare feelings, or the speaker's fear of
26
30
  * being found out. Coefficients are engineering defaults, not calibrated estimates. Nothing here judges the
@@ -150,12 +154,25 @@ export function converse(speaker, listener, ctx) {
150
154
  if (b.prop.startsWith('advice:') || b.prop.includes(':should:'))
151
155
  continue;
152
156
  const subject = b.prop.split(':')[0];
153
- if (subject === listener.id || subject === speaker.id)
157
+ if (subject === speaker.id)
154
158
  continue;
155
159
  const cred = expit(b.logOdds);
156
- if (Math.abs(cred - 0.5) < C.confidentMin)
160
+ const threshold = Number.isFinite(ctx.confidentMin) &&
161
+ ctx.confidentMin >= 0 &&
162
+ ctx.confidentMin <= 0.5
163
+ ? ctx.confidentMin
164
+ : C.confidentMin;
165
+ if (Math.abs(cred - 0.5) < threshold)
157
166
  continue;
158
167
  const value = cred > 0.5;
168
+ if (subject === listener.id) {
169
+ const trait = parseTraitProp(b.prop, table)?.trait;
170
+ if (!ctx.loyalWarning ||
171
+ relationshipWith(speaker, listener.id).affection < 0.5 ||
172
+ trait === undefined ||
173
+ !isUnfavourableTrait(trait, value, table))
174
+ continue;
175
+ }
159
176
  const confidence = value ? cred : 1 - cred;
160
177
  const recency = 1 / (1 + Math.max(0, ctx.at - b.updatedAt) / C.recencyScale);
161
178
  consider(b.prop, value, confidence, cred, false, R.confidence * (confidence - 0.5) * 2 + R.recency * recency);
@@ -231,6 +248,8 @@ export function converse(speaker, listener, ctx) {
231
248
  ? `${nameOf(speaker)} says ${c.about} is ${c.value ? '' : 'not '}${c.prop.split(':')[1] ?? ''}`
232
249
  : `${nameOf(speaker)} says ${c.prop} is ${c.value ? 'so' : 'not so'}`,
233
250
  };
251
+ if (ctx.phrase)
252
+ pc.summary = ctx.phrase(Object.freeze(structuredClone(c)), pc.summary ?? '');
234
253
  const claim = pc.claims?.[0];
235
254
  if (claim && c.origin !== undefined)
236
255
  claim.origin = c.origin;
@@ -292,6 +311,9 @@ export function converse(speaker, listener, ctx) {
292
311
  suggestion.appeal = 'benevolence';
293
312
  out.advice.push({ action: a.action, percept, suggestion });
294
313
  }
314
+ if (ctx.phrase)
315
+ for (const withheld of out.withheld)
316
+ withheld.summary = ctx.phrase(Object.freeze(structuredClone(withheld)), withheld.reason);
295
317
  const magnitude = clamp01((ctx.magnitude ?? C.chatMagnitude) + C.chatPerClaim * out.told.length);
296
318
  out.socialEvents.speaker.push({ at: ctx.at, kind: 'chat', otherId: listener.id, byMe: true, magnitude });
297
319
  out.socialEvents.listener.push({ at: ctx.at, kind: 'chat', otherId: speaker.id, byMe: false, magnitude });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.16.0";
2
+ export declare const FRAMEWORK_VERSION = "2.17.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';
@@ -32,7 +32,7 @@ export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistenc
32
32
  export * from './person.ts';
33
33
  export * from './persona.ts';
34
34
  export * from './persona-calendar.ts';
35
- export { considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal } from './projects/index.ts';
35
+ export { adoptProjectTask, considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal, } from './projects/index.ts';
36
36
  export * from './promptings/index.ts';
37
37
  export type { ConsequenceEvidence, PlanRoute, PlanStep } from './prospection/index.ts';
38
38
  export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export const FRAMEWORK_VERSION = '2.16.0';
2
+ export const FRAMEWORK_VERSION = '2.17.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, 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';
@@ -29,7 +29,7 @@ export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistenc
29
29
  export * from './person.js';
30
30
  export * from './persona.js';
31
31
  export * from './persona-calendar.js';
32
- export { considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal } from './projects/index.js';
32
+ export { adoptProjectTask, considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal, } from './projects/index.js';
33
33
  export * from './promptings/index.js';
34
34
  export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.js';
35
35
  export { fingerprint, restore, snapshot } from './restore.js';
package/dist/migrate.js CHANGED
@@ -71,6 +71,11 @@ export const MIGRATIONS = {
71
71
  note: 'the surroundings field `ambient.now` is renamed `ambient.percept`; per-person retention is optional and starts absent; behaviour unchanged',
72
72
  apply: renameAmbient,
73
73
  },
74
+ '2.14.1': {
75
+ to: '2.17.0',
76
+ note: 'version stamp only; project protection and narration/social options require explicit opt-in, and existing enabled faculties retain exact behavior',
77
+ apply: stamp,
78
+ },
74
79
  '2.14.0': {
75
80
  to: '2.14.1',
76
81
  note: 'version stamp only; restored projects and persistence retain 2.14.0 behaviour until explicitly re-enabled',
@@ -34,6 +34,23 @@ export const EN_LINES = {
34
34
  // Decision lines by other dominant sources.
35
35
  'source:habit': ['Same as always.', 'Out of habit, really.', "It's what I do at this hour."],
36
36
  'source:effort': ['The easy thing, for now.', 'Nothing strenuous.'],
37
+ 'review.cause': [
38
+ 'After {cause}, {missed} remained undone. {next}',
39
+ 'I left {missed} unfinished after {cause}. {next}',
40
+ 'I was {cause}; {missed} went unfinished. {next}',
41
+ ],
42
+ 'review.accounting': [
43
+ "I didn't keep {missed}. Next time I'll be ready when it comes.",
44
+ 'I left {missed} undone. I will prepare for the next occasion.',
45
+ 'Today {missed} went unfinished. Next time I will make room for it.',
46
+ ],
47
+ 'review.ruminating': [
48
+ 'I keep going back over {missed}.',
49
+ 'My thoughts keep returning to {missed}.',
50
+ 'I cannot stop turning {missed} over in my mind.',
51
+ ],
52
+ 'source:material.named': ['It brings {material}.', 'We can use the {material}.'],
53
+ 'intention.material.named': ['for {material}'],
37
54
  'source:material': ['There is money in it.', 'It pays.'],
38
55
  'source:expectation': ['It went well last time.', 'I know how this goes.'],
39
56
  'source:conscience:repair': ['I owe them that much.', 'I have to make this right.'],
@@ -363,6 +380,12 @@ function regularGerund(verb) {
363
380
  export function actionForms(action, lexicon) {
364
381
  const given = lexicon?.actions?.[action];
365
382
  const base = given?.base ?? action.replace(/[-_]/g, ' ');
383
+ if (lexicon?.nounActions)
384
+ return {
385
+ base: given?.base ?? `do ${base}`,
386
+ past: given?.past ?? `did ${base}`,
387
+ gerund: given?.gerund ?? `doing ${base}`,
388
+ };
366
389
  const [verb = base, ...rest] = base.split(' ');
367
390
  const tail = rest.length > 0 ? ` ${rest.join(' ')}` : '';
368
391
  return {
@@ -1,4 +1,4 @@
1
- import type { DecisionRecord, Lexicon, Person, SuggestionResolution } from '../types.ts';
1
+ import type { DecisionRecord, Episode, Lexicon, Person, SuggestionResolution } from '../types.ts';
2
2
  /** Intention string for a decision: 'for Allah', 'to feed myself', 'to keep my promise to X', ... */
3
3
  export declare function intentionFor(p: Person, record: DecisionRecord, lexicon?: Lexicon): string;
4
4
  /** First-person narration of a decision record. Deterministic by the decision id. */
@@ -7,3 +7,9 @@ export declare function narrateDecision(p: Person, record: DecisionRecord, lexic
7
7
  export declare function voiceLine(p: Person, res: SuggestionResolution, key?: string, lexicon?: Lexicon): string;
8
8
  /** Short UI summary of a person's state. */
9
9
  export declare function describePerson(p: Person, lexicon?: Lexicon): string;
10
+ /**
11
+ * SCOPE: a host-authored addressee form of an episode, selected only for its named actor.
12
+ * Free text cannot safely be inflected. With no authored form, return the original summary.
13
+ * This read-only helper neither recalls an episode nor changes its salience or RNG.
14
+ */
15
+ export declare function recallLine(episode: Episode, toActor: string): string;
@@ -93,7 +93,9 @@ export function intentionFor(p, record, lexicon) {
93
93
  case 'emotion':
94
94
  return phraseLine('intention.emotion', {}, lex);
95
95
  case 'material':
96
- return phraseLine('intention.material', {}, lex);
96
+ return lex?.materialMeaning !== undefined
97
+ ? phraseLine('intention.material.named', { material: lex.materialMeaning }, lex)
98
+ : phraseLine('intention.material', {}, lex);
97
99
  case 'precommit':
98
100
  return phraseLine('intention.precommit', {}, lex);
99
101
  case 'expectation':
@@ -157,6 +159,8 @@ export function narrateDecision(p, record, lexicon) {
157
159
  ? pickLine(`emotion:${tag}`, id, {}, lex)
158
160
  : pickLine('emotion:default', id, {}, lex));
159
161
  }
162
+ else if (kind === 'material' && lex?.materialMeaning !== undefined)
163
+ parts.push(pickLine('source:material.named', id, { material: lex.materialMeaning }, lex));
160
164
  else
161
165
  parts.push(linesFor(`source:${source}`, lex).length > 0
162
166
  ? pickLine(`source:${source}`, id, {}, lex)
@@ -192,6 +196,8 @@ export function narrateDecision(p, record, lexicon) {
192
196
  /** The person's spoken reply to a suggestion. */
193
197
  export function voiceLine(p, res, key = res.reason, lexicon) {
194
198
  const lex = lexicon ?? p.lexicon;
199
+ if (lex?.variety && (res.verdict === 'assented' || res.verdict === 'deferred'))
200
+ key = `${p.id}:${p.now}:${p.traits.extraversion}:${p.traits.honesty}:${key}`;
195
201
  const trust = p.will.voices.find((v) => v.voiceId === res.voiceId)?.trust ?? 0.5;
196
202
  switch (res.verdict) {
197
203
  case 'assented':
@@ -293,3 +299,13 @@ export function describePerson(p, lexicon) {
293
299
  const state = bits.length > 0 ? bits.join(', ') : phraseLine('describe.well', {}, lex);
294
300
  return phraseLine('describe.line', { name: p.name, act, state }, lex);
295
301
  }
302
+ /**
303
+ * SCOPE: a host-authored addressee form of an episode, selected only for its named actor.
304
+ * Free text cannot safely be inflected. With no authored form, return the original summary.
305
+ * This read-only helper neither recalls an episode nor changes its salience or RNG.
306
+ */
307
+ export function recallLine(episode, toActor) {
308
+ return episode.actorId === toActor && episode.addressedSummary !== undefined
309
+ ? episode.addressedSummary
310
+ : episode.summary;
311
+ }
package/dist/person.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type BodyForecastSpan } from './body/index.ts';
2
2
  import { type NafsPattern } from './promptings/index.ts';
3
3
  import { type SkillTransfer } from './skills/index.ts';
4
+ import { applyReputationBelief } from './social/index.ts';
4
5
  import type { Activity, Affordance, BodyLoad, BodyReadout, Command, Commitment, DecisionRecord, LearningDomain, Minute, NeedId, NeedReading, NormDefinition, Outcome, Percept, Person, PersonSpec, Retention, Suggestion, SuggestionResolution, Unit } from './types.ts';
5
6
  import { type CommandOutcome } from './will/index.ts';
6
7
  export declare const PERSON_DEFAULTS: {
@@ -85,12 +86,16 @@ export declare function standUp(p: Person): boolean;
85
86
  * only age (from `life.bornAt`) changes. Hosts that want the interval lived call `tick`.
86
87
  */
87
88
  export declare function skip(p: Person, to: Minute): void;
89
+ export interface PerceiveOptions {
90
+ /** Complete host character-trait vocabulary; absent uses HF's default table. */
91
+ reputationTraits?: Parameters<typeof applyReputationBelief>[5];
92
+ }
88
93
  /**
89
94
  * Attend to percepts (limited capacity), then encode what got through: claims into beliefs, interactions
90
95
  * into relationships, others' acts judged by my norms, appraisal into emotions, and an episode each.
91
96
  * Returns the percepts that were attended to.
92
97
  */
93
- export declare function perceive(p: Person, percepts: readonly Percept[]): Percept[];
98
+ export declare function perceive(p: Person, percepts: readonly Percept[], options?: PerceiveOptions): Percept[];
94
99
  export interface DecideOptions {
95
100
  suggestion?: Suggestion;
96
101
  /** Several voices in one decision (N1); merged with `suggestion`, one per voice id. */
package/dist/person.js CHANGED
@@ -38,7 +38,7 @@ import { intentionFor, narrateDecision, voiceLine } from './narrate/index.js';
38
38
  import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisfy, urgencyOf, } from './needs/index.js';
39
39
  import { widow } from './partnering/index.js';
40
40
  import { beginPursuit, enablePersistence, releasePursuit } from './persistence/index.js';
41
- import { considerProjects, enableProjects, noteProjectStatus, perceiveProspect } from './projects/index.js';
41
+ import { considerProjects, enableProjects, noteProjectAct, noteProjectStatus, perceiveProspect, } from './projects/index.js';
42
42
  import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
43
43
  import { bodyForecastFor, consumeConsequences, enablePlanning, forecastLine, forecastThresholds, pruneConsequences, receiveConsequences, } from './prospection/index.js';
44
44
  import { enableReview, reviewIfDue } from './review/index.js';
@@ -585,10 +585,10 @@ function socialKindOf(kind) {
585
585
  return kind;
586
586
  return SOCIAL_ALIASES[kind];
587
587
  }
588
- function perceiveOne(p, pc) {
588
+ function perceiveOne(p, pc, options) {
589
589
  receiveConsequences(p, pc);
590
590
  if (p.projects && pc.prospect)
591
- perceiveProspect(p, pc.prospect, pc.at);
591
+ perceiveProspect(p, pc.prospect, pc.at, (pc.channel === 'saw' || pc.channel === 'outcome') && (pc.actorId === undefined || pc.actorId === p.id));
592
592
  const at = Math.min(Math.max(pc.at, p.affect.lastUpdated), p.now);
593
593
  const actor = pc.actorId;
594
594
  const target = pc.targetId;
@@ -606,7 +606,7 @@ function perceiveOne(p, pc) {
606
606
  if (claim.origin !== undefined || claim.lineage !== undefined)
607
607
  noteProvenance(p, claim, direct ? p.id : (actor ?? 'rumour'), at);
608
608
  // Hearsay about someone's character moves my relationship with them (reputation).
609
- applyReputationBelief(p, claim.prop, before, credence(p, claim.prop), at);
609
+ applyReputationBelief(p, claim.prop, before, credence(p, claim.prop), at, options.reputationTraits);
610
610
  }
611
611
  // Another source's account, reported by the teller (2.10.0): one source telling two people opposite things.
612
612
  if (pc.channel === 'told' && actor !== undefined && Array.isArray(pc.reports)) {
@@ -724,8 +724,11 @@ function perceiveOne(p, pc) {
724
724
  summary: pc.summary,
725
725
  tags: [pc.kind, pc.channel, ...(isNight(at) ? ['night'] : []), ...(threat ? ['threat'] : [])],
726
726
  };
727
- if (actor !== undefined)
727
+ if (actor !== undefined) {
728
728
  episode.actorId = actor;
729
+ if (pc.addressedSummary !== undefined)
730
+ episode.addressedSummary = pc.addressedSummary;
731
+ }
729
732
  if (target !== undefined)
730
733
  episode.targetId = target;
731
734
  if (pc.placeId !== undefined)
@@ -748,7 +751,7 @@ function judgeDeed(p, actor, pc, at) {
748
751
  * into relationships, others' acts judged by my norms, appraisal into emotions, and an episode each.
749
752
  * Returns the percepts that were attended to.
750
753
  */
751
- export function perceive(p, percepts) {
754
+ export function perceive(p, percepts, options = {}) {
752
755
  if (percepts.length === 0 || !p.body.alive)
753
756
  return [];
754
757
  const body = readBodyOf(p);
@@ -758,7 +761,7 @@ export function perceive(p, percepts) {
758
761
  fear: emotionLevel(p, 'fear'),
759
762
  });
760
763
  for (const pc of attended) {
761
- perceiveOne(p, pc);
764
+ perceiveOne(p, pc, options);
762
765
  // What I just noticed may bring a memory back (the cemetery road, her name on the phone).
763
766
  const at = Math.min(Math.max(pc.at, p.affect.lastUpdated), p.now);
764
767
  const cue = { at, action: pc.kind, tags: [pc.kind] };
@@ -1449,8 +1452,10 @@ export function finish(p, outcome, opts = {}) {
1449
1452
  ...(act.necessity ? { necessity: true } : {}),
1450
1453
  ...(opts.catalog ? { catalog: opts.catalog } : {}),
1451
1454
  });
1452
- if (p.projects)
1455
+ if (p.projects) {
1456
+ noteProjectAct(p, aff, completed, now);
1453
1457
  noteProjectStatus(p, now);
1458
+ }
1454
1459
  if (p.pursuit)
1455
1460
  releasePursuit(p);
1456
1461
  // A completed violating action inside an open abstention's window broke it (the fast, e.g. "no work after the evening prayer").
package/dist/persona.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * identity, age, family and health compatibility before returning a new Person. Neither input is mutated.
5
5
  * This is not a world migration, identity registry, transport, hash, authentication or general calendar mapper.
6
6
  * v1 admits zero-duration travel and healthy compatible bodies only; family/bonds remain host-authored and must
7
- * match source provenance. Pregnancy and nonempty temporal bonds are unsupported in v1. All other world-dependent stores stay with the source and are disclosed as omitted.
7
+ * match source provenance. Portable v1 accepts its published 2.14.1 stamp and the current engine;
8
+ * the 2.14.1 to 2.17.0 migration changes no portable field semantics. Pregnancy and nonempty temporal bonds are unsupported in v1. All other world-dependent stores stay with the source and are disclosed as omitted.
8
9
  */
9
10
  import { hasUnresolvedMakeUps, sameChain } from './agenda/agenda.js';
10
11
  import { createPerson } from './person.js';
@@ -185,7 +186,9 @@ export function validatePersonaProjection(input) {
185
186
  text(input.mappingVersion);
186
187
  text(input.name);
187
188
  keys(input.source, ['schema', 'engine', 'id', 'now']);
188
- if (input.source.schema !== PERSON_SCHEMA || input.source.engine !== ENGINE_VERSION)
189
+ // Both published passport releases used 2.14.1; the next engine upgrade is stamp-only here.
190
+ const compatibleEngine = input.source.engine === ENGINE_VERSION || input.source.engine === '2.14.1';
191
+ if (input.source.schema !== PERSON_SCHEMA || !compatibleEngine)
189
192
  fail('unsupported source schema/engine; migrate the source before projection');
190
193
  text(input.source.id);
191
194
  num(input.source.now);
@@ -1,4 +1,4 @@
1
- import type { Minute, Person, ProjectsSpec, ProjectsState, Prospect } from '../types.ts';
1
+ import type { Goal, Minute, Person, ProjectsSpec, ProjectsState, Prospect } from '../types.ts';
2
2
  export declare const PROJECTS_DEFAULTS: {
3
3
  readonly maxProspects: 8;
4
4
  readonly maxHistory: 8;
@@ -13,10 +13,16 @@ export declare const PROJECTS_DEFAULTS: {
13
13
  };
14
14
  /** Enable or reconfigure without discarding retained prospects and history. */
15
15
  export declare function enableProjects(p: Person, spec?: ProjectsSpec): ProjectsState;
16
+ /** Turn an active held project into a host task. Undefined means there is no active held project.
17
+ * Reuses identity, adoption time, progress, peak and progress clock; never revives an abandoned project.
18
+ * Retains its temporal horizon unless the task supplies a valid replacement.
19
+ * The caller supplies task meaning and deadlines. No second goal or fresh progress is created.
20
+ */
21
+ export declare function adoptProjectTask(p: Person, prospectId: string, task: Omit<Goal, 'id' | 'status' | 'progress' | 'adoptedAt' | 'project' | 'lastAdvancedAt'>, now?: Minute): Goal | undefined;
16
22
  /** Read-only, deterministic appeal. Thresholds close the relaxed gate before the bounded bonus. */
17
23
  export declare function projectAppeal(p: Person, prospect: Prospect, now?: Minute): number;
18
24
  /** Only the composite calls this, after attention. Newest event minute wins; equal-minute input order wins. */
19
- export declare function perceiveProspect(p: Person, input: Prospect, at: Minute): void;
25
+ export declare function perceiveProspect(p: Person, input: Prospect, at: Minute, attributeOwnAct?: boolean): void;
20
26
  /** Called on each tick segment, including one-minute begin/tick/finish host loops. No RNG. */
21
27
  export declare function considerProjects(p: Person, now: Minute): void;
22
28
  /** Conservative restore: bounded fields, finite past timestamps, and no ambiguous duplicate ids. */
@@ -9,7 +9,12 @@
9
9
  * goals. All kinds serve competence and leisure: making is chosen as a free-time pursuit, not duty.
10
10
  * Behaviour 2 credits only new perceived highs; event-credited projects cannot observe reversals and stay unchanged.
11
11
  * Restore recovers one active project, keeping the newest (id breaks ties) and abandoning extras.
12
- * No invented prospects, host task interpretation, world search, persistence, shielding, faith lens,
12
+ * Explicit protectProgress blocks goal credit for host-declared reductions and exact contextual acts
13
+ * followed within five minutes by an attended drop after one's own completion. This is a bounded
14
+ * causal assumption, not proof of causation. Only direct saw/outcome readings without another actor
15
+ * can teach that link. It learns no effects from unattended world state or testimony.
16
+ * adoptProjectTask reuses an active project goal, preserving net credit and pursuit identity.
17
+ * No invented prospects, implicit host task interpretation, world search, persistence, shielding, faith lens,
13
18
  * LLM or RNG. Without the slice, perception and minute bookkeeping write nothing.
14
19
  */
15
20
  import { emotionLevel } from '../affect/index.js';
@@ -44,6 +49,8 @@ function cleanSpec(input) {
44
49
  const out = {};
45
50
  if (!isObj(input))
46
51
  return out;
52
+ if (typeof input.protectProgress === 'boolean')
53
+ out.protectProgress = input.protectProgress;
47
54
  for (const key of KEYS) {
48
55
  const x = input[key];
49
56
  const unit = key === 'calmUrgency' || key === 'fearBelow' || key === 'adoptAppeal';
@@ -90,6 +97,70 @@ export function enableProjects(p, spec = {}) {
90
97
  p.projects = state;
91
98
  return state;
92
99
  }
100
+ /** Whether this offer is understood to undo this prospect. Read-only; exact context, no action-only generalisation. @internal */
101
+ export function diminishesProject(p, aff, prospectId) {
102
+ const s = p.projects;
103
+ return (s?.spec?.protectProgress === true &&
104
+ ((aff.diminishes?.includes(prospectId) ?? false) ||
105
+ (s.setbacks?.some((x) => x.prospectId === prospectId &&
106
+ x.action === aff.action &&
107
+ x.targetId === aff.targetId &&
108
+ x.placeId === aff.placeId) ??
109
+ false)));
110
+ }
111
+ /** Own completed acts alone can explain the next attended drop. Failed/interrupted acts clear attribution. @internal */
112
+ export function noteProjectAct(p, aff, completed, at) {
113
+ const s = p.projects;
114
+ if (!s?.spec?.protectProgress)
115
+ return;
116
+ delete s.pendingAct;
117
+ if (!completed ||
118
+ !str(aff.action) ||
119
+ (aff.targetId !== undefined && !str(aff.targetId)) ||
120
+ (aff.placeId !== undefined && !str(aff.placeId)))
121
+ return;
122
+ const readings = s.prospects
123
+ .filter((x) => x.progress !== undefined &&
124
+ p.agenda.goals.some((g) => g.status === 'active' && g.project?.prospectId === x.id))
125
+ .map((x) => ({ prospectId: x.id, progress: x.progress }));
126
+ if (!readings.length)
127
+ return;
128
+ s.pendingAct = {
129
+ action: aff.action,
130
+ at,
131
+ readings,
132
+ ...(aff.targetId !== undefined ? { targetId: aff.targetId } : {}),
133
+ ...(aff.placeId !== undefined ? { placeId: aff.placeId } : {}),
134
+ };
135
+ }
136
+ /** Turn an active held project into a host task. Undefined means there is no active held project.
137
+ * Reuses identity, adoption time, progress, peak and progress clock; never revives an abandoned project.
138
+ * Retains its temporal horizon unless the task supplies a valid replacement.
139
+ * The caller supplies task meaning and deadlines. No second goal or fresh progress is created.
140
+ */
141
+ export function adoptProjectTask(p, prospectId, task, now = p.now) {
142
+ if (!p.projects || !minute(now, p.now))
143
+ return undefined;
144
+ const held = p.agenda.goals.find((g) => g.status === 'active' && g.project?.prospectId === prospectId);
145
+ if (!held)
146
+ return undefined;
147
+ // Use agenda's copying and sanitization, then retain the existing goal rather than allocating a new id.
148
+ const nextId = p.agenda.nextId;
149
+ const staged = adoptGoal(p, task, now);
150
+ p.agenda.goals.pop();
151
+ p.agenda.nextId = nextId;
152
+ held.label = staged.label;
153
+ held.serves = staged.serves;
154
+ held.advancedBy = staged.advancedBy;
155
+ held.importance = staged.importance;
156
+ held.baseImportance = staged.baseImportance;
157
+ delete held.deadline;
158
+ if (staged.deadline !== undefined)
159
+ held.deadline = staged.deadline;
160
+ if (staged.temporal !== undefined)
161
+ held.temporal = staged.temporal;
162
+ return held;
163
+ }
93
164
  function relaxed(p) {
94
165
  const needs = readPerson(p).needs;
95
166
  return {
@@ -166,7 +237,7 @@ export function noteProjectStatus(p, now) {
166
237
  recordProjectStatus(p, g, now);
167
238
  }
168
239
  /** Only the composite calls this, after attention. Newest event minute wins; equal-minute input order wins. */
169
- export function perceiveProspect(p, input, at) {
240
+ export function perceiveProspect(p, input, at, attributeOwnAct = false) {
170
241
  const s = p.projects;
171
242
  if (!s || !minute(at, p.now))
172
243
  return;
@@ -178,6 +249,32 @@ export function perceiveProspect(p, input, at) {
178
249
  return;
179
250
  if (p.now - at > setting(s, 'forgetMinutes'))
180
251
  return;
252
+ const act = s.pendingAct;
253
+ if (s.spec?.protectProgress && act && at >= act.at) {
254
+ const before = act.readings.find((x) => x.prospectId === prospect.id);
255
+ if (before && prospect.progress !== undefined) {
256
+ if (attributeOwnAct && at - act.at <= 5 && prospect.progress < before.progress) {
257
+ const learned = {
258
+ prospectId: prospect.id,
259
+ action: act.action,
260
+ ...(act.targetId !== undefined ? { targetId: act.targetId } : {}),
261
+ ...(act.placeId !== undefined ? { placeId: act.placeId } : {}),
262
+ };
263
+ const list = s.setbacks ?? [];
264
+ if (!list.some((x) => x.prospectId === learned.prospectId &&
265
+ x.action === learned.action &&
266
+ x.targetId === learned.targetId &&
267
+ x.placeId === learned.placeId))
268
+ list.push(learned);
269
+ s.setbacks = list.slice(-16);
270
+ }
271
+ act.readings = act.readings.filter((x) => x.prospectId !== prospect.id);
272
+ if (!act.readings.length)
273
+ delete s.pendingAct;
274
+ }
275
+ if (at - act.at > 5)
276
+ delete s.pendingAct;
277
+ }
181
278
  const held = { ...prospect, lastSeenAt: at };
182
279
  if (old)
183
280
  s.prospects[s.prospects.indexOf(old)] = held;
@@ -268,6 +365,49 @@ export function sanitizeProjects(input, now) {
268
365
  return undefined;
269
366
  out.currentGoalId = input.currentGoalId;
270
367
  }
368
+ if (input.setbacks !== undefined) {
369
+ if (!Array.isArray(input.setbacks) || input.setbacks.length > 16)
370
+ return undefined;
371
+ out.setbacks = [];
372
+ for (const x of input.setbacks) {
373
+ if (!isObj(x) ||
374
+ !str(x.prospectId) ||
375
+ !str(x.action) ||
376
+ (x.targetId !== undefined && !str(x.targetId)) ||
377
+ (x.placeId !== undefined && !str(x.placeId)))
378
+ return undefined;
379
+ out.setbacks.push({
380
+ prospectId: x.prospectId,
381
+ action: x.action,
382
+ ...(x.targetId !== undefined ? { targetId: x.targetId } : {}),
383
+ ...(x.placeId !== undefined ? { placeId: x.placeId } : {}),
384
+ });
385
+ }
386
+ }
387
+ const act = input.pendingAct;
388
+ if (act !== undefined) {
389
+ if (!isObj(act) ||
390
+ !str(act.action) ||
391
+ !minute(act.at, now) ||
392
+ (act.targetId !== undefined && !str(act.targetId)) ||
393
+ (act.placeId !== undefined && !str(act.placeId)) ||
394
+ !Array.isArray(act.readings) ||
395
+ act.readings.length > PROJECTS_DEFAULTS.maxProspects)
396
+ return undefined;
397
+ const readings = [];
398
+ for (const x of act.readings) {
399
+ if (!isObj(x) || !str(x.prospectId) || !unit(x.progress))
400
+ return undefined;
401
+ readings.push({ prospectId: x.prospectId, progress: x.progress });
402
+ }
403
+ out.pendingAct = {
404
+ action: act.action,
405
+ at: act.at,
406
+ readings,
407
+ ...(act.targetId !== undefined ? { targetId: act.targetId } : {}),
408
+ ...(act.placeId !== undefined ? { placeId: act.placeId } : {}),
409
+ };
410
+ }
271
411
  const seen = new Set();
272
412
  for (const entry of input.prospects.slice(0, PROJECTS_DEFAULTS.maxProspects)) {
273
413
  const prospect = cleanProspect(entry);
@@ -70,7 +70,9 @@ export interface PromptingsNoted {
70
70
  */
71
71
  export declare function notePromptings(p: Person, seen: readonly PromptingSeen[], chosenId: string | null, now: Minute): PromptingsNoted;
72
72
  /** Vocabulary for one prompt under a held reading (narration only; it changes nothing). */
73
- export declare function promptingLine(reading: PromptingReading, outcome: PromptingRecord['outcome'], kind?: PromptingResponseKind): string;
73
+ export declare function promptingLine(reading: PromptingReading, outcome: PromptingRecord['outcome'], kind?: PromptingResponseKind, options?: {
74
+ voice?: 'first' | 'third';
75
+ }): string;
74
76
  /** Read-only summary of recent prompts (a lens; computes nothing new about the person). */
75
77
  export interface PromptingsReadingView {
76
78
  reading: PromptingReading;
@@ -256,7 +256,19 @@ export function notePromptings(p, seen, chosenId, now) {
256
256
  return noted;
257
257
  }
258
258
  /** Vocabulary for one prompt under a held reading (narration only; it changes nothing). */
259
- export function promptingLine(reading, outcome, kind) {
259
+ export function promptingLine(reading, outcome, kind, options = {}) {
260
+ // Plain third-person observations describe the response, without inferring the prompt's origin.
261
+ if (options.voice === 'third') {
262
+ if (outcome === 'assented')
263
+ return 'an urge came and was acted on';
264
+ if (outcome === 'turned')
265
+ return kind === 'refuge'
266
+ ? 'an urge came; the person sought refuge and let it pass'
267
+ : kind === 'redirect'
268
+ ? 'an urge came; the person turned to something else'
269
+ : 'an urge came; the person held back';
270
+ return 'an urge came and passed';
271
+ }
260
272
  if (outcome === 'assented')
261
273
  return reading === 'whisper'
262
274
  ? 'a whisper came and was followed'
@@ -17,6 +17,7 @@ import { temporalGoalValue } from '../agenda/agenda.js';
17
17
  import { trustOf } from '../beliefs/index.js';
18
18
  import { cmpStr, isNum, isObj, round } from '../core/index.js';
19
19
  import { expectedEffect } from '../memory/index.js';
20
+ import { diminishesProject } from '../projects/index.js';
20
21
  import { MAX_MINUTE, MINUTES_PER_DAY } from '../types.js';
21
22
  export const PROSPECTION_DEFAULTS = {
22
23
  maxEntries: 16,
@@ -92,6 +93,10 @@ function consequence(x, receivedAt) {
92
93
  (!Array.isArray(s.advances) ||
93
94
  s.advances.length > 8 ||
94
95
  !s.advances.every((x) => typeof x === 'string' && x.length > 0 && x.length <= 512))) ||
96
+ (s.diminishes !== undefined &&
97
+ (!Array.isArray(s.diminishes) ||
98
+ s.diminishes.length > 8 ||
99
+ !s.diminishes.every((x) => typeof x === 'string' && x.length > 0 && x.length <= 512))) ||
95
100
  (s.focus !== undefined && !unit(s.focus)) ||
96
101
  (s.materialCost !== undefined &&
97
102
  (!isNum(s.materialCost) || s.materialCost < 0 || s.materialCost > PROSPECTION_DEFAULTS.maxMaterialCost)))
@@ -115,6 +120,7 @@ function consequence(x, receivedAt) {
115
120
  duration: s.duration,
116
121
  effort: s.effort,
117
122
  ...(s.advances !== undefined ? { advances: [...s.advances] } : {}),
123
+ ...(s.diminishes !== undefined ? { diminishes: [...s.diminishes] } : {}),
118
124
  ...(s.focus !== undefined ? { focus: s.focus } : {}),
119
125
  ...(s.materialCost !== undefined ? { materialCost: s.materialCost } : {}),
120
126
  },
@@ -262,7 +268,9 @@ function successorOffer(e) {
262
268
  }
263
269
  /** Select a whole route in the exact rounded units cognition emits. @internal */
264
270
  export function receivedGoalRoute(p, aff, now, maturityScale, costs) {
265
- const goals = p.agenda.goals.filter((g) => g.status === 'active' && g.temporal !== undefined);
271
+ const goals = p.agenda.goals.filter((g) => g.status === 'active' &&
272
+ g.temporal !== undefined &&
273
+ !(g.project && diminishesProject(p, aff, g.project.prospectId)));
266
274
  if (!goals.length || !p.prospection?.entries.length)
267
275
  return undefined;
268
276
  const direct = goals.map((g) => temporalGoalValue(p, g, aff, now, now + Math.max(0, aff.duration)));
@@ -297,7 +305,12 @@ export function receivedGoalRoute(p, aff, now, maturityScale, costs) {
297
305
  entry.confidence *
298
306
  reliability(p, entry);
299
307
  const successor = successorOffer(entry);
300
- const nextValues = goals.map((g, i) => Math.max(values[i] ?? 0, nextAccess * beta * temporalGoalValue(p, g, successor, now, end)));
308
+ const nextValues = goals.map((g, i) => {
309
+ const prospectId = g.project?.prospectId;
310
+ return prospectId && [...done, previous, successor].some((a) => diminishesProject(p, a, prospectId))
311
+ ? 0
312
+ : Math.max(values[i] ?? 0, nextAccess * beta * temporalGoalValue(p, g, successor, now, end));
313
+ });
301
314
  const nextCost = accumulatedCost + beta * nextAccess * Math.min(0, costs(successor));
302
315
  const cost = round(nextCost);
303
316
  const total = nextValues.reduce((sum, value) => sum + round(value * maturityScale), 0) + cost;
@@ -42,6 +42,9 @@ export declare function reviewIfDue(p: Person, now: Minute, recent: readonly Day
42
42
  distress: number;
43
43
  } | undefined;
44
44
  /** The person's own words for their latest review, or undefined when they hold no review. */
45
- export declare function reviewLine(p: Person): string | undefined;
45
+ export declare function reviewLine(p: Person, options?: {
46
+ variety?: boolean;
47
+ cause?: 'ordered' | 'interrupted';
48
+ }): string | undefined;
46
49
  /** Restore guard: drop malformed fields, keep valid resolves. */
47
50
  export declare function sanitizeReview(x: unknown, now?: Minute): ReviewState | undefined;
@@ -31,6 +31,7 @@
31
31
  * fast-forward) should still live, or release, that day's occasions, or the review counts each one as missed.
32
32
  */
33
33
  import { clamp01, dexp, isObj, minuteOfDay, round } from '../core/index.js';
34
+ import { pickLine } from '../narrate/lexicon.js';
34
35
  export const REVIEW_DEFAULTS = {
35
36
  /** Default evening review time, minute of day. */
36
37
  at: 21 * 60,
@@ -192,7 +193,7 @@ export function reviewIfDue(p, now, recent, closed) {
192
193
  return { record, distress };
193
194
  }
194
195
  /** The person's own words for their latest review, or undefined when they hold no review. */
195
- export function reviewLine(p) {
196
+ export function reviewLine(p, options = {}) {
196
197
  const st = p.review;
197
198
  const r = st?.last;
198
199
  if (!st || st.style === 'none' || !r)
@@ -200,6 +201,16 @@ export function reviewLine(p) {
200
201
  const list = (xs) => xs.length <= 1 ? (xs[0] ?? '') : `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}`;
201
202
  if (r.missed.length === 0)
202
203
  return r.kept.length > 0 ? `Today I kept ${list(r.kept)}.` : undefined;
204
+ if (options.cause) {
205
+ const cause = options.cause === 'ordered' ? 'following an order' : 'being interrupted';
206
+ const next = options.cause === 'ordered'
207
+ ? 'Next time I will say what the order would put aside.'
208
+ : 'Next time I will make room to return to it.';
209
+ return pickLine('review.cause', `${p.id}:${r.day}`, { cause, missed: list(r.missed), next }, p.lexicon);
210
+ }
211
+ if (options.variety ?? p.lexicon?.variety) {
212
+ return pickLine(`review.${st.style}`, `${p.id}:${r.day}`, { missed: list(r.missed) }, p.lexicon);
213
+ }
203
214
  if (st.style === 'ruminating')
204
215
  return `I keep going back over ${list(r.missed)}.`;
205
216
  return `I didn't keep ${list(r.missed)}. Next time I'll be ready when it comes.`;
package/dist/types.d.ts CHANGED
@@ -373,6 +373,8 @@ export interface Episode {
373
373
  /** 0..1 how memorable; decays and is boosted by recall. */
374
374
  salience: Unit;
375
375
  summary: string;
376
+ /** Host-authored summary spoken back to actorId (second person); never inferred from free text. */
377
+ addressedSummary?: string;
376
378
  tags: string[];
377
379
  /**
378
380
  * The outside voice whose suggestion led to this episode (set on outcomes of assented/complied activities and
@@ -896,7 +898,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
896
898
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
897
899
  * `dutyReviewAt`). Person shape unchanged.
898
900
  */
899
- export declare const ENGINE_VERSION = "2.14.1";
901
+ export declare const ENGINE_VERSION = "2.17.0";
900
902
  /** A pregnancy carried by this person (host-driven; see `family/`). */
901
903
  export interface Pregnancy {
902
904
  fatherId: PersonId;
@@ -1013,6 +1015,8 @@ export interface ReceivedConsequence {
1013
1015
  effort: Unit;
1014
1016
  focus?: Unit;
1015
1017
  materialCost?: number;
1018
+ /** Prospect ids this successor reduces; read only with projects protectProgress. */
1019
+ diminishes?: string[];
1016
1020
  /** Goal or prospect ids this successor contributes to (bounded to 8). */
1017
1021
  advances?: string[];
1018
1022
  };
@@ -1186,6 +1190,8 @@ export interface Affordance {
1186
1190
  * earliest-ending match, explicit or by action, as before.
1187
1191
  */
1188
1192
  fulfills?: string[];
1193
+ /** Prospect ids this offer reduces; read only with projects protectProgress. */
1194
+ diminishes?: string[];
1189
1195
  /** Goal ids this advances. */
1190
1196
  advances?: string[];
1191
1197
  /** Probability of harm and its severity, as advertised. */
@@ -1251,6 +1257,8 @@ export interface Percept {
1251
1257
  aboutId?: PersonId;
1252
1258
  };
1253
1259
  summary: string;
1260
+ /** Host-authored summary spoken back to actorId (second person); never inferred from free text. */
1261
+ addressedSummary?: string;
1254
1262
  /**
1255
1263
  * Host flag for `sim/` interrupts. true: this percept is near the person (the framework has no spatial model),
1256
1264
  * so a salient one forces re-decision even if it does not target them. false: never interrupts.
@@ -1759,6 +1767,12 @@ export interface VoiceConflict {
1759
1767
  * slots documented next to each key.
1760
1768
  */
1761
1769
  export interface Lexicon {
1770
+ /** Include person identity and stable temperament in assent/deferral selection (no RNG draws). */
1771
+ variety?: boolean;
1772
+ /** What material gains mean here, e.g. 'food', 'household keep' or 'resources'; absent keeps legacy money lines. */
1773
+ materialMeaning?: string;
1774
+ /** Treat action labels as noun phrases unless explicit verb forms are supplied. */
1775
+ nounActions?: boolean;
1762
1776
  /** Informational locale tag, e.g. 'en', 'tr'. */
1763
1777
  locale?: string;
1764
1778
  /** Display names by entity id: { ana: 'Ana', ben: 'Ben' }. */
@@ -1987,6 +2001,8 @@ export interface Prospect {
1987
2001
  progress?: Unit;
1988
2002
  }
1989
2003
  export interface ProjectsSpec {
2004
+ /** Learn and respect declared reductions of a held project. Absent preserves existing choice. */
2005
+ protectProgress?: boolean;
1990
2006
  forgetMinutes?: number;
1991
2007
  calmUrgency?: Unit;
1992
2008
  fearBelow?: Unit;
@@ -2008,6 +2024,24 @@ export interface ProjectsState {
2008
2024
  /** Absent retains 2.14.0 credit. Re-enabling, including on every load, opts into net credit. */
2009
2025
  behaviour?: 2;
2010
2026
  spec?: ProjectsSpec;
2027
+ /** Exact contextual acts learned to reduce a prospect; oldest entries are evicted. */
2028
+ setbacks?: {
2029
+ prospectId: string;
2030
+ action: string;
2031
+ targetId?: EntityId;
2032
+ placeId?: EntityId;
2033
+ }[];
2034
+ /** Last completed act and attended progress before it; consumed by the next matching perception. */
2035
+ pendingAct?: {
2036
+ action: string;
2037
+ targetId?: EntityId;
2038
+ placeId?: EntityId;
2039
+ at: Minute;
2040
+ readings: {
2041
+ prospectId: string;
2042
+ progress: Unit;
2043
+ }[];
2044
+ };
2011
2045
  prospects: (Prospect & {
2012
2046
  lastSeenAt: Minute;
2013
2047
  })[];
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.14.1';
123
+ export const ENGINE_VERSION = '2.17.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "2.16.0",
3
+ "version": "2.17.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",