Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
632ebe1
fix(27-quick): count hyphenated compounds on both venues (remove inve…
helloiamvu Jul 2, 2026
5228913
fix(27-quick): add compound_type axis + occurrence classifier
helloiamvu Jul 2, 2026
bfbaadf
fix(27-quick): split fact rows per compound_type + fail-loud closed-c…
helloiamvu Jul 2, 2026
fa7c63d
fix(27-quick): regenerate exported earnings_fact schema for compound_…
helloiamvu Jul 2, 2026
215ecb7
fix(27-quick): share fail-loud form validation with classify_mentions…
helloiamvu Jul 2, 2026
11fb66c
fix(27-quick): exclude acronyms + honor case-sensitivity in closed-ca…
helloiamvu Jul 2, 2026
696ccb0
fix(27-quick): prefix residuals always classify closed, never affix (…
helloiamvu Jul 2, 2026
e5bb561
fix(27-quick): lock firefighter as closed — de-tautologize ambiguity …
helloiamvu Jul 2, 2026
51258d2
fix(27-quick): fail loud on unknown compound_type in venue tallies (r…
helloiamvu Jul 2, 2026
19c4521
fix(27-quick): wire classify_mentions into the live counting path (re…
helloiamvu Jul 2, 2026
79ccf7d
fix(27-quick): project compound_type through the catalog read boundar…
helloiamvu Jul 2, 2026
b9f3bf6
fix(27-quick): scan acronyms case-sensitively in closed-candidate pas…
helloiamvu Jul 2, 2026
da1ca29
fix(27-quick): map engine seconds to offset_seconds, fail loud on bad…
helloiamvu Jul 2, 2026
e1ac3d5
fix(27-quick): scan hyphenated-token components for fused compounds (…
helloiamvu Jul 2, 2026
e33c19b
Merge remote-tracking branch 'origin/main' into phase27/earnings-venu…
helloiamvu Jul 3, 2026
0b7de3f
fix(27-quick): emit FactDelta engine seconds as offset_seconds on the…
helloiamvu Jul 3, 2026
9e949ae
fix(27-quick): fail loud on single-sided naive/numeric live temporals…
helloiamvu Jul 3, 2026
d821dc8
fix(27-quick): re-validate compound_type enum at the FactLedger write…
helloiamvu Jul 3, 2026
ff94d45
docs(27-quick): changelog entry for the venue-count fix
helloiamvu Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to `mostlyright`. The format follows [Keep a Changelog](http

## [Unreleased]

### Fixed

- **Earnings venue-rule compound-word under-count (D-30).** Hyphenated compounds
(e.g. `supply-chain` for `chain`) were dropped from BOTH venue counts by an
inverted guard; they now count on both venues per each issuer's actual rule.
Adds the per-occurrence `compound_type` axis to `schema.earnings_fact.v1`
(`standalone`/`open`/`hyphenated`/`closed`/`affix_derivation` — additive,
nullable): closed compounds (`wildfire` for `fire`) are Kalshi-No +
Polymarket human-review candidates, affix derivations (`joyful` for `joy`)
count for neither venue, and an unknown value fails loud in every venue
tally, at the `FactLedger` durable-write boundary, and through the catalog
read boundary. Live-path parity: the streaming classifier emits one delta
per `(term, compound_type)`; its engine-relative timestamp crosses the SSE
wire as `offset_seconds` (never as the wallclock `spoken_at`), and the
stream consumer fail-louds on any naive/numeric temporal field.

## [1.11.0] — 2026-07-02 — Earnings-mention markets engine + SDK surface (Phase 27)

Minor release. Adds the earnings-mention markets vertical (Kalshi
Expand Down
43 changes: 43 additions & 0 deletions packages/core/src/mostlyright/core/schemas/earnings_fact.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,32 @@
"regex",
)

#: Compound-type axis (D-30; RESEARCH-MARKETS §2.1(3) "Compound words — REAL
#: CROSS-VENUE DIVERGENCE" + §2.2 "Closed-compound divergence"). This is a
#: SEPARATE per-occurrence axis from :data:`MATCH_RULE_VALUES` — do NOT overload
#: the match-rule enum with these values. A hyphenated compound counts for the
#: bare term on BOTH venues; a closed (unhyphenated) compound (``wildfire`` for
#: ``fire``) diverges: Kalshi No, Polymarket candidate-only in v1. An affix
#: derivation (``joyful`` for ``joy``) counts for NEITHER venue.
COMPOUND_TYPE_VALUES: tuple[str, ...] = (
"standalone",
"open",
"hyphenated",
"closed",
"affix_derivation",
)

#: Venue auto-count membership over :data:`COMPOUND_TYPE_VALUES` (D-30). Both
#: venues auto-count standalone/open/hyphenated in v1. ``closed`` is a
#: candidate-only type — it never auto-counts; instead it surfaces for human
#: review when it could flip a Polymarket outcome (fact_builder), and it is
#: excluded from the Kalshi count entirely (Kalshi says No on a closed compound).
#: ``affix_derivation`` is in NEITHER set (counts for no venue).
KALSHI_AUTOCOUNT_COMPOUND_TYPES: frozenset[str] = frozenset({"standalone", "open", "hyphenated"})
POLYMARKET_AUTOCOUNT_COMPOUND_TYPES: frozenset[str] = frozenset(
{"standalone", "open", "hyphenated"}
)

#: Resolution status (RESEARCH-MARKETS §2.1(6)). ``"provisional"`` (27-10 Task 1)
#: marks a LIVE early-signal fact delta — NOT a settlement/backtest source
#: (D-27.16); authority is gated on this column + ``source``, never on
Expand Down Expand Up @@ -320,6 +346,20 @@ class EarningsFactSchema(Schema):
enum_values=MATCH_RULE_VALUES,
notes="§2.1(3) term.match_rule (plural/possessive OK, NOT tense)",
),
ColumnSpec(
name="compound_type",
dtype="enum",
units=None,
nullable=True,
enum_values=COMPOUND_TYPE_VALUES,
notes=(
"D-30 §2.1(3) per-occurrence compound axis — SEPARATE from "
"term_match_rule (do NOT overload MATCH_RULE_VALUES). standalone/"
"open/hyphenated auto-count on both venues; closed is Polymarket "
"candidate-only + Kalshi-No; affix_derivation counts for neither. "
"Nullable: pre-fix rows omit it (default 'standalone')"
),
),
ColumnSpec(
name="matched_surface_form",
dtype="string",
Expand Down Expand Up @@ -498,11 +538,14 @@ class EarningsFactSchema(Schema):


__all__ = [
"COMPOUND_TYPE_VALUES",
"COUNTING_MODE_VALUES",
"DELIVERY_VALUES",
"KALSHI_AUTOCOUNT_COMPOUND_TYPES",
"KALSHI_COUNTABLE_ROLE_SOURCES",
"KALSHI_COUNTABLE_SPEAKER_ROLES",
"MATCH_RULE_VALUES",
"POLYMARKET_AUTOCOUNT_COMPOUND_TYPES",
"RESOLUTION_STATUS_VALUES",
"ROLE_SOURCE_VALUES",
"SEGMENT_VALUES",
Expand Down
48 changes: 48 additions & 0 deletions packages/core/tests/test_earnings_fact_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,54 @@ def test_resolution_status_includes_provisional() -> None:
assert "provisional" in BY_NAME["resolution_status"].enum_values


def test_compound_type_axis_and_venue_autocount_sets() -> None:
"""D-30: compound_type is a NEW axis (not overloaded onto term_match_rule).

A hyphenated compound counts for the bare term on both venues; a closed
(unhyphenated) compound diverges — Kalshi No, Polymarket candidate-only in v1.
The compound_type column carries this per-occurrence axis; the venue
auto-count membership sets diverge the settlement filters row-wise exactly as
kalshi_counted does for speaker-scope.
"""
from mostlyright.core.schemas.earnings_fact import (
COMPOUND_TYPE_VALUES,
KALSHI_AUTOCOUNT_COMPOUND_TYPES,
POLYMARKET_AUTOCOUNT_COMPOUND_TYPES,
)

assert COMPOUND_TYPE_VALUES == (
"standalone",
"open",
"hyphenated",
"closed",
"affix_derivation",
)
# New axis — NOT overloaded onto the match-rule enum.
assert "closed" not in MATCH_RULE_VALUES
assert "compound_type" not in MATCH_RULE_VALUES

ct = BY_NAME["compound_type"]
assert ct.dtype == "enum"
assert ct.nullable is True # back-compat: old rows omit it
assert ct.enum_values == COMPOUND_TYPE_VALUES

# v1 auto-count sets (closed is candidate-only, NOT auto-counted).
expected = frozenset({"standalone", "open", "hyphenated"})
assert expected == KALSHI_AUTOCOUNT_COMPOUND_TYPES
assert expected == POLYMARKET_AUTOCOUNT_COMPOUND_TYPES


def test_compound_type_names_exported() -> None:
import mostlyright.core.schemas.earnings_fact as ef

for name in (
"COMPOUND_TYPE_VALUES",
"KALSHI_AUTOCOUNT_COMPOUND_TYPES",
"POLYMARKET_AUTOCOUNT_COMPOUND_TYPES",
):
assert name in ef.__all__


def test_no_columnspec_carries_regex_or_cross_field() -> None:
# ColumnSpec only has name/dtype/units/nullable/enum_values/notes — assert no
# rogue regex/cross-field attribute leaked onto any spec.
Expand Down
61 changes: 45 additions & 16 deletions packages/weather/src/mostlyright/weather/catalog/earnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@
"mention_count",
"role_source",
"kalshi_counted",
# D-30 compound axis (review R2-F1): stripping this at the read boundary
# laundered closed/affix occurrences into standalone auto-counts on BOTH
# venues (inverting Kalshi's closed-compound No and bypassing the fail-loud
# Polymarket review). Projected only-when-present, so transcript rows and
# pre-fix payloads are unaffected.
"compound_type",
)


Expand Down Expand Up @@ -427,27 +433,50 @@ def _project_stream_row(event: str, payload: object, stream_seq: int | None) ->
return row


def _as_aware_live_timestamp(value: object, *, field: str) -> pd.Timestamp | None:
"""Parse one live temporal field to a tz-aware Timestamp (None passes).

An absent value is a schema shape, not a contract violation. Any PRESENT
value must parse to a tz-aware wallclock: a naive value would mis-order
against the tz-aware as_of cutoff downstream, and a bare number (e.g. an
engine-relative ``spoken_at`` float leaked across the D-30 wire seam —
``offset_seconds`` is the correct wire field) silently coerces into a
1970-epoch wallclock. The 27-11 wire contract is tz-aware UTC.
"""
if value is None:
return None
try:
ts = pd.Timestamp(value)
except (TypeError, ValueError) as exc:
raise LiveStreamError(
f"earnings live stream: {field}={value!r} is not parseable as a "
"timestamp — the 27-11 wire contract is tz-aware UTC."
) from exc
if ts.tz is None:
raise LiveStreamError(
f"earnings live stream: {field}={value!r} must be tz-aware (UTC). "
"A naive/numeric value would coerce into a 1970-epoch wallclock or "
"mis-order against the tz-aware as_of cutoff downstream; an "
"engine-relative offset belongs in offset_seconds, not here."
)
return ts


def _assert_live_temporal_contract(spoken_at: object, knowledge_time: object) -> None:
"""Enforce ``knowledge_time (published_at) >= spoken_at`` for a live row.

A no-op when either value is absent (a transcript segment may carry only one,
and an absent value is a schema shape, not a contract violation). When BOTH
are present, a publish wallclock that predates the aired instant is
impossible and signals a mis-paired feed — raise ``LiveStreamError`` so a
malformed feed fails loud rather than yielding a knowledge_time that
understates availability.
Each PRESENT value must be a tz-aware wallclock (validated independently —
a transcript segment may carry only one, and a fact delta may carry
neither, but a malformed value never passes just because its partner is
absent). When BOTH are present, a publish wallclock that predates the
aired instant is impossible and signals a mis-paired feed — raise
``LiveStreamError`` so a malformed feed fails loud rather than yielding a
knowledge_time that understates availability.
"""
if spoken_at is None or knowledge_time is None:
spoken = _as_aware_live_timestamp(spoken_at, field="spoken_at")
published = _as_aware_live_timestamp(knowledge_time, field="knowledge_time")
if spoken is None or published is None:
return
spoken = pd.Timestamp(spoken_at)
published = pd.Timestamp(knowledge_time)
if spoken.tz is None or published.tz is None:
# A naive live timestamp would mis-order against the tz-aware as_of
# cutoff downstream. The 27-11 wire contract is tz-aware UTC.
raise LiveStreamError(
"earnings live stream: spoken_at / knowledge_time must be tz-aware "
f"(UTC); got spoken_at={spoken_at!r}, knowledge_time={knowledge_time!r}."
)
if published < spoken:
raise LiveStreamError(
"earnings live stream: knowledge_time (publish/STT-finalization "
Expand Down
Loading
Loading