@its-not-rocket-science/ananke 0.1.17 → 0.1.22

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/CHANGELOG.md CHANGED
@@ -6,6 +6,118 @@ Versioning follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.1.22] — 2026-03-26
10
+
11
+ ### Added
12
+
13
+ - **Phase 77 · Dynasty & Succession** (`src/succession.ts`)
14
+ - `SuccessionRuleType`: `"primogeniture" | "renown_based" | "election"`.
15
+ - `SuccessionCandidate { entityId, kinshipDegree, renown_Q, inheritedRenown_Q, claimStrength_Q }`.
16
+ - `SuccessionResult { heirId, candidates, rule, stabilityImpact_Q }` — signed Q stability delta.
17
+ - `findSuccessionCandidates(lineage, deceasedId, renownRegistry, maxDegree?)` — BFS over family graph (Phase 76), computes `renown_Q` and `inheritedRenown_Q` per candidate.
18
+ - `resolveSuccession(lineage, deceasedId, renownRegistry, rule, worldSeed, tick)` → `SuccessionResult`:
19
+ - **primogeniture**: first-born child (lowest entityId) gets SCALE.Q claim; others by distance.
20
+ - **renown_based**: claim = 70% own renown + 30% inherited renown.
21
+ - **election**: renown-weighted deterministic lottery via `eventSeed`.
22
+ - Stability: `+STABILITY_CLEAN_SUCCESSION_Q` for uncontested direct heir; `−STABILITY_DISTANT_HEIR_Q` per extra degree; `−STABILITY_CONTESTED_Q` when top-two gap < q(0.10); `−STABILITY_NO_HEIR_Q` if no candidates.
23
+ - `applySuccessionToPolity(polity, result)` — applies `stabilityImpact_Q` to `polity.stabilityQ` (clamped).
24
+ - Added `./succession` subpath export to `package.json`.
25
+ - 21 new tests; 4,142 total. Coverage maintained above all thresholds.
26
+
27
+ ---
28
+
29
+ ## [0.1.21] — 2026-03-26
30
+
31
+ ### Added
32
+
33
+ - **Phase 76 · Kinship & Lineage** (`src/kinship.ts`)
34
+ - `LineageNode { entityId, parentIds, childIds, partnerIds }` — family links per entity.
35
+ - `LineageRegistry { nodes: Map<number, LineageNode> }` — flat registry, no Entity field changes.
36
+ - `createLineageRegistry()` / `getLineageNode(registry, entityId)` — factory and lazy-init accessor.
37
+ - `recordBirth(registry, childId, parentAId, parentBId?)` — links child to 1–2 parents; idempotent.
38
+ - `recordPartnership(registry, entityAId, entityBId)` — mutual partner link; idempotent.
39
+ - `getParents / getChildren / getSiblings` — direct family queries; siblings deduplicated.
40
+ - `findAncestors(registry, entityId, maxDepth?)` — BFS upward through parent links (default depth 4).
41
+ - `computeKinshipDegree(registry, entityA, entityB)` — BFS on undirected family graph (parents + children + partners); returns 0–4 or `null` beyond `MAX_KINSHIP_DEPTH = 4`.
42
+ - `isKin(registry, entityA, entityB, maxDegree?)` — convenience boolean.
43
+ - `getKinshipLabel(degree)` → `"self" | "immediate" | "close" | "extended" | "distant" | "unrelated"`.
44
+ - `computeInheritedRenown(lineage, entityId, renownRegistry, maxDepth?)` — sums ancestor `renown_Q` with geometric decay (`RENOWN_DEPTH_DECAY_Q = q(0.50)` per generation); clamped to SCALE.Q.
45
+ - Added `./kinship` subpath export to `package.json`.
46
+ - 42 new tests; 4,121 total. Coverage maintained above all thresholds.
47
+
48
+ ---
49
+
50
+ ## [0.1.20] — 2026-03-26
51
+
52
+ ### Added
53
+
54
+ - **Phase 75 · Entity Renown & Legend Registry** (`src/renown.ts`)
55
+ - `RenownRecord { entityId, renown_Q, infamy_Q, entries: LegendEntry[] }` — per-entity reputation on two orthogonal axes.
56
+ - `LegendEntry { entryId, tick, eventType, significance }` — lightweight reference to a significant `ChronicleEntry`.
57
+ - `RenownRegistry { records: Map<number, RenownRecord> }` — flat registry, one record per entity.
58
+ - `createRenownRegistry()` / `getRenownRecord(registry, entityId)` — factory and lazy-init accessor.
59
+ - `updateRenownFromChronicle(registry, chronicle, entityId, minSignificance?)` — idempotent scan; renown events (legendary_deed, quest_completed, combat_victory, masterwork_crafted, rank_promotion, settlement_founded, first_contact) add to `renown_Q`; infamy events (relationship_betrayal, settlement_raided, settlement_destroyed, quest_failed) add to `infamy_Q`; both capped at SCALE.Q.
60
+ - `getRenownLabel(renown_Q)` → `"unknown" | "noted" | "known" | "renowned" | "legendary" | "mythic"` (6 tiers at q(0.10) boundaries).
61
+ - `getInfamyLabel(infamy_Q)` → `"innocent" | "suspect" | "notorious" | "infamous" | "reviled" | "condemned"`.
62
+ - `deriveFactionStandingAdjustment(renown_Q, infamy_Q, allianceBias)` — signed Q adjustment; heroic factions (bias=1.0) reward renown and punish infamy; criminal factions (bias=0.0) the reverse; clamped to [-SCALE.Q, SCALE.Q].
63
+ - `getTopLegendEntries(record, n)` — top N entries by significance (tick-descending tie-break).
64
+ - `renderLegendWithTone(record, entryMap, ctx, maxEntries?)` — renders top entries as prose via Phase 74's `renderEntryWithTone`.
65
+ - Added `./narrative-prose` and `./renown` subpath exports to `package.json`.
66
+ - 42 new tests; 4,079 total. Coverage maintained above all thresholds.
67
+
68
+ ---
69
+
70
+ ## [0.1.19] — 2026-03-26
71
+
72
+ ### Added
73
+
74
+ - **Phase 74 · Simulation Trace → Narrative Prose** (`src/narrative-prose.ts`)
75
+ - 6 prose tones: `neutral | heroic | tragic | martial | spiritual | mercantile`
76
+ - Tone-varied templates for all 19 `ChronicleEventType` values.
77
+ - `deriveNarrativeTone(culture)` — maps dominant `CultureProfile` value → `ProseTone`
78
+ via `VALUE_TONE_MAP` (martial_virtue→martial, spiritual_devotion→spiritual,
79
+ commerce→mercantile, honour→heroic, fatalism→tragic; others fall back to neutral).
80
+ - `mythArchetypeFrame(archetype)` — returns a culturally-flavoured closing phrase for
81
+ each `MythArchetype` (hero, monster, trickster, great_plague, divine_wrath, golden_age).
82
+ - `createNarrativeContext(entityNames, culture?, myth?)` — bundles tone + name map + myth frame.
83
+ - `renderEntryWithTone(entry, ctx)` — picks the tone variant for each event, substitutes
84
+ `{name}`, `{target}`, computed helper strings (`{cause_str}`, `{location_str}`, etc.),
85
+ raw `entry.variables`, and appends the myth frame (replacing terminal period).
86
+ - `renderChronicleWithTone(chronicle, ctx, minSignificance?)` — filters by significance,
87
+ sorts chronologically, maps via `renderEntryWithTone`.
88
+ - **Success criterion met:** martial, spiritual, and mercantile tones produce clearly
89
+ distinguishable prose from the same chronicle events.
90
+ - 39 new tests; 4,037 total. Coverage: statements 96.81%, branches 86.87%, functions 94.80%.
91
+
92
+ ---
93
+
94
+ ## [0.1.18] — 2026-03-26
95
+
96
+ ### Added
97
+
98
+ - **CE-18 · External Agent Interface** (`tools/agent-server.ts`)
99
+ - WebSocket server (default port 3001) implementing an agent observation/action loop
100
+ over the existing `stepWorld` kernel — no src/ changes, no new npm exports.
101
+ - **Protocol:**
102
+ - Client → `{ type: "step", commands?: AgentCommand[] }` or `{ type: "reset" }`
103
+ - Server → `{ type: "obs", tick, entities: ObservationSlice[], done, winner? }`
104
+ - On connect → `{ type: "init", config, obs }`
105
+ - **`ObservationSlice`** — safe subset: position, velocity, fatigue, shock/consciousness/dead,
106
+ detected nearby enemies (filtered via Phase 52 `canDetect`). No raw internals exposed.
107
+ - **`AgentCommand`** — validated high-level actions: `attack | move | dodge | flee | idle`.
108
+ Invalid team targeting silently dropped; `decideCommandsForEntity` fills in missing commands.
109
+ - Configurable scenario: `TEAM1_SIZE` / `TEAM2_SIZE` (1–4 each), `SEED`, `MAX_TICKS` via env vars.
110
+ Default: 1v1, Knight (longsword + mail) vs Brawler (club).
111
+ - Agent-driven stepping: server advances only when client sends `step` — agent controls tick rate.
112
+ - Determinism preserved: external commands injected via existing `CommandMap` before `stepWorld`.
113
+ - HTTP endpoints: `GET /config`, `GET /status`, `POST /reset`.
114
+ - Run: `npm run agent-server`
115
+ - **Success criterion met:** An external Python script using only `websockets` can drive a single
116
+ entity through a 1v1 fight, receiving `ObservationSlice` observations each tick and submitting
117
+ `attack` / `move` commands, without importing any Ananke TypeScript.
118
+
119
+ ---
120
+
9
121
  ## [0.1.17] — 2026-03-26
10
122
 
11
123
  ### Added
@@ -0,0 +1,92 @@
1
+ import type { RenownRegistry } from "./renown.js";
2
+ import type { Q } from "./units.js";
3
+ /** A single entity's family links within the lineage graph. */
4
+ export interface LineageNode {
5
+ entityId: number;
6
+ /** 0, 1, or 2 biological parent IDs. */
7
+ parentIds: number[];
8
+ /** All children recorded via `recordBirth`. */
9
+ childIds: number[];
10
+ /** All recorded partners (may grow over time). */
11
+ partnerIds: number[];
12
+ }
13
+ /** Registry of all lineage nodes, keyed by entityId. */
14
+ export interface LineageRegistry {
15
+ nodes: Map<number, LineageNode>;
16
+ }
17
+ /** Human-readable kinship label derived from `computeKinshipDegree`. */
18
+ export type KinshipLabel = "self" | "immediate" | "close" | "extended" | "distant" | "unrelated";
19
+ /** Maximum BFS depth for kinship searches; beyond this entities are "unrelated". */
20
+ export declare const MAX_KINSHIP_DEPTH = 4;
21
+ /**
22
+ * Depth-decay factor for inherited renown.
23
+ * Each generation reduces the renown contribution by this fraction:
24
+ * depth 1 (parent) → q(0.50) × parent renown
25
+ * depth 2 (grandparent) → q(0.25) × grandparent renown
26
+ */
27
+ export declare const RENOWN_DEPTH_DECAY_Q: Q;
28
+ export declare function createLineageRegistry(): LineageRegistry;
29
+ /**
30
+ * Return the `LineageNode` for `entityId`, creating a root node (no parents,
31
+ * no children, no partners) if one does not yet exist.
32
+ */
33
+ export declare function getLineageNode(registry: LineageRegistry, entityId: number): LineageNode;
34
+ /**
35
+ * Register a birth: create a node for `childId` and link it to up to two parents.
36
+ * Parent nodes are created if they do not already exist.
37
+ * No-op if `childId` already has a node (idempotent).
38
+ */
39
+ export declare function recordBirth(registry: LineageRegistry, childId: number, parentAId: number, parentBId?: number): void;
40
+ /**
41
+ * Record a partnership between two entities.
42
+ * Partners are considered degree-1 kin (immediate).
43
+ * Idempotent: duplicate calls are safe.
44
+ */
45
+ export declare function recordPartnership(registry: LineageRegistry, entityAId: number, entityBId: number): void;
46
+ /** Return the parent IDs of `entityId` (0–2 elements). */
47
+ export declare function getParents(registry: LineageRegistry, entityId: number): number[];
48
+ /** Return the child IDs of `entityId`. */
49
+ export declare function getChildren(registry: LineageRegistry, entityId: number): number[];
50
+ /**
51
+ * Return the sibling IDs of `entityId` — entities that share at least one parent,
52
+ * excluding `entityId` itself.
53
+ */
54
+ export declare function getSiblings(registry: LineageRegistry, entityId: number): number[];
55
+ /**
56
+ * Return all ancestors of `entityId` within `maxDepth` generations.
57
+ * Uses BFS upward through parent links only.
58
+ */
59
+ export declare function findAncestors(registry: LineageRegistry, entityId: number, maxDepth?: number): Set<number>;
60
+ /**
61
+ * Compute the degree of kinship between two entities via BFS on the undirected
62
+ * family graph (parents, children, and partners are all degree-1 neighbours).
63
+ *
64
+ * Returns:
65
+ * - `0` if `entityA === entityB`
66
+ * - `1`–`MAX_KINSHIP_DEPTH` for kin within range
67
+ * - `null` if no path exists within `MAX_KINSHIP_DEPTH`
68
+ */
69
+ export declare function computeKinshipDegree(registry: LineageRegistry, entityA: number, entityB: number): number | null;
70
+ /** Whether two entities are kin within `maxDegree` (default `MAX_KINSHIP_DEPTH`). */
71
+ export declare function isKin(registry: LineageRegistry, entityA: number, entityB: number, maxDegree?: number): boolean;
72
+ /**
73
+ * Map a numeric kinship degree (or `null`) to a `KinshipLabel`.
74
+ *
75
+ * @param degree Result of `computeKinshipDegree`; pass `null` for unrelated.
76
+ */
77
+ export declare function getKinshipLabel(degree: number | null): KinshipLabel;
78
+ /**
79
+ * Compute the renown bonus an entity inherits from their ancestors.
80
+ *
81
+ * For each ancestor at depth d, contribution = `ancestor.renown_Q × decay^d`
82
+ * where `decay = RENOWN_DEPTH_DECAY_Q / SCALE.Q` (default 0.5 per generation).
83
+ * The sum is clamped to `[0, SCALE.Q]`.
84
+ *
85
+ * Entities with no renown records or no ancestors return 0.
86
+ *
87
+ * @param registry Lineage registry.
88
+ * @param entityId Entity whose ancestors are being summed.
89
+ * @param renownRegistry Phase 75 renown registry.
90
+ * @param maxDepth How many generations to look back (default 3).
91
+ */
92
+ export declare function computeInheritedRenown(lineage: LineageRegistry, entityId: number, renownRegistry: RenownRegistry, maxDepth?: number): Q;
@@ -0,0 +1,234 @@
1
+ // src/kinship.ts — Phase 76: Kinship & Lineage
2
+ //
3
+ // Tracks parent-child-partner links for entities and provides ancestry queries,
4
+ // degree-of-kinship computation, and inherited renown from the Phase 75 system.
5
+ //
6
+ // Design:
7
+ // - Separate from Entity — the lineage graph lives in a `LineageRegistry`,
8
+ // not in entity fields, so no kernel changes are required.
9
+ // - `computeKinshipDegree` uses BFS over the undirected family graph (parents,
10
+ // children, partners counted at degree 1).
11
+ // - `inheritedRenown` sums ancestor renown_Q with geometric depth-decay so that
12
+ // a mythic grandparent grants a modest but real reputation bonus.
13
+ // - Deterministic: no Math.random(); only pure data queries.
14
+ import { q, SCALE, clampQ } from "./units.js";
15
+ /** Maximum BFS depth for kinship searches; beyond this entities are "unrelated". */
16
+ export const MAX_KINSHIP_DEPTH = 4;
17
+ /**
18
+ * Depth-decay factor for inherited renown.
19
+ * Each generation reduces the renown contribution by this fraction:
20
+ * depth 1 (parent) → q(0.50) × parent renown
21
+ * depth 2 (grandparent) → q(0.25) × grandparent renown
22
+ */
23
+ export const RENOWN_DEPTH_DECAY_Q = q(0.50);
24
+ // ── Factory ───────────────────────────────────────────────────────────────────
25
+ export function createLineageRegistry() {
26
+ return { nodes: new Map() };
27
+ }
28
+ // ── Node access ───────────────────────────────────────────────────────────────
29
+ /**
30
+ * Return the `LineageNode` for `entityId`, creating a root node (no parents,
31
+ * no children, no partners) if one does not yet exist.
32
+ */
33
+ export function getLineageNode(registry, entityId) {
34
+ let node = registry.nodes.get(entityId);
35
+ if (!node) {
36
+ node = { entityId, parentIds: [], childIds: [], partnerIds: [] };
37
+ registry.nodes.set(entityId, node);
38
+ }
39
+ return node;
40
+ }
41
+ // ── Mutation helpers ──────────────────────────────────────────────────────────
42
+ /**
43
+ * Register a birth: create a node for `childId` and link it to up to two parents.
44
+ * Parent nodes are created if they do not already exist.
45
+ * No-op if `childId` already has a node (idempotent).
46
+ */
47
+ export function recordBirth(registry, childId, parentAId, parentBId) {
48
+ // Ensure child node exists with the given parents
49
+ const existing = registry.nodes.get(childId);
50
+ if (!existing) {
51
+ const parentIds = parentBId != null
52
+ ? [parentAId, parentBId]
53
+ : [parentAId];
54
+ registry.nodes.set(childId, { entityId: childId, parentIds, childIds: [], partnerIds: [] });
55
+ }
56
+ // Ensure parent nodes exist and include childId
57
+ const nodeA = getLineageNode(registry, parentAId);
58
+ if (!nodeA.childIds.includes(childId))
59
+ nodeA.childIds.push(childId);
60
+ if (parentBId != null) {
61
+ const nodeB = getLineageNode(registry, parentBId);
62
+ if (!nodeB.childIds.includes(childId))
63
+ nodeB.childIds.push(childId);
64
+ }
65
+ }
66
+ /**
67
+ * Record a partnership between two entities.
68
+ * Partners are considered degree-1 kin (immediate).
69
+ * Idempotent: duplicate calls are safe.
70
+ */
71
+ export function recordPartnership(registry, entityAId, entityBId) {
72
+ const nodeA = getLineageNode(registry, entityAId);
73
+ const nodeB = getLineageNode(registry, entityBId);
74
+ if (!nodeA.partnerIds.includes(entityBId))
75
+ nodeA.partnerIds.push(entityBId);
76
+ if (!nodeB.partnerIds.includes(entityAId))
77
+ nodeB.partnerIds.push(entityAId);
78
+ }
79
+ // ── Family queries ────────────────────────────────────────────────────────────
80
+ /** Return the parent IDs of `entityId` (0–2 elements). */
81
+ export function getParents(registry, entityId) {
82
+ return registry.nodes.get(entityId)?.parentIds ?? [];
83
+ }
84
+ /** Return the child IDs of `entityId`. */
85
+ export function getChildren(registry, entityId) {
86
+ return registry.nodes.get(entityId)?.childIds ?? [];
87
+ }
88
+ /**
89
+ * Return the sibling IDs of `entityId` — entities that share at least one parent,
90
+ * excluding `entityId` itself.
91
+ */
92
+ export function getSiblings(registry, entityId) {
93
+ const parents = getParents(registry, entityId);
94
+ const result = new Set();
95
+ for (const pid of parents) {
96
+ for (const sibId of getChildren(registry, pid)) {
97
+ if (sibId !== entityId)
98
+ result.add(sibId);
99
+ }
100
+ }
101
+ return [...result];
102
+ }
103
+ /**
104
+ * Return all ancestors of `entityId` within `maxDepth` generations.
105
+ * Uses BFS upward through parent links only.
106
+ */
107
+ export function findAncestors(registry, entityId, maxDepth = MAX_KINSHIP_DEPTH) {
108
+ const ancestors = new Set();
109
+ const queue = [{ id: entityId, depth: 0 }];
110
+ while (queue.length > 0) {
111
+ const item = queue.shift();
112
+ if (item.depth >= maxDepth)
113
+ continue;
114
+ for (const pid of getParents(registry, item.id)) {
115
+ if (!ancestors.has(pid)) {
116
+ ancestors.add(pid);
117
+ queue.push({ id: pid, depth: item.depth + 1 });
118
+ }
119
+ }
120
+ }
121
+ return ancestors;
122
+ }
123
+ /**
124
+ * Compute the degree of kinship between two entities via BFS on the undirected
125
+ * family graph (parents, children, and partners are all degree-1 neighbours).
126
+ *
127
+ * Returns:
128
+ * - `0` if `entityA === entityB`
129
+ * - `1`–`MAX_KINSHIP_DEPTH` for kin within range
130
+ * - `null` if no path exists within `MAX_KINSHIP_DEPTH`
131
+ */
132
+ export function computeKinshipDegree(registry, entityA, entityB) {
133
+ if (entityA === entityB)
134
+ return 0;
135
+ const visited = new Set([entityA]);
136
+ const queue = [{ id: entityA, depth: 0 }];
137
+ while (queue.length > 0) {
138
+ const item = queue.shift();
139
+ if (item.depth >= MAX_KINSHIP_DEPTH)
140
+ continue;
141
+ const node = registry.nodes.get(item.id);
142
+ if (!node)
143
+ continue;
144
+ // BFS over parents + children + partners (undirected)
145
+ const neighbours = [
146
+ ...node.parentIds,
147
+ ...node.childIds,
148
+ ...node.partnerIds,
149
+ ];
150
+ for (const nbr of neighbours) {
151
+ if (nbr === entityB)
152
+ return item.depth + 1;
153
+ if (!visited.has(nbr)) {
154
+ visited.add(nbr);
155
+ queue.push({ id: nbr, depth: item.depth + 1 });
156
+ }
157
+ }
158
+ }
159
+ return null; // unrelated within MAX_KINSHIP_DEPTH
160
+ }
161
+ /** Whether two entities are kin within `maxDegree` (default `MAX_KINSHIP_DEPTH`). */
162
+ export function isKin(registry, entityA, entityB, maxDegree = MAX_KINSHIP_DEPTH) {
163
+ const degree = computeKinshipDegree(registry, entityA, entityB);
164
+ return degree !== null && degree <= maxDegree;
165
+ }
166
+ // ── Label ─────────────────────────────────────────────────────────────────────
167
+ /**
168
+ * Map a numeric kinship degree (or `null`) to a `KinshipLabel`.
169
+ *
170
+ * @param degree Result of `computeKinshipDegree`; pass `null` for unrelated.
171
+ */
172
+ export function getKinshipLabel(degree) {
173
+ if (degree === null)
174
+ return "unrelated";
175
+ if (degree === 0)
176
+ return "self";
177
+ if (degree === 1)
178
+ return "immediate";
179
+ if (degree === 2)
180
+ return "close";
181
+ if (degree === 3)
182
+ return "extended";
183
+ if (degree <= MAX_KINSHIP_DEPTH)
184
+ return "distant";
185
+ return "unrelated";
186
+ }
187
+ // ── Inherited renown ──────────────────────────────────────────────────────────
188
+ /**
189
+ * Compute the renown bonus an entity inherits from their ancestors.
190
+ *
191
+ * For each ancestor at depth d, contribution = `ancestor.renown_Q × decay^d`
192
+ * where `decay = RENOWN_DEPTH_DECAY_Q / SCALE.Q` (default 0.5 per generation).
193
+ * The sum is clamped to `[0, SCALE.Q]`.
194
+ *
195
+ * Entities with no renown records or no ancestors return 0.
196
+ *
197
+ * @param registry Lineage registry.
198
+ * @param entityId Entity whose ancestors are being summed.
199
+ * @param renownRegistry Phase 75 renown registry.
200
+ * @param maxDepth How many generations to look back (default 3).
201
+ */
202
+ export function computeInheritedRenown(lineage, entityId, renownRegistry, maxDepth = 3) {
203
+ // BFS upward through parent links only (not children/partners)
204
+ let total = 0;
205
+ const visited = new Set([entityId]);
206
+ const queue = [];
207
+ for (const pid of getParents(lineage, entityId)) {
208
+ if (!visited.has(pid)) {
209
+ visited.add(pid);
210
+ queue.push({ id: pid, depth: 1 });
211
+ }
212
+ }
213
+ while (queue.length > 0) {
214
+ const item = queue.shift();
215
+ if (item.depth > maxDepth)
216
+ continue;
217
+ const record = renownRegistry.records.get(item.id);
218
+ if (record && record.renown_Q > 0) {
219
+ // decay^depth applied via repeated integer multiplication
220
+ let contribution = record.renown_Q;
221
+ for (let i = 0; i < item.depth; i++) {
222
+ contribution = Math.round(contribution * RENOWN_DEPTH_DECAY_Q / SCALE.Q);
223
+ }
224
+ total += contribution;
225
+ }
226
+ for (const pid of getParents(lineage, item.id)) {
227
+ if (!visited.has(pid)) {
228
+ visited.add(pid);
229
+ queue.push({ id: pid, depth: item.depth + 1 });
230
+ }
231
+ }
232
+ }
233
+ return clampQ(total, 0, SCALE.Q);
234
+ }
@@ -0,0 +1,58 @@
1
+ import type { ChronicleEntry } from "./chronicle.js";
2
+ import type { Chronicle } from "./chronicle.js";
3
+ import type { CultureProfile } from "./culture.js";
4
+ import type { MythArchetype, Myth } from "./mythology.js";
5
+ /**
6
+ * Voice tone used when rendering chronicle entries.
7
+ * Derived from the dominant cultural values of the originating polity.
8
+ */
9
+ export type ProseTone = "neutral" | "heroic" | "tragic" | "martial" | "spiritual" | "mercantile";
10
+ /**
11
+ * Context bundle for a tone-aware rendering pass.
12
+ * Created by `createNarrativeContext` and passed to render functions.
13
+ */
14
+ export interface NarrativeContext {
15
+ /** Map of entity id → display name. Missing ids fall back to "entity {id}". */
16
+ entityNames: Map<number, string>;
17
+ /** Prose tone for this rendering pass. */
18
+ tone: ProseTone;
19
+ /**
20
+ * Optional myth-archetype closing phrase appended to each rendered sentence.
21
+ * Produced by `mythArchetypeFrame(archetype)`.
22
+ */
23
+ mythFrame?: string;
24
+ }
25
+ /** Returns a closing phrase appropriate to the myth archetype. */
26
+ export declare function mythArchetypeFrame(archetype: MythArchetype): string;
27
+ /**
28
+ * Derive the best matching `ProseTone` from a `CultureProfile`.
29
+ *
30
+ * Uses the top-ranked cultural value; falls back to `"neutral"` for values
31
+ * without a direct tone mapping (hospitality, hierarchy, innovation, etc.).
32
+ */
33
+ export declare function deriveNarrativeTone(culture: CultureProfile): ProseTone;
34
+ /**
35
+ * Create a `NarrativeContext` for a rendering pass.
36
+ *
37
+ * @param entityNames Map of entity id → display name (numeric ids are looked up here).
38
+ * @param culture Optional culture profile — used to derive tone automatically.
39
+ * @param myth Optional myth — used to append archetype-framing suffix.
40
+ */
41
+ export declare function createNarrativeContext(entityNames: Map<number, string>, culture?: CultureProfile, myth?: Myth): NarrativeContext;
42
+ /**
43
+ * Render a single `ChronicleEntry` with cultural-tone awareness.
44
+ *
45
+ * Selects the tone variant for `entry.eventType`; falls back to `"neutral"` if
46
+ * the requested tone has no specific variant. Appends `ctx.mythFrame` if set.
47
+ *
48
+ * Does NOT mutate `entry.rendered` — call `entry.rendered = renderEntryWithTone(...)`
49
+ * manually if caching is desired.
50
+ */
51
+ export declare function renderEntryWithTone(entry: ChronicleEntry, ctx: NarrativeContext): string;
52
+ /**
53
+ * Render all entries in a `Chronicle` above `minSignificance` (default 50),
54
+ * returned in chronological order.
55
+ *
56
+ * Uses `renderEntryWithTone` for each entry.
57
+ */
58
+ export declare function renderChronicleWithTone(chronicle: Chronicle, ctx: NarrativeContext, minSignificance?: number): string[];