kaldamus entity reference · Summaries · Scripts · Prophecies

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.

01

Three narratives, one receipt

EntityidHoldsOn rewrite
EventSummaryevent_idsignificance · causes · outcomes (LLM prose)whole row replaced
EventScriptevent_idanchor-desk narration + precedentswhole row replaced
Prophecyslugify(category)not an event idper-category forecast prosewhole row replaced
GenerationStatsevent_id / category / date — shared{gen_ms, gen_tokens} per artifactslot overwritten
Concept
Example
Three narratives — Summary, Script, Prophecy — plus one cost sidecar, all sharing one stateless generator: one LLM response in, one whole row out. No dedup, no join table, no corpus-wide identity resolution.
generate(event, client, cfg) one row
Every field is either copied from the event or written fresh by the LLM this run — nothing behaves differently on a second write than a first.
significance rewritten whole each run
GenerationStats isn't a fourth narrative — it's a cost stamp riding alongside all three (and other jobs), keyed by the same id.
{gen_ms, gen_tokens} per artifact slot
02

How the three jobs run

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.

Concept
Example
Summary and Script share one shape — a priority gate, a cascade+TTL freshness check, one LLM call, one whole-row write. Prophecy swaps the per-event gate for a per-category sample.
summary gate looser than script gate
Script depends structurally on Summary: with no summary row it skips and warns — it never co-loads a second model to backfill.
missing summary script skipped + warned
Freshness is cascade-first; a script clears two upstream timestamps where a summary clears one.
script checks enriched_at + summary.generated_at
No dedup, alias-fold, or join step anywhere — every generate() is a one-shot write to an id the caller already knows.
generate() upsert(event_id)
Prophecy breaks the per-event shape — it samples a few events per country within a category instead.
≤2 events/country, min 3 total
03

How state is managed

EntityCascade upstreamTTL
EventSummaryevent.enriched_at7-day
EventScriptevent.enriched_at and summary.generated_at7-day
Prophecymax(sampled enriched_at) and event_sample_hashnone — cascade alone
Concept
Example
Summary and Script use the plain cascade+TTL rule; Prophecy replaces the TTL entirely with a content hash of the sample it was built from.
event_sample_hash unchanged fresh
A script watches two upstreams, so regenerating a summary alone forces its script to regenerate on the next run.
summary regenerates script now stale
Prophecy needs no absolute clock — the sample hash catches both sample drift and a single-event re-enrichment.
sample changes prophecy regenerates
04

Transient, or permanent?

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.

Concept
Example
Every row in this family is a whole-row overwrite on refresh — never an accumulation. No first-write-wins field, no reconciled counter.
regenerate whole row replaced
Summary and Script can be orphaned when their event rotates out of events.json; a nightly purge clears them.
--purge-orphans, run after update-events
GenerationStats is never purged by anything — it can outlive the entity it was stamped for.
an excluded category's prophecy slot still on disk
05

Three can orphan. One structurally cannot.

Entityid shapeCan orphan?
EventSummaryevent_idyes
EventScriptevent_idyes
Prophecycategory slugno
GenerationStatsevent_id / category / dateyes — and nothing purges it
Concept
Example
Orphaning is a property of depending on something that can vanish. Prophecy can't orphan — its id is a fixed category slug, never tied to an event that could rotate out.
id = slugify("politics"), same slugs every run
Summary and Script depend on their Event row still existing, so they can be left behind when it isn't.
event rotates out summary orphaned
GenerationStats depends on nothing and shares its id across five jobs, so no writer owns cleaning it — which is exactly why it accumulates the most.
finance-crypto prophecy slot lingers (excluded category)

See also