kaldamus entity reference · Factor · EventFactor · Dependency · Observation
Every event that clears the priority gate gets a fresh LLM read of its causal drivers. What survives across runs isn't that reading — it's the canonical Factor those readings keep converging on.
| Entity | id | Holds | Write rule |
|---|---|---|---|
| Factor | slugify(canonical_name) | canonical_name · description · category · grain / geo · indicators[] | first-write-wins |
| EventFactor | {event_id}::{factor_id} | just the two ids + generated_at | replaced wholesale |
Factor (the causal claim) and a disposable EventFactor join (this event asserted it this run).Factor, in which fields may change after the first write; EventFactor is only ids and a timestamp.category, grain, geo are never asked as free text — name is de-kebabed in code, the others inherited or coerced at write time.name = de-kebab(canonical_name)
flowchart TD
classDef srcNode fill:#16233b,stroke:#5a8fd4,stroke-width:1.5px,color:#dfe7f5;
classDef factorNode fill:#241d0c,stroke:#c9a84c,stroke-width:1.5px,color:#f3e6bd;
classDef decisionNode fill:#12151f,stroke:#5a6178,stroke-width:1px,color:#c9cbd6;
classDef llmNode fill:#161822,stroke:#e0e0e0,stroke-width:2px,color:#f0f0f0;
classDef deadEnd fill:#12151a,stroke:#3a4058,stroke-width:1px,color:#7a8296,stroke-dasharray:3 3;
A["events.json"]:::srcNode --> B{"priority gate
sorted, capped"}:::decisionNode
B -- below --> Z1["excluded this run"]:::deadEnd
B -- selected --> C{"EventFactor set fresh?
all rows ≥ enriched_at, within TTL"}:::decisionNode
C -- fresh --> Z2["skipped"]:::deadEnd
C -- "stale / forced" --> D["one LLM call
flat_event_dict + dedup hint"]:::llmNode
D --> E{"response parses?"}:::decisionNode
E -- no --> F["stats stamped,
nothing written"]:::deadEnd
E -- "yes, ≤6 factors" --> G["id = slugify(canonical_name)
folded through alias map"]:::llmNode
G --> H{"id already on disk?"}:::decisionNode
H -- yes --> I["bump last_updated_at only"]
H -- no --> J["insert new Factor row"]
I --> M["replace EventFactor set:
delete old, insert new"]
J --> M
M --> N["reconcile_times_referenced"]:::factorNode
N --> O["factors.json
event_factors.json"]:::factorNode
Every run of update_factors.py, one event at a time. Gold = the canonical corpus; blue = the raw event input.
slugify(name) → alias map| Row / field | Written by | Changes again when… |
|---|---|---|
| canonical_name, description, category, grain, geo | the first successful discovery of that id | never automatically — only a manual delete + rerun |
| last_updated_at | every successful re-discovery hit | bumped each hit; left untouched by backfills, so age reporting stays honest |
| times_referenced | nobody, directly | recomputed from event_factors.json every run — never incremented |
| EventFactor set (one event) | that event's most recent call | replaced wholesale next run — old set deleted first, nothing merged |
Factor's description is written by exactly one model call, ever.times_referenced is recomputed from the join table each run, never incremented — an earlier increment-on-write scheme double-counted re-discoveries.
stateDiagram-v2
direction LR
classDef factorState fill:#241d0c,stroke:#c9a84c,stroke-width:1.5px,color:#f3e6bd;
classDef warnState fill:#231a0c,stroke:#c98500,stroke-width:1.5px,color:#f0c968;
classDef deadState fill:#12151a,stroke:#3a4058,stroke-width:1px,color:#8892a4;
[*] --> Stable: first LLM emission
of this canonical_name
Stable --> Stable: re-discovered by another event
(first-write-wins)
Stable --> Orphaned: last referencing EventFactor
retired elsewhere
Orphaned --> Stable: asserted again by a
new or returning event
Orphaned --> Purged: --purge-orphans
(manual, not in update-all)
Stable --> Merged: merge_factors.py folds id
into a curated target
Purged --> [*]
Merged --> [*]
class Stable factorState
class Orphaned warnState
class Purged deadState
class Merged deadState
A Factor row's full lifecycle. There is no automatic path out of Orphaned — it requires a human to run the purge.
Factor is built to last; the EventFactor evidence that it applies to a given event is rebuilt from scratch each cycle.Factor — --purge-orphans is manual and not wired into update-all — so zero-join factors accumulate.Illustrative: one Factor, persisted category: "Legal" (the gold-ringed chip — set once by whichever event discovered it first), joined to events sitting under every other category. None of those categories feed back into the row.
category is provenance, not membership — set once from the event that discovered it first, then never updated, even as the factor joins events across most other categories."Legal", reaches events in ~nine categoriesEventFactor join graph, which the Factor row never summarizes.grain/geo are the deliberate version: the LLM self-declares event-specific / macro-country / macro-global, feeding the news ranker's hub penalty.macro-global = rates, oil, global equitiesgrain is under-applied — a backfill promotes high-degree factors to macro-global, but a heavily-joined factor can still sit tagged event-specific.| EventDependency field | Note |
|---|---|
| id | {event_id}::{depends_on_event_id} — composite by construction |
| kind | "hard" (must occur first) or "soft" (materially enables) |
| description | the LLM's stated reason for the link |
| generated_at | this run's timestamp |
EventDependency ("what has to happen first?") and FactorObservation ("what's the real-world number?").EventDependency runs a closed-world pass — the candidate list of other open markets is the only permitted value; off-list, self, or empty is dropped.depends_on must be a listed candidateKXFDAAPPROVE-GED → KXGEDATOLISIB-26 (hard)[] replaces the set (mirror of §3).[], slot marks "checked, none"FactorObservation is the one network stage: one row per cited (source, series_id) with a value, a short rolling buffer, and a computed trend.