"""The claim catalog: every material number a reader can see, with its identity attached. A claim is a typed pointer into the frozen AnalysisState. It has one id, one state path, one semantic type, one label, and a list of labels it must never be given. The renderer and the prose ingestion use claims exclusively, so the identity of a number is fixed when the number is produced and is never re-inferred from the words around it. Claim ids never collide: the headline return and the PM return are different claims with different labels, and each forbids the other's vocabulary next to it. """ from __future__ import annotations from dataclasses import dataclass, field, replace from units import format_percent # semantic types → formatter CURRENCY, CURRENCY_BIG, PCT, PCT_SIGNED, GAP, MULTIPLE, RATIO, COUNT, TEXT, PP, COUNT_SIGNED, BPS = ( "CURRENCY", "CURRENCY_BIG", "PCT", "PCT_SIGNED", "GAP", "MULTIPLE", "RATIO", "COUNT", "TEXT", "PP", "COUNT_SIGNED", "BPS") # REL: a RelationshipClaim rendered as one complete clause; PHRASE: a signed quantity rendered with its own direction; # GUIDANCE: a typed guidance range (rendering is presentation, the row is the claim) REL, PHRASE, GUIDANCE = "REL", "PHRASE", "GUIDANCE" # a share printed to two decimals: the story's peak share, so market size × share reproduces the revenue at maturity at # the precision the report prints ($130.00B × 17.2% = $22.36B read against a stated $22.30B) PCT2 = "PCT2" SIGNED_STYPES = (PCT_SIGNED, COUNT_SIGNED) def fmt(stype, v): if v is None: return None if stype == CURRENCY: return f"${v:,.2f}" if stype == CURRENCY_BIG: a = abs(v) sign = "-" if v < 0 else "" if a >= 1e12: return f"{sign}${a / 1e12:,.2f}T" if a >= 1e9: return f"{sign}${a / 1e9:,.2f}B" if a >= 1e6: return f"{sign}${a / 1e6:,.1f}M" return f"{sign}${a:,.0f}" if stype == PCT: return format_percent(v, 1) if stype == PCT2: return format_percent(v, 2) if stype == PCT_SIGNED: return format_percent(v, 1, sign=True) if stype == GAP: if abs(v) < 0.0005: return "in line with" return f"{format_percent(abs(v), 1)} {'above' if v > 0 else 'below'}" if stype == MULTIPLE: return f"{v:,.1f}x" if stype == RATIO: return f"{v:,.2f}" if stype == COUNT: return f"{int(round(v)):,}" if stype == COUNT_SIGNED: return f"{int(round(v)):+,}" if stype == BPS: # a canonical decimal change (0.0035) stated in basis points return f"{v * 10000.0:,.0f} bps" if stype == PP: return f"{v * 100:+.1f} pts" return str(v) @dataclass(frozen=True) class ClaimDef: claim_id: str path: str stype: str label: str prohibited: tuple = () deferred: bool = False # only exists after the panel / blend (post-debate) # the words that must label this claim in prose (at least one, in the words before it): a stable-period # rate cited without "stable"/"mature"/"terminal" reads as today's rate required_qualifiers: tuple = () persona_visible: bool = True # False: rendered by the report, not offered to personas (redundant there) label_path: str | None = None # the label a reader sees comes from the state (e.g. a pro-forma story base) phrase: tuple = () # PHRASE: (template if > 0, template if < 0, template if 0, magnitude stype) render_mode: str = "FULL" # REL: FULL (a complete clause), CLAUSE or COMPACT (a predicate after its lhs subject) clause: bool = False # renders a complete clause: it cannot serve as a noun phrase inside another clause slot: str = "" # PREDICATE: renders a predicate ("down 11.8% …"), after a verb or its subject unit_after: str = "" # a bare count whose unit the reader needs right after it ("10" → "10 years") names: tuple = () # how a reader names this quantity without citing it ("the growth the price demands") _HEADLINE_WORDS = ("headline", "blended", "model-implied", "overall 12-month", "final return") _PM_WORDS = ("panel", "PM", "portfolio manager", "scenario-weighted", "probability-weighted") _FORECAST_WORDS = ("guidance", "consensus", "company expects", "management expects") _CONSENSUS_WORDS = ("our forecast", "engine forecast", "the model forecasts", "guidance") _STORY_WORDS = ("next year", "next-year", "12-month", "forecast", "guidance", "consensus") _INTRINSIC_WORDS = ("target", "consensus", "market composite", "analyst") _COMPOSITE_WORDS = ("intrinsic", "fair value", "worth", "DCF") CLAIMS = [ ClaimDef("PRICE", "market.price", CURRENCY, "share price"), ClaimDef("MARKET_CAP", "market.market_cap", CURRENCY_BIG, "market capitalisation"), ClaimDef("DIVIDEND_YIELD", "market.dividend_yield", PCT, "trailing dividend yield"), ClaimDef("RETURN_3M", "market.return_3m", PCT_SIGNED, "3-month price return"), ClaimDef("RETURN_12M", "market.return_12m", PCT_SIGNED, "12-month price return"), ClaimDef("SMA200", "market.sma200", CURRENCY, "200-day moving average"), ClaimDef("REVENUE_TTM", "historical.revenue_ttm", CURRENCY_BIG, "trailing twelve-month revenue", _FORECAST_WORDS + ("forecast",)), ClaimDef("OPERATING_INCOME_TTM", "historical.operating_income_ttm", CURRENCY_BIG, "trailing operating income"), ClaimDef("OPERATING_MARGIN_TTM", "historical.operating_margin_ttm", PCT, "trailing operating margin", ("forecast", "next year", "guidance", "story", "maturity")), ClaimDef("NET_INCOME_TTM", "historical.net_income_ttm", CURRENCY_BIG, "trailing net income"), ClaimDef("NET_MARGIN_TTM", "historical.net_margin_ttm", PCT, "trailing net margin", ("forecast", "next year")), ClaimDef("EPS_ADJUSTED_TTM", "historical.eps_adjusted_ttm", CURRENCY, "trailing adjusted EPS", ("GAAP", "reported")), ClaimDef("EPS_GAAP_TTM", "historical.eps_gaap_ttm", CURRENCY, "trailing reported EPS", ("adjusted",)), ClaimDef("BEAT_RATE", "historical.beat_rate_8q", PCT, "share of recent quarters beating estimates"), ClaimDef("FORECAST_REVENUE", "forecast.revenue", CURRENCY_BIG, "engine forecast revenue (next 12 months)", _FORECAST_WORDS), ClaimDef("FORECAST_REVENUE_GROWTH", "forecast.revenue_growth", PCT_SIGNED, "engine forecast revenue growth", _FORECAST_WORDS), ClaimDef("FORECAST_OPERATING_MARGIN", "forecast.operating_margin", PCT, "engine forecast operating margin", _FORECAST_WORDS + ("trailing", "story", "maturity")), ClaimDef("FORECAST_NET_MARGIN", "forecast.net_margin", PCT, "engine forecast net margin", _FORECAST_WORDS), ClaimDef("FORECAST_OPERATING_MARGIN_CHANGE", "forecast.operating_margin_change", PP, "change in operating margin from the trailing twelve months to the engine forecast", _FORECAST_WORDS), ClaimDef("FORECAST_EPS", "forecast.eps", CURRENCY, "engine forecast EPS (next 12 months)", _FORECAST_WORDS), ClaimDef("FORECAST_EPS_GROWTH", "forecast.eps_growth", PCT_SIGNED, "engine forecast EPS growth", _FORECAST_WORDS), ClaimDef("FORECAST_EPS_VS_CONSENSUS", "forecast.eps_vs_consensus", GAP, "engine EPS relative to consensus for the same twelve months"), ClaimDef("CONSENSUS_EPS_NTM", "consensus.eps_ntm", CURRENCY, "sell-side consensus EPS for the forecast's twelve months (time-weighted FY1/FY2)", _CONSENSUS_WORDS), ClaimDef("CONSENSUS_EPS_FY1", "consensus.eps_fy1", CURRENCY, "sell-side consensus EPS (current fiscal year)", _CONSENSUS_WORDS), ClaimDef("CONSENSUS_EPS_FY2", "consensus.eps_fy2", CURRENCY, "sell-side consensus EPS (next fiscal year)", _CONSENSUS_WORDS), ClaimDef("CONSENSUS_REVENUE_FY1", "consensus.revenue_fy1", CURRENCY_BIG, "sell-side consensus revenue (current fiscal year)", _CONSENSUS_WORDS), ClaimDef("CONSENSUS_REVENUE_NTM", "consensus.revenue_ntm", CURRENCY_BIG, "sell-side consensus revenue for the forecast's twelve months (time-weighted FY1/FY2)", _CONSENSUS_WORDS), ClaimDef("CONSENSUS_ANALYSTS", "consensus.n_analysts", COUNT, "estimate contributors"), ClaimDef("ANALYST_TARGET", "consensus.target_price", CURRENCY, "raw mean sell-side price target (as published)", ("intrinsic", "fair value", "our valuation")), ClaimDef("TARGET_VS_PRICE", "pricing.target_vs_price", PCT_SIGNED, "upside to the mean analyst target"), ClaimDef("INTRINSIC_VALUE", "valuation.intrinsic_value", CURRENCY, "intrinsic value", _INTRINSIC_WORDS), ClaimDef("PRICE_VS_INTRINSIC", "valuation.price_vs_intrinsic", GAP, "price relative to intrinsic value"), ClaimDef("MECHANICAL_VALUE", "valuation.mechanical_value", CURRENCY, "cross-check value", _INTRINSIC_WORDS), ClaimDef("PRICE_VS_MECHANICAL", "valuation.price_vs_mechanical", GAP, "price relative to the cross-check value"), ClaimDef("MARKET_COMPOSITE", "pricing.market_composite", CURRENCY, "market composite (pricing)", _COMPOSITE_WORDS), ClaimDef("PRICE_VS_COMPOSITE", "pricing.price_vs_composite", GAP, "price relative to the market composite"), ClaimDef("MARKET_COMPOSITE_RAW", "pricing.raw_composite", CURRENCY, "market composite before trimming and caps", _COMPOSITE_WORDS), ClaimDef("MARKET_COMPOSITE_POST_TRIM", "pricing.post_trim_composite", CURRENCY, "market composite after outlier trimming", _COMPOSITE_WORDS), ClaimDef("ANALYST_TARGET_ADJUSTED", "pricing.analyst_target_adjusted", CURRENCY, "risk-adjusted analyst-target contribution to the composite (not the published target)", ("intrinsic", "fair value")), ClaimDef("PE_FORWARD", "pricing.pe_forward", MULTIPLE, "forward P/E (engine EPS)"), ClaimDef("PE_TRAILING", "pricing.pe_trailing", MULTIPLE, "trailing P/E (adjusted EPS)"), ClaimDef("PE_TRAILING_GAAP", "pricing.pe_trailing_gaap", MULTIPLE, "trailing P/E (reported EPS)"), ClaimDef("PEG", "pricing.peg_forward", RATIO, "forward PEG"), ClaimDef("STORY_TAM", "valuation.story_tam", CURRENCY_BIG, "story market size at maturity"), ClaimDef("STORY_SHARE", "valuation.story_share", PCT2, "story peak market share"), ClaimDef("STORY_REVENUE_MATURITY", "valuation.story_revenue_maturity", CURRENCY_BIG, "story revenue at maturity"), ClaimDef("STORY_YEARS", "valuation.story_years", COUNT, "years to maturity"), ClaimDef("STORY_MARGIN", "valuation.story_maturity_margin", PCT, "story maturity operating margin assumption", _STORY_WORDS), ClaimDef("STORY_START_MARGIN", "valuation.story_base.operating_margin", PCT, "story starting operating margin", label_path="valuation.story_base.label"), ClaimDef("STORY_START_REVENUE", "valuation.story_base.revenue", CURRENCY_BIG, "story starting revenue", label_path="valuation.story_base.revenue_label"), ClaimDef("STORY_MARGIN_CHANGE", "valuation.story.margin_change", PP, "story operating-margin change from the starting margin to maturity", _STORY_WORDS), ClaimDef("STORY_WACC", "valuation.discount_rate_path.initial_wacc_metric", PCT, "initial cost of capital (years at the start)"), ClaimDef("STORY_WACC_STABLE", "valuation.discount_rate_path.terminal_wacc_metric", PCT, "terminal cost of capital used in the valuation (stable period)", ("current", "today", "initial", "now", "present"), required_qualifiers=("stable", "mature", "terminal", "long-run", "long run", "fading to", "fades to", "falls to", "declining to")), ClaimDef("STORY_WACC_HOLD", "valuation.discount_rate_path.hold_years", COUNT, "years at the initial cost of capital"), ClaimDef("STORY_TERMINAL_GROWTH", "valuation.story.assumptions.terminal_growth", PCT, "terminal growth"), ClaimDef("STORY_FAILURE", "valuation.story.assumptions.failure_probability", PCT, "failure probability"), ClaimDef("CEO", "leadership.roles.CEO.name", TEXT, "chief executive officer"), ClaimDef("CFO", "leadership.roles.CFO.name", TEXT, "chief financial officer"), ClaimDef("INSIDER_CHARACTERIZATION", "insiders.canon.characterization", TEXT, "insider transactions (SEC Form 4)"), ClaimDef("MOMENTUM", "market.momentum.state", TEXT, "momentum state"), ClaimDef("LIFECYCLE_STAGE", "valuation.lifecycle.stage", TEXT, "life-cycle stage (Damodaran corporate life cycle)"), ClaimDef("LIFECYCLE_TRAILING_GROWTH", "valuation.lifecycle_trailing_growth", PCT_SIGNED, "trailing revenue growth (year over year)", ("forecast", "consensus")), ClaimDef("LIFECYCLE_FORWARD_GROWTH", "valuation.lifecycle_forward_growth", PCT_SIGNED, "forward revenue growth (consensus, next fiscal year)", ("trailing",)), ClaimDef("BE_REVENUE", "valuation.breakeven_revenue", CURRENCY_BIG, "revenue today's price needs at maturity (break-even)"), ClaimDef("BE_REVENUE_CAGR", "valuation.breakeven_revenue_cagr", PCT, "annual revenue growth from the story's starting revenue to the break-even revenue (bound to that pair)"), ClaimDef("BE_MARGIN", "valuation.breakeven_margin", PCT, "operating margin today's price needs at maturity (break-even)"), ClaimDef("ML_RANK_SHARE", "quant.panel_rank.figures.rank_share", PCT, "share of ranked companies below this one"), ClaimDef("ML_RANK_DECILE", "quant.panel_rank.figures.decile", COUNT, "machine-learning rank decile"), ClaimDef("ML_RANK_UNIVERSE", "quant.panel_rank.figures.n_universe", COUNT, "companies ranked"), ClaimDef("ML_RANK_IC", "quant.panel_rank.reliability.rank_ic", RATIO, "walk-forward rank correlation"), ClaimDef("ML_RANK_POSITIVE", "quant.panel_rank.reliability.ic_positive_share", PCT, "share of test months the rank was right-ordered"), ClaimDef("DMD_INDUSTRY", "valuation.damodaran_inputs.industry", TEXT, "industry (Damodaran classification)"), ClaimDef("DMD_UNLEVERED_BETA", "valuation.damodaran_inputs.unlevered_beta", RATIO, "industry unlevered beta (cash-corrected)"), ClaimDef("DMD_BETA", "valuation.damodaran_inputs.beta", RATIO, "bottom-up beta (re-levered at the company's D/E)"), ClaimDef("DMD_DE", "valuation.damodaran_inputs.debt_to_equity", PCT, "market debt-to-equity"), ClaimDef("DMD_ERP", "valuation.damodaran_inputs.erp", PCT, "equity risk premium used"), ClaimDef("DMD_IMPLIED_ERP", "valuation.damodaran_inputs.implied_erp", PCT, "implied equity risk premium (Damodaran, monthly)"), ClaimDef("DMD_CRP", "valuation.damodaran_inputs.crp", PCT, "country risk premium"), ClaimDef("DMD_ERP_MONTH", "valuation.damodaran_inputs.erp_month", TEXT, "month of the implied ERP"), ClaimDef("DMD_COUNTRY", "valuation.damodaran_inputs.country", TEXT, "country of operations"), ClaimDef("DMD_S2C", "valuation.damodaran_inputs.industry_sales_to_capital", RATIO, "industry sales-to-invested-capital"), ClaimDef("WACC_BENCHMARK", "valuation.discount_rate_path.benchmark_wacc_metric", PCT, "mature-market cost-of-capital benchmark (a reference, not necessarily the rate used)", ("used", "current", "today", "initial", "our rate"), persona_visible=False), ClaimDef("DMD_MARGIN", "valuation.industry_operating_margin", PCT, "industry pre-tax operating margin (industry aggregate)"), ClaimDef("OWN_13F_AS_OF", "ownership.f13.as_of", TEXT, "13F quarter end"), ClaimDef("OWN_13F_HOLDERS", "ownership.f13.n_holders", COUNT, "institutions reporting a position (13F)"), ClaimDef("OWN_13F_HOLDERS_PREV", "ownership.f13.n_holders_prev", COUNT, "institutions reporting a position the quarter before"), ClaimDef("OWN_13F_TRACKED", "ownership.f13.n_gurus", COUNT, "tracked long-term investors holding"), ClaimDef("OWN_13F_NEW", "ownership.f13.guru_new_buys", COUNT, "tracked investors opening a position"), ClaimDef("OWN_13F_ADDS", "ownership.f13.guru_adds", COUNT, "tracked investors adding"), ClaimDef("OWN_13F_TRIMS", "ownership.f13.guru_trims", COUNT, "tracked investors trimming"), ClaimDef("OWN_13F_EXITS", "ownership.f13.guru_exits", COUNT, "tracked investors exiting"), ClaimDef("OWN_13F_NAMES", "ownership.f13.guru_names_short", TEXT, "tracked investors holding (names)"), ClaimDef("OWN_13F_HOLDERS_CHANGE", "ownership.f13.n_holders_change", COUNT_SIGNED, "net change in the number of institutions reporting a position vs the quarter before (signed; positions, not intent)"), ClaimDef("INSIDER_DISCRETIONARY_NET", "insiders.canon.materiality.discretionary_net_value", CURRENCY_BIG, "discretionary open-market purchases less sales in the window (SEC Form 4)"), ClaimDef("INSIDER_MATERIALITY", "insiders.canon.materiality.share_of_market_cap", PCT, "discretionary net insider trading as a share of market cap"), ClaimDef("ADJ_GAAP_BRIDGE", "historical.adjustment_bridge.selected_ratio", RATIO, "adjusted EPS ÷ GAAP diluted EPS, like for like (the adjusted↔GAAP bridge)"), ClaimDef("SEGMENTS_COVERAGE", "segments.canonical.coverage", PCT, "filed segment revenue as a share of consolidated revenue, same period (the business AS FILED)"), ClaimDef("SEGMENT_COUNT_FILED", "valuation.segment_count_filed", COUNT, "reportable segments in the latest filing (the business AS FILED, before any separation)"), ClaimDef("SEGMENT_COUNT_CONTINUING", "valuation.segment_count_continuing", COUNT, "businesses in the continuing company (the separated segment removed)"), # post-debate claims ClaimDef("SCENARIO_BEAR_RETURN", "scenarios.bear.return", PCT_SIGNED, "bear scenario price return", deferred=True), ClaimDef("SCENARIO_NEUTRAL_RETURN", "scenarios.neutral.return", PCT_SIGNED, "neutral scenario price return", deferred=True), ClaimDef("SCENARIO_BULL_RETURN", "scenarios.bull.return", PCT_SIGNED, "bull scenario price return", deferred=True), ClaimDef("SCENARIO_BEAR_PROB", "scenarios.bear.probability", PCT, "bear scenario probability", deferred=True), ClaimDef("SCENARIO_NEUTRAL_PROB", "scenarios.neutral.probability", PCT, "neutral scenario probability", deferred=True), ClaimDef("SCENARIO_BULL_PROB", "scenarios.bull.probability", PCT, "bull scenario probability", deferred=True), ClaimDef("PM_RETURN", "scenarios.pm_return", PCT_SIGNED, "panel probability-weighted scenario return", _HEADLINE_WORDS, deferred=True), ClaimDef("HEADLINE_RETURN", "blend.headline_return", PCT_SIGNED, "blended model-implied 12-month return", _PM_WORDS, deferred=True), ClaimDef("RATING", "recommendation.rating", TEXT, "rating", deferred=True), ClaimDef("RANGE_LOW", "uncertainty.scenario_range.low", PCT_SIGNED, "bear-case end of the scenario range", deferred=True), ClaimDef("RANGE_HIGH", "uncertainty.scenario_range.high", PCT_SIGNED, "bull-case end of the scenario range", deferred=True), # ── RelationshipClaims: each renders lhs, relation and rhs as one clause (relations.py) ── ClaimDef("REL_PRICE_INTRINSIC", "relations.state.items.REL_PRICE_INTRINSIC", REL, "price vs intrinsic value (complete relation)"), ClaimDef("REL_PRICE_MECHANICAL", "relations.state.items.REL_PRICE_MECHANICAL", REL, "price vs cross-check value (complete relation)"), ClaimDef("REL_PRICE_MARKET_COMPOSITE", "relations.state.items.REL_PRICE_MARKET_COMPOSITE", REL, "price vs market composite (complete relation)"), ClaimDef("REL_TARGET_PRICE", "relations.state.items.REL_TARGET_PRICE", REL, "mean analyst target vs price (complete relation)"), ClaimDef("REL_FORECAST_EPS_CONSENSUS", "relations.state.items.REL_FORECAST_EPS_CONSENSUS", REL, "engine EPS forecast vs consensus for the same twelve months (complete relation)"), ClaimDef("REL_ENGINE_REVENUE_CONSENSUS", "relations.state.items.REL_ENGINE_REVENUE_CONSENSUS", REL, "engine revenue forecast vs consensus revenue for the same twelve months (complete relation)"), ClaimDef("REL_GUIDANCE_EPS_CONSENSUS", "relations.state.items.REL_GUIDANCE_EPS_CONSENSUS", REL, "company EPS guidance range vs consensus EPS for the same fiscal year and basis (complete relation)"), ClaimDef("REL_BREAK_EVEN_REVENUE_STORY", "relations.state.items.REL_BREAK_EVEN_REVENUE_STORY", REL, "break-even revenue vs the story's revenue at maturity (complete relation)"), ClaimDef("REL_BREAK_EVEN_MARGIN_STORY", "relations.state.items.REL_BREAK_EVEN_MARGIN_STORY", REL, "break-even operating margin vs the story's maturity margin (complete relation)"), ClaimDef("REL_BREAK_EVEN_REVENUE_GROWTH", "relations.state.items.REL_BREAK_EVEN_REVENUE_GROWTH", REL, "growth needed from the starting revenue to the break-even revenue (complete relation)"), ClaimDef("REL_OPERATING_MARGIN_INDUSTRY", "relations.state.items.REL_OPERATING_MARGIN_INDUSTRY", REL, "company operating margin vs the industry aggregate (complete relation)"), ClaimDef("REL_EXPECTATIONS_STORY", "relations.state.items.REL_EXPECTATIONS_STORY", REL, "what today's price requires vs the adopted story (the only permitted statement of that polarity)"), # ── signed quantities rendered with their own direction: the only form offered to authors ── ClaimDef("RETURN_3M_PHRASE", "market.return_3m", PHRASE, "3-month share-price move", phrase=("up {m} over three months", "down {m} over three months", "unchanged over three months", PCT)), ClaimDef("RETURN_12M_PHRASE", "market.return_12m", PHRASE, "12-month share-price move", phrase=("up {m} over twelve months", "down {m} over twelve months", "unchanged over twelve months", PCT)), ClaimDef("OWN_13F_HOLDERS_CHANGE_PHRASE", "ownership.f13.n_holders_change", PHRASE, "change in institutions reporting a position vs the quarter before, as a complete clause (positions, not intent)", phrase=("the number of institutions reporting a position rose by {m} from the quarter before", "the number of institutions reporting a position fell by {m} from the quarter before", "the number of institutions reporting a position was unchanged from the quarter before", COUNT), clause=True), ClaimDef("SCENARIO_BEAR_RETURN_PHRASE", "scenarios.bear.return", PHRASE, "bear scenario price return", deferred=True, phrase=("{a} {m} upside case", "{a} {m} downside case", "a flat case", PCT)), ClaimDef("SCENARIO_NEUTRAL_RETURN_PHRASE", "scenarios.neutral.return", PHRASE, "neutral scenario price return", deferred=True, phrase=("{a} {m} upside case", "{a} {m} downside case", "a flat case", PCT)), ClaimDef("SCENARIO_BULL_RETURN_PHRASE", "scenarios.bull.return", PHRASE, "bull scenario price return", deferred=True, phrase=("{a} {m} upside case", "{a} {m} downside case", "a flat case", PCT)), ClaimDef("PM_RETURN_PHRASE", "scenarios.pm_return", PHRASE, "panel probability-weighted scenario return", _HEADLINE_WORDS, deferred=True, phrase=("{a} {m} probability-weighted gain", "{a} {m} probability-weighted loss", "a flat probability-weighted return", PCT)), ClaimDef("HEADLINE_RETURN_PHRASE", "blend.headline_return", PHRASE, "blended model-implied 12-month return", _PM_WORDS, deferred=True, phrase=("{a} {m} model-implied gain", "{a} {m} model-implied loss", "a flat model-implied return", PCT)), ] # CLAUSE / COMPACT render modes of the two-sided relations: the same typed lhs, operator, magnitude and rhs, shorter _MODED = ("REL_PRICE_INTRINSIC", "REL_PRICE_MECHANICAL", "REL_PRICE_MARKET_COMPOSITE", "REL_TARGET_PRICE", "REL_FORECAST_EPS_CONSENSUS", "REL_ENGINE_REVENUE_CONSENSUS", "REL_BREAK_EVEN_REVENUE_STORY", "REL_BREAK_EVEN_MARGIN_STORY", "REL_OPERATING_MARGIN_INDUSTRY") for _cid in _MODED + ("REL_EXPECTATIONS_STORY",): for _mode in ("CLAUSE", "COMPACT", "PREDICATE", "NOUN_PHRASE", "FULL_SENTENCE"): if _cid == "REL_EXPECTATIONS_STORY" and _mode not in ("FULL_SENTENCE", "COMPACT"): continue CLAIMS.append(ClaimDef(f"{_cid}__{_mode}", f"relations.state.items.{_cid}", REL, f"{next(c.label for c in CLAIMS if c.claim_id == _cid).split(' (')[0]} ({_mode.lower()} form: " + {"FULL_SENTENCE": "a whole sentence on its own; write nothing else in that sentence", "CLAUSE": "after its subject and a verb, e.g. 'the price sits …'", "COMPACT": "after its subject and a verb, e.g. 'the price is …'", "PREDICATE": "directly after its subject; it carries its own verb", "NOUN_PHRASE": "as an object, e.g. 'the stock trades at …'"}[_mode] + ")", render_mode=_mode, # a whole-sentence rendering is for report templates; authors write clauses persona_visible=_mode != "FULL_SENTENCE")) # TrendRelationships: offered only when the earlier and later values are compatible (same metric, basis and perimeter) for _cid, _lbl in (("TREND_REVENUE_GROWTH", "revenue growth trend"), ("TREND_REVENUE", "revenue trend"), ("TREND_OPERATING_MARGIN", "operating margin trend"), ("TREND_EPS", "EPS trend")): CLAIMS.append(ClaimDef(_cid, f"relations.state.items.{_cid}", REL, f"{_lbl}: trailing → forward (complete relation)")) CLAIMS = [replace(c, slot="PREDICATE") if c.claim_id in ("RETURN_3M_PHRASE", "RETURN_12M_PHRASE") else c for c in CLAIMS] CLAIMS = [replace(c, unit_after="year") if c.claim_id in ("STORY_YEARS", "STORY_WACC_HOLD") else c for c in CLAIMS] _REQ = ("the price demands", "the price needs", "the price requires", "today's price needs", "today's price requires") READER_NAMES = { "BE_REVENUE_CAGR": ("break-even revenue growth", "break-even growth", "required growth", "required revenue growth") + tuple(f"growth {r}" for r in _REQ), "BE_REVENUE": ("break-even revenue", "required revenue", "required maturity revenue") + tuple(f"revenue {r}" for r in _REQ), "BE_MARGIN": ("break-even margin", "required margin", "required operating margin") + tuple(f"margin {r}" for r in _REQ)} READER_NAMES.update({ "FORECAST_EPS": ("our forecast", "our eps forecast", "engine forecast", "engine's forecast", "engine eps", "our estimate", "the forecast assumes", "our forecast assumes"), "CONSENSUS_EPS_NTM": ("consensus eps", "street eps", "consensus estimate"), "STORY_REVENUE_MATURITY": ("story's revenue at maturity", "maturity revenue", "story's maturity", "story revenue")}) CLAIMS = [replace(c, names=READER_NAMES[c.claim_id]) if c.claim_id in READER_NAMES else c for c in CLAIMS] BY_ID = {c.claim_id: c for c in CLAIMS} # the signed claim each PHRASE claim replaces for authors (the signed form stays for tables) PHRASE_OF = {"RETURN_3M": "RETURN_3M_PHRASE", "RETURN_12M": "RETURN_12M_PHRASE", "OWN_13F_HOLDERS_CHANGE": "OWN_13F_HOLDERS_CHANGE_PHRASE", "SCENARIO_BEAR_RETURN": "SCENARIO_BEAR_RETURN_PHRASE", "SCENARIO_NEUTRAL_RETURN": "SCENARIO_NEUTRAL_RETURN_PHRASE", "SCENARIO_BULL_RETURN": "SCENARIO_BULL_RETURN_PHRASE", "PM_RETURN": "PM_RETURN_PHRASE", "HEADLINE_RETURN": "HEADLINE_RETURN_PHRASE"} # reported and forecast income-statement lines (a statement noun in their clause must be the income statement) INCOME_STATEMENT_CLAIMS = ("REVENUE_TTM", "OPERATING_INCOME_TTM", "OPERATING_MARGIN_TTM", "NET_INCOME_TTM", "NET_MARGIN_TTM", "EPS_ADJUSTED_TTM", "EPS_GAAP_TTM", "FORECAST_REVENUE", "FORECAST_REVENUE_GROWTH", "FORECAST_OPERATING_MARGIN", "FORECAST_NET_MARGIN", "FORECAST_EPS", "FORECAST_EPS_GROWTH", "CONSENSUS_EPS_NTM", "CONSENSUS_EPS_FY1", "CONSENSUS_EPS_FY2", "CONSENSUS_REVENUE_FY1") # presentation claims that show only a relation's magnitude: report templates only, never offered to authors RELATION_PRESENTATION = ("PRICE_VS_INTRINSIC", "PRICE_VS_MECHANICAL", "PRICE_VS_COMPOSITE", "TARGET_VS_PRICE", "FORECAST_EPS_VS_CONSENSUS", "BE_REVENUE_CAGR") BY_ID = {c.claim_id: c for c in CLAIMS} @dataclass class Claim: claim_id: str path: str stype: str label: str value: object rendered: str | None prohibited: tuple = () deferred: bool = False extra: dict = field(default_factory=dict) required_qualifiers: tuple = () def to_dict(self): return {"claim_id": self.claim_id, "path": self.path, "stype": self.stype, "label": self.label, "value": self.value, "rendered": self.rendered} def _resolve(state, path): v = state.get(path) if hasattr(v, "value") and hasattr(v, "metric_id"): v = v.value if isinstance(v, float) and v != v: # NaN is missing data, never a figure return None return v def _article(m): """'an' before a figure read with a vowel sound (8…, 11…, 18…, 80–89…).""" d = m.lstrip("+-$").split(".")[0].replace(",", "") return "an" if (d.startswith("8") or (len(d) == 2 and d in ("11", "18"))) else "a" def return_claim(value, phrase): """ReturnClaim: signed value, magnitude, direction and the phrase that carries the direction itself.""" if value is None or not phrase: return None up, down, zero, mst = phrase v = float(value) mag = abs(v) m = fmt(mst, mag) direction = "FLAT" if (fmt(mst, mag) in ("0.0%", "0") or mag == 0) else ("UP" if v > 0 else "DOWN") tpl = {"UP": up, "DOWN": down, "FLAT": zero}[direction] return {"value_signed": v, "magnitude": mag, "direction": direction, "render_mode": "phrase", "rendered": tpl.format(m=m, a=_article(m))} def _rel_value(v): from collections.abc import Mapping if not isinstance(v, Mapping): return None return {k: v.get(k) for k in ("claim_id", "lhs_claim_id", "rhs_claim_id", "relation_type", "magnitude", "magnitude_unit", "magnitude_definition", "lhs_value", "rhs_value", "earlier_value", "later_value")} def _guidance_value(v): from collections.abc import Mapping if not isinstance(v, Mapping): return None return {k: v.get(k) for k in ("concept", "quantity_type", "unit", "operator", "low", "high", "raw_low", "raw_high", "raw_unit", "accounting_basis", "period_label")} def claim_for(state, cdef): if cdef.label_path and state.get(cdef.label_path): cdef = replace(cdef, label=str(state.get(cdef.label_path))) if cdef.stype == REL: raw = state.get(cdef.path) v = _rel_value(raw) rendered = (((raw.get("renderings") or {}).get(cdef.render_mode) or (raw.get("rendered") if cdef.render_mode == "FULL" else None)) if v is not None else None) if rendered is None: v = None return Claim(cdef.claim_id, cdef.path, cdef.stype, cdef.label, v, rendered, cdef.prohibited, cdef.deferred, required_qualifiers=cdef.required_qualifiers) if cdef.stype == GUIDANCE: raw = state.get(cdef.path) v = _guidance_value(raw) rendered = render_guidance(raw) if v is not None else None return Claim(cdef.claim_id, cdef.path, cdef.stype, cdef.label, v, rendered, cdef.prohibited, cdef.deferred, required_qualifiers=cdef.required_qualifiers) v = _resolve(state, cdef.path) if cdef.stype == PHRASE: rc = return_claim(v, cdef.phrase) return Claim(cdef.claim_id, cdef.path, cdef.stype, cdef.label, v, rc["rendered"] if rc else None, cdef.prohibited, cdef.deferred, extra={"return_claim": rc} if rc else {}, required_qualifiers=cdef.required_qualifiers) if cdef.stype == TEXT and cdef.path.endswith("_usd_b") and v is not None: rendered = f"${float(v):,.0f}B" else: rendered = fmt(cdef.stype, v) if not (cdef.stype == TEXT and v is not None) else str(v) return Claim(cdef.claim_id, cdef.path, cdef.stype, cdef.label, v, rendered, cdef.prohibited, cdef.deferred, required_qualifiers=cdef.required_qualifiers) _GUIDANCE_PROHIBITED = ("consensus", "analyst", "our forecast", "engine forecast", "the model", "street") def guidance_claims(state): """GUIDANCE_n: one claim per verified company-guidance row (guidance.build_state), and nothing else. The only way prose may state or describe company guidance.""" from collections.abc import Mapping g = state.get("guidance.company") or {} if not isinstance(g, Mapping) or g.get("status") != "AVAILABLE": # sealed namespaces hold read-only mappings return [] # the token is the VALUE only ("335–360 bps"); its label and period are words the author writes, so a sentence # that names the metric does not repeat it ("… amortization guidance of [[GUIDANCE_6]]") return [ClaimDef(f"GUIDANCE_{i}", f"guidance.company.metrics.{i - 1}", GUIDANCE, f"company guidance value: {m.get('label')}, {m.get('period_label')} " f"({str(m.get('quantity_type') or '').lower().replace('_', ' ')})", _GUIDANCE_PROHIBITED) for i, m in enumerate(g.get("metrics") or [], 1)] def claim_def(state, cid): """The ClaimDef for a claim id: the static catalog, else a dynamic guidance claim.""" if cid in BY_ID: return BY_ID[cid] return next((c for c in guidance_claims(state) if c.claim_id == cid), None) # quantity families: two claims are candidates for comparison only within a family (or across related families) FAMILY = {"OPERATING_MARGIN": "margin", "NET_MARGIN": "margin", "GROSS_MARGIN": "margin", "TAX_RATE": "margin", "OPERATING_MARGIN_CHANGE": "margin_change", "GROSS_MARGIN_CHANGE": "margin_change", "REVENUE_GROWTH": "growth", "ORGANIC_REVENUE_GROWTH": "growth", "EPS_GROWTH": "growth", "REVENUE": "revenue", "EPS": "eps", "PE": "pe", "COST_OF_CAPITAL": "wacc", "PRICE": "value", "INTRINSIC_VALUE": "value", "MARKET_PRICING": "value", "TARGET_PRICE": "value", "BREAK_EVEN_REVENUE": "revenue", "BREAK_EVEN_OPERATING_MARGIN": "margin", "BREAK_EVEN_REVENUE_CAGR": "growth"} _RELATED = {frozenset({"margin", "margin_change"}): "a level and a change are different quantities"} def _claim_metric(state, cid): d = claim_def(state, cid) if d is None: return None m = state.get(d.path) if d.stype == GUIDANCE and m is not None: # a guidance row is a typed range: its identity (concept, quantity type, unit, period) is the claim from state import FY1, metric as _metric lo, hi = m.get("low"), m.get("high") return _metric(str(m.get("metric_id") or cid), m.get("concept"), ((lo + hi) / 2.0) if (lo is not None and hi is not None) else (lo if lo is not None else hi), FY1, quantity_type=m.get("quantity_type"), display_unit=m.get("unit"), unit="ratio" if m.get("unit") not in ("USD", "USD_MILLION", "USD_BILLION", "USD_PER_SHARE") else "USD", perimeter="company guidance", source="company guidance (8-K)") return m if hasattr(m, "economic_concept") else None def comparison(state, cid_a, cid_b): """(allowed, reasons) for comparing two claims, or None when they are not the same kind of quantity (then the sentence is not a like-for-like comparison and this layer has no opinion). Uses state.compatible().""" from state import compatible a, b = _claim_metric(state, cid_a), _claim_metric(state, cid_b) if a is None or b is None: return None fa, fb = FAMILY.get(a.economic_concept), FAMILY.get(b.economic_concept) if fa is None or fb is None: return None if fa != fb: why = _RELATED.get(frozenset({fa, fb})) return (False, [why]) if why else None from state import SCENARIO_BASE if fa == "value": ctype = "valuation" elif a.economic_concept in SCENARIO_BASE or b.economic_concept in SCENARIO_BASE: ctype = "scenario" if a.period_class == b.period_class else "path" elif "INDUSTRY" in (a.scope, b.scope) or "PEER" in (a.scope, b.scope): ctype = "benchmark" elif a.period_class != b.period_class: ctype = "trend" else: ctype = "level" ok, why = compatible(a, b, ctype) return ok, list(dict.fromkeys(why)) def pair_status(state, a, b, pairs=None): """None when two claims are not related quantities; ("RELATION", id) when an approved relation connects them; ("OK",) when they are compatible without one; ("INCOMPATIBLE", reasons) otherwise (compatible(), not words).""" ma, mb = _claim_metric(state, a), _claim_metric(state, b) if ma is None or mb is None: return None fa, fb = FAMILY.get(ma.economic_concept), FAMILY.get(mb.economic_concept) if fa is None or fb is None or (fa != fb and frozenset({fa, fb}) not in _RELATED): return None rel = (pairs or {}).get(frozenset({a, b})) if rel: return ("RELATION", rel) r = comparison(state, a, b) if r is None: return None return ("OK",) if r[0] else ("INCOMPATIBLE", r[1]) def comparison_permissions(state, cat): """[(a, b, reasons)] for every pair of offered claims of one family that may NOT be compared.""" ids = [c for c in cat if not cat[c].deferred] out = [] for i, a in enumerate(ids): for b in ids[i + 1:]: r = comparison(state, a, b) if r is not None and not r[0]: out.append((a, b, r[1])) return out # words a sentence uses to point at a counterpart it does not cite ("growth of [[X]] below trailing") _PERIOD_WORDS = {"TRAILING": ("trailing", "last year", "prior year", "past year", "historical", "historically", "a year ago", "last twelve months"), "FORWARD": ("forward", "next year", "next twelve months", "going forward")} _PERIMETER_WORDS = {"pre-separation": ("before the spin", "before the separation", "pre-spin", "pre-separation", "including the separated", "with the divested")} def implicit_counterparts(state, cat): """{claim: [(counterpart, reasons, reference words, trend)]} for every incomparable same-family pair. A sentence that cites one claim and points at the other by its period or perimeter ("below trailing"), or describes a trend in a growth rate whose historical counterpart is incomparable ("decelerating"), compares them implicitly.""" out = {} for a, b, why in comparison_permissions(state, cat): ma, mb = _claim_metric(state, a), _claim_metric(state, b) for x, mx, y, my in ((a, ma, b, mb), (b, mb, a, ma)): own = set(_PERIOD_WORDS.get(mx.period_class, ())) | set(_PERIMETER_WORDS.get(mx.perimeter, ())) words = tuple(w for w in _PERIOD_WORDS.get(my.period_class, ()) + _PERIMETER_WORDS.get(my.perimeter, ()) if w not in own) trend = FAMILY.get(mx.economic_concept) == "growth" and my.period_class == "TRAILING" and mx.period_class != "TRAILING" if words or trend: out.setdefault(x, []).append((y, why, words, trend)) return out def claim_descriptors(state, cat): """{claim: (family, period/perimeter words that describe it)}: a word that describes a CITED claim of the same family points at that claim, not at an uncited counterpart.""" out = {} for cid in cat: m = _claim_metric(state, cid) if m is not None and FAMILY.get(m.economic_concept): out[cid] = (FAMILY[m.economic_concept], tuple(_PERIOD_WORDS.get(m.period_class, ())) + tuple(_PERIMETER_WORDS.get(m.perimeter, ()))) return out _STYPE_KIND = {PCT: "rate", PCT_SIGNED: "rate", PP: "rate_change", BPS: "rate_change", CURRENCY: "per_share", CURRENCY_BIG: "money", MULTIPLE: "multiple"} _UNIT_KIND = {"PERCENT": "rate", "DECIMAL_FRACTION": "rate", "BASIS_POINTS": "rate_change", "PERCENTAGE_POINTS": "rate_change", "USD": "money", "USD_MILLION": "money", "USD_BILLION": "money", "USD_PER_SHARE": "per_share", "MULTIPLE": "multiple"} def claim_range(state, cid): """(low, high, kind) of a numeric claim in canonical units, or None. A guidance row is a range; every other claim is a point. Two claims can be ordered only when their kinds match (a rate against a rate, never a rate against a change in basis points).""" d = claim_def(state, cid) if d is None: return None if cid.startswith("GUIDANCE_"): try: m = ((state.get("guidance.company") or {}).get("metrics") or [])[int(cid.split("_")[1]) - 1] except (ValueError, IndexError): return None k = _UNIT_KIND.get(m.get("unit")) lo, hi = m.get("low"), m.get("high") return (lo, hi if hi is not None else lo, k) if (k and lo is not None) else None k = _STYPE_KIND.get(d.stype) v = state.value(d.path) if hasattr(state, "value") else None return (float(v), float(v), k) if (k and isinstance(v, (int, float))) else None def render_guidance(m, mode="value_only"): """One guidance row as text, formatted from its typed unit and quantity type (guidance.render).""" import guidance as _g return _g.render(m, mode) def guidance_basis_label(cat, cid): """'GAAP' / 'adjusted' when a guidance row has a sibling of the same concept on another basis with a different value (the basis then decides which figure is meant); None when the basis does not change the figure.""" from collections.abc import Mapping c0 = cat.get(cid) if str(cid or "").startswith("GUIDANCE_") else None # only a guidance row has a basis v = c0.value if c0 is not None and isinstance(c0.value, Mapping) else {} if not v.get("concept"): return None sib = {(x.get("accounting_basis"), x.get("low"), x.get("high")) for x in (c.value for k, c in cat.items() if k.startswith("GUIDANCE_") and isinstance(c.value, Mapping)) if x.get("concept") == v["concept"]} if len({(lo, hi) for _b, lo, hi in sib}) > 1 and len({b for b, _lo, _hi in sib}) > 1: return {"GAAP": "GAAP", "ADJUSTED": "adjusted"}.get(v.get("accounting_basis")) return None def build_catalog(state, include_deferred=True): """{claim_id: Claim} for every claim whose value exists in the state.""" cat = {} for c in list(CLAIMS) + guidance_claims(state): if c.deferred and not include_deferred: continue cl = claim_for(state, c) if cl.value is not None and cl.rendered is not None: cat[c.claim_id] = cl return cat def persona_comparison_text(state, cat): """The structural comparison permissions a persona receives (computed with compatible(), not asserted in prose).""" bad = comparison_permissions(state, cat) if not bad: return "" lines = ["COMPARISONS NOT ALLOWED (different concept, period basis or business perimeter; a sentence that compares " "them — vs, above, below, exceeds, from … to, decelerates, premium, discount — is removed):"] for a, b, why in bad[:30]: lines.append(f" [[{a}]] ✕ [[{b}]] — {'; '.join(why)[:120]}") return "\n".join(lines) def offered(state, cat, include_deferred=False): """The claim ids an LLM author may cite: facts, complete relations, phrase-rendered signed quantities and the evidence of models whose OWN reliability allows it. Relation magnitudes without their pair, signed returns without their direction and hidden models' claims are never offered.""" import evidence as _ev out = [] for cid, c in cat.items(): d = BY_ID.get(cid) or (claim_def(state, cid) if state is not None else None) if d is None or not d.persona_visible or (c.deferred and not include_deferred): continue if cid in RELATION_PRESENTATION or cid in PHRASE_OF: continue mid = _ev.claim_model(cid) if state is not None and mid and not _ev.model_visible(state, mid): continue out.append(cid) return out def persona_catalog_text(cat, state=None, include_deferred=False): """What an author sees, in three groups: FACT tokens (one quantity), RELATIONSHIP tokens (a complete typed comparison) and EVIDENCE tokens (a model's reliability, with the models it may be used about).""" import evidence as _ev ids = offered(state, cat, include_deferred) rels = [c for c in ids if cat[c].stype == REL] evid = [c for c in ids if c in _ev._EVIDENCE] facts = [c for c in ids if c not in rels and c not in evid] lines = ["NUMBER TOKENS — write every figure ONLY as its token. Never type a number, percentage, dollar amount or multiple " "yourself; a sentence that does is removed. Only the tokens listed below may be used.", "FACT tokens state one quantity. RELATIONSHIP tokens state a complete comparison — its subject, direction, size and " "the other side — as one clause: write it as a clause (e.g. \"Today, [[REL_PRICE_MARKET_COMPOSITE]].\") and never " "restate its direction or attach it to a different subject.", "Two figures of the same kind (two per-share values, two revenues, two margins, two growth rates, or a margin " "CHANGE and a margin LEVEL) may share a sentence ONLY through a relationship token that connects them; otherwise " "put them in separate sentences and do not compare them yourself.", "Whether today's price is more or less demanding (or optimistic) than the story may be stated ONLY by citing " "[[REL_EXPECTATIONS_STORY]], and only as it states.", "Each relationship has up to three forms of the SAME typed relation: [[REL_X]] is a complete clause — it must open " "the sentence or follow a comma, semicolon, colon or a conjunction (and, but, while, because, since, that), and be " "followed by punctuation or a conjunction; never write \"is [[REL_X]]\", \"per [[REL_X]]\" or \"[[REL_X]] shows\". " "Typed slots: [[REL_X__PREDICATE]] carries its own verb and follows its subject directly (\"the price " "[[REL_PRICE_MARKET_COMPOSITE__PREDICATE]]\"); [[REL_X__CLAUSE]] and [[REL_X__COMPACT]] follow the subject AND a verb " "(\"the price sits [[REL_PRICE_MARKET_COMPOSITE__COMPACT]]\"); [[REL_X__NOUN_PHRASE]] is an object (\"the stock trades " "at [[REL_PRICE_INTRINSIC__NOUN_PHRASE]]\"). Never write the other side again after the token. At most two complete " "relationship clauses per sentence.", "Premium, discount, above, below, cheap or expensive about the story value, the mechanical DCF or the market " "composite may be written only with that anchor's relationship token, and only in its direction.", "The break-even revenue and break-even margin are ALTERNATIVE solves (each holds the other constant): never put " "both in one sentence, and never say the price requires both revenue and margin, except through " "[[REL_EXPECTATIONS_STORY]].", "A segment count belongs to a perimeter: [[SEGMENT_COUNT_FILED]] is the business as filed (write it with 'before the " "separation' or 'as filed'); [[SEGMENT_COUNT_CONTINUING]] is the continuing company. Never type a segment count.", "Momentum, ownership, insider and quant evidence cannot establish a trend in the business (e.g. a price below the " "200-day average says nothing about revenue deceleration).", "A trend conclusion (accelerating, decelerating, improving, deteriorating, expanding, contracting) about revenue, " "growth, margins or earnings needs a TREND token; when none is listed there is no compatible earlier figure and no " "such trend may be asserted.", "A comma alone does not join a complete relationship clause to a phrase: write \"At [[PRICE]], [[REL_X]]\" or " "\"…, and [[REL_X]]\", never \"Buybacks at [[PRICE]], [[REL_X]], destroy value\" or \"…, which [[REL_X]]\". A " "relationship token already states its own figures: do not cite them again as separate tokens in that sentence.", "Whether a fundamental (growth, margins, earnings) is stronger or weaker than what the market prices in is a " "statement of expectations: only through [[REL_EXPECTATIONS_STORY]], and only in its direction.", "How the stock has performed (a weak year, a rally, a sell-off, underperformance) needs its signed return token " "in the same sentence ([[RETURN_12M_PHRASE]] for a year, [[RETURN_3M_PHRASE]] for three months).", "A quotation from the call keeps its speaker: an analyst's question is never something the company or management " "acknowledges; quote only words that are in the transcript.", "Keep each figure on its own financial statement: amortization, interest expense, tax rate and EPS are income-" "statement items; capex, buybacks and dividends are cash-flow items.", "[[RETURN_3M_PHRASE]] / [[RETURN_12M_PHRASE]] are predicates (\"down … over twelve months\"): write them after a " "verb or their subject (\"the shares are …\", \"with shares …\"), never after a bare preposition. A token whose " "value is a list (items joined by semicolons) ends its sentence.", "Never compare two different metric families (earnings vs revenue, margins vs revenue, cash flow vs earnings) " "\"faster\", \"slower\" or \"ahead of\" one another: no typed relation connects them.", "\"Close to fair value\", \"fairly valued\" or \"in line with value\" need a valuation relationship token whose " "gap is within 5%; otherwise state the gaps and do not call it close.", "A direction you give your own expected return (bearish, bullish, negative, positive) must match its sign.", "What management said on the call is stated only with its own words in quotation marks (at least three " "words, verbatim from a management answer); never paraphrase what an executive said or acknowledged.", "Verbal magnitudes are numbers: \"mid-single-digit\", \"high-teens\" or \"double-digit\" growth must fit a " "growth figure listed above for that metric. Never spell out a count (\"two adds\"): cite its token.", "Name every money figure and multiple by what it measures (\"revenue of …\", \"a forward P/E of …\").", "You may use [[OWN_RETURN]] for your own scenario return (it renders with its own direction)."] assess = ((state.get("relations.state") or {}).get("assessments") or {}) if state is not None else {} if assess: words = {"CONSERVATIVE": "may be called conservative, never aggressive", "AGGRESSIVE": "may be called aggressive, " "never conservative", "NEUTRAL": "is neither conservative nor aggressive", "NO_HAIRCUT": "applies no haircut: it is never conservative"} lines.append("ASSUMPTION ASSESSMENTS (conservative / aggressive may be said only as stated here; any other story " "assumption, or the assumptions as a group, takes neither word): " + "; ".join(f"[[{c}]] {words.get(a.get('assessment'), 'takes neither word')} ({a.get('basis')})" for c, a in assess.items())) interp = ((state.get("evidence.state") or {}).get("interpretation") or {}) if state is not None else {} for fam, d in interp.items(): if d.get("permission") == "DESCRIPTIVE_ONLY": lines.append(f"{fam} evidence is DESCRIPTIVE ONLY: state the positions, trades or counts; never characterise them " "(weakening, supportive, constructive, fading, a floor, a signal …) or draw a direction from them.") q = lambda cid: (BY_ID.get(cid).required_qualifiers if cid in BY_ID else ()) lines.append("FACT TOKENS (token = value — meaning):") for cid in facts: lines.append(f" [[{cid}]] = {cat[cid].rendered} — {cat[cid].label}" + (f" (write it with one of: {', '.join(q(cid)[:3])})" if q(cid) else "")) if rels: lines.append("RELATIONSHIP TOKENS (token = what it renders; FULL is a clause, __CLAUSE / __COMPACT follow their subject):") lines += [f" [[{cid}]] = \"{cat[cid].rendered}\"" for cid in rels] if evid: lines.append("EVIDENCE TOKENS (usable ONLY about the model named; never about another model):") for cid in evid: src, tg = _ev.evidence_targets(state, cid) if state is not None else (_ev._EVIDENCE[cid][0], [_ev._EVIDENCE[cid][0]]) lines.append(f" [[{cid}]] = {cat[cid].rendered} — {cat[cid].label}; source model {src}; may describe only: " f"{', '.join(tg)}") return "\n".join(lines) def path_claim(state, path, stype, label): """An ad-hoc claim for a renderer number that is not in the named catalog (tables, story path). Its id is derived from the path, so it still resolves structurally to one state location.""" v = _resolve(state, path) cid = "P:" + path return Claim(cid, path, stype, label, v, fmt(stype, v) if v is not None else None) def is_full_clause(cid, cat=None): """A FULL relationship, a trend relationship or a clause-rendered phrase: a complete clause, never a noun phrase.""" d = BY_ID.get(cid) if d is None: return False return (d.stype == REL and d.render_mode == "FULL") or d.clause