kaldamus entity reference · Summaries · Scripts · Prophecies
Three narrative artifacts and one cost receipt, sharing a single backend-agnostic generator pattern — no dedup, no join table, no corpus-wide identity resolution anywhere in this family.
| Entity | id | Holds | On rewrite |
|---|---|---|---|
| EventSummary | event_id | significance · causes · outcomes (LLM prose) | whole row replaced |
| EventScript | event_id | anchor-desk narration + precedents | whole row replaced |
| Prophecy | slugify(category) — not an event id | per-category forecast prose | whole row replaced |
| GenerationStats | event_id / category / date — shared | {gen_ms, gen_tokens} per artifact | slot overwritten |
generate(event, client, cfg) → one rowsignificance rewritten whole each run{gen_ms, gen_tokens} per artifact slot
flowchart TD
classDef srcNode fill:#16233b,stroke:#5a8fd4,stroke-width:1.5px,color:#dfe7f5;
classDef llmNode fill:#161822,stroke:#e0e0e0,stroke-width:2px,color:#f0f0f0;
classDef decisionNode fill:#12151f,stroke:#5a6178,stroke-width:1px,color:#c9cbd6;
classDef deadEnd fill:#12151a,stroke:#3a4058,stroke-width:1px,color:#7a8296,stroke-dasharray:3 3;
classDef doneNode fill:#0e1a2e,stroke:#5a8fd4,stroke-width:1.5px,color:#dfe7f5;
A["events.json"]:::srcNode --> B{"priority gate
sorted, capped"}:::decisionNode
B -- below --> Z1["excluded this run"]:::deadEnd
B -- selected --> C{"script only:
EventSummary row exists?"}:::decisionNode
C -- "no summary on disk" --> Z2["skip + warn —
never co-loads a 2nd model"]:::deadEnd
C -- "summary exists, or n/a" --> D{"cascade fresh?
event.enriched_at
(+ summary.generated_at for scripts)"}:::decisionNode
D -- fresh --> Z3["skipped"]:::deadEnd
D -- "stale (or --force)" --> E["build_chat_client(cfg)
once per job, reused"]:::llmNode
E --> F["generate(event[, summary], client, cfg)
one LLM call, stateless"]:::llmNode
F --> G{"response parses?"}:::decisionNode
G -- no --> H["error logged,
prior row untouched"]:::deadEnd
G -- yes --> I["whole-row upsert:
entity row + generation_stats slot"]:::doneNode
I --> J["event_summaries.json /
event_scripts.json"]:::doneNode
One selected event, one job. Blue = the event-derived path summary and script share. Prophecy replaces the top of this diagram with a per-category sample.
enriched_at + summary.generated_atgenerate() → upsert(event_id)| Entity | Cascade upstream | TTL |
|---|---|---|
| EventSummary | event.enriched_at | 7-day |
| EventScript | event.enriched_at and summary.generated_at | 7-day |
| Prophecy | max(sampled enriched_at) and event_sample_hash | none — cascade alone |
event_sample_hash unchanged → fresh
stateDiagram-v2
direction LR
classDef freshState fill:#16233b,stroke:#5a8fd4,stroke-width:1.5px,color:#dfe7f5;
classDef warnState fill:#231a0c,stroke:#c98500,stroke-width:1.5px,color:#f0c968;
classDef deadState fill:#12151a,stroke:#3a4058,stroke-width:1px,color:#8892a4;
[*] --> Fresh: first successful generate call
Fresh --> Fresh: regenerated on next
cascade/TTL miss (row replaced whole)
Fresh --> Stale: event.enriched_at moves
(script also watches summary.generated_at)
Stale --> Fresh: next successful run
Fresh --> Orphaned: event_id rotates
out of events.json
Orphaned --> Purged: --purge-orphans
(nightly)
Purged --> [*]
class Fresh freshState
class Stale warnState
class Orphaned warnState
class Purged deadState
Applies to EventSummary and EventScript. Prophecy never enters this diagram — it has no Orphaned state.
--purge-orphans, run after update-events| Entity | id shape | Can orphan? |
|---|---|---|
| EventSummary | event_id | yes |
| EventScript | event_id | yes |
| Prophecy | category slug | no |
| GenerationStats | event_id / category / date | yes — and nothing purges it |
id = slugify("politics"), same slugs every runfinance-crypto prophecy slot lingers (excluded category)