QEDGE AMFR Copilot

Packaged agent

Fallen Angel Identifier v1

A script-first multi-agent framework for identifying fallen angels: former compounders in a sustained mid-term guide-down, beaten down hard, with negatives largely exhausted, that still survive. The thesis is mid-term mean reversion on top of long-term compounding. The opposite is a fallen knife — still deteriorating, still falling.

36orthogonal factors
100weight points
8evidence agents
0–2score rubric

Overview

From scoped company to tiered conviction

Fallen Angel Identifier v1 is built on the same machine as Compounder Identifier v2: subagents gather MCP-backed evidence in parallel, Python scripts compute every measurable input, LLM subagents adjudicate only the factors that genuinely require judgment, and a deterministic aggregator produces the score. It differs in three ways that follow from what it is scoring.

It is adversarial where the compounder agent is constructive. The compounder framework judges a moat on its strongest barrier. This framework judges a franchise on its weakest permanence vector: one verified permanent impairment caps Franchise Integrity, and a verified existential vector is a hard gate to AVOID regardless of price.

It imports rather than rebuilds “was-a-compounder.” Factor 9 is the Compounder Identifier v2 composite from the archive. Prior franchise quality is not re-litigated here.

It produces a tier, not only a score. The composite ranks candidates; a deterministic gate ladder assigns CORE / SATELLITE / TRACKING / AVOID / STRANDED / NOT-A-CANDIDATE. Gates are computed from factor scores and hard data, never from prose.

Each factor scores on a compact 0 / 1 / 2 rubric with numeric anchors. Weights sum to 100. A factor contributes score / 2 × weight to a 0–100 composite.

Design

Simple measures, adversarial franchise

  1. Deterministic scripts first — revision streaks, valuation, forensics, drawdowns. Never an LLM guess for a number that can be computed.
  2. One factor per economic mechanism. Dislocation, expectation reset, franchise permanence, normalization, survival, price, and confirmation are separate and are not allowed to score the same fact twice.
  3. One measure per factor, always defined, bands monotone in it. Rubrics assembled from several conditions joined by and/or are not permitted.
  4. Prefer a simple general measure to a precise fragile one. A crude input available for every name beats a better input that is missing half the time.
  5. Direction of estimate revisions outranks magnitude. Magnitude tells you how far it fell; direction tells you whether it is still falling.
  6. Franchise Integrity is prosecuted, not defended: assume the challenger wins until a primary-source rebuttal is on the record.
  7. Diagnostics (Policy-Permanence, AI-Disruption, Short-Seller Ledger) are checklists whose pass rate over the answered checks sets a factor's 0/1/2. A check nobody could answer leaves the denominator rather than counting against the name. They are never a separate score.
  8. Announced is not executed. Buybacks, dividends, and cost programs earn credit only for delivered dollars or delivered margin.
  9. Emit n.m. with a reason rather than a garbage number (Business-Model Law).
  10. Every load-bearing number carries a primary-filing citation or is UNVERIFIED. CORE is impossible while a load-bearing number is UNVERIFIED.
  11. Insufficient evidence produces status: unscored, which drags coverage. It never produces a softened score.
  12. Humans approve; agents inform. No tier is final without the analyst confirm gate and the audit log.
Point-in-time discipline: consensus, guidance, and price series are consumed as-known-then. Fundamentals change only on a new filing; only price-driven columns re-mark to the pricing date. Every factor consuming an expectation carries a pit_basis field on the scorecard.

Trajectory / snapshot legend

Snapshot

Scores the current level, exposure, or structural state at the analysis date.

Trajectory

Scores demonstrated direction, persistence, or change through time.

Snapshot + trajectory

Uses both the current level and the time-series trend; neither alone is sufficient.

Data need legend

Historical / observed

Realized history, latest reported data, or current market observations.

PIT expectation

Point-in-time consensus, guidance, or forecast, as known then, never forwarded.

Historical + expectation

Both realized record and forward evidence; the score separates what was delivered from what is promised.

Peer need legend

company

Company history, filings, and KPIs are sufficient.

peer

Peer context materially improves calibration.

peer/industry

Needs peer, industry, market-share, or market-structure context.

MCP support

Source map and market support matrix

Each factor names one or more data types in its MCP support field. Resolve those data types to concrete MCP slugs by listing market using the matrix below. A dash means there is no reliable structured MCP for that market and the input must be sourced from filings or flagged UNVERIFIED.

Ten capability rows cover the framework. Third-party textis the single “someone other than the issuer wrote this” type. Factor 9's input is an imported Compounder v2 scorecard — an internal archive artifact, not an MCP data type.

MCP source map

wind

China A-share prices, PIT consensus history, macro, and A-share fundamentals. The only source with genuine point-in-time consensus for A-shares.

stockanalysis

Global fundamentals, segment/geography KPIs, analyst forecasts, dividends, buyback history, and transcripts.

defeatbeta

Public equity fundamentals, news, and transcripts, especially US and global equities.

sec-edgar

US domestic filings, XBRL company facts, 10-K/10-Q/8-K/proxy. Locates Form 4 filings but does not extract their transactions; the maturity schedule and the Form 4 line items are read from SEC XBRL and the filing documents directly.

edgar-13f

US institutional holders, filer portfolios, and quarter-over-quarter holding changes — the smart-money factor.

yfinance

Prices, statements, holders, insider transactions, and sector data.

twse

Taiwan (TWSE/TPEx) structured data: fundamentals (income/balance/cash flow, monthly revenue, valuation ratios), adjusted daily-K and TAIEX/TPEx index, dividends/除權息, and institutional/insider/董監持股 holdings. No PIT consensus. Factors 32 (insider) and 34 (holders) stay na for Taiwan — twse returns snapshot holdings, not transaction-level Form 4 / CIK-keyed 13F.

firecrawl

Public web research: HKEX filings, 20-F/6-K retrieval, short-seller reports, regulator notices, industry price data, and the per-quarter consensus-vs-actual proxy.

rag

Ingested broker research and papers; target-multiple history, market-share series, and reference-class precedents.

synology

Long-term archive of finished assessments and the accumulating PIT consensus snapshot store.

Support matrix by data type

Data typeUSHKChina A-shareOther ADR / foreignTaiwan
Fundamentalsstockanalysis, defeatbeta, yfinance, sec-edgarstockanalysiswindstockanalysis, defeatbeta, yfinancetwse
Pricesstockanalysis, yfinance, defeatbetastockanalysis, yfinancewindstockanalysis, yfinancetwse
PIT consensusfirecrawl → earnings-history pages; wind monthly where coveredfirecrawl → earnings-history pageswind, monthlyfirecrawl → earnings-history pagesfirecrawl → earnings-history pages
Segment / KPIsec-edgar, stockanalysisstockanalysis, firecrawlwind, firecrawlfirecrawl (20-F), stockanalysistwse (monthly revenue), firecrawl (公開資訊觀測站)
Filing-textsec-edgar, ragfirecrawl (HKEX), ragfirecrawl, ragfirecrawl (20-F/6-K), ragfirecrawl (公開資訊觀測站), rag
Transcriptdefeatbeta, stockanalysis, ragstockanalysis, ragstockanalysis, ragdefeatbeta, stockanalysis, ragfirecrawl (法說會), rag
InsiderForm 4 XML via sec-edgarnone resolved — factor 32 nanone resolved — factor 32 naForm 4 XML where the issuer is a Section 16 registrant; na for foreign private issuersnone resolved — factor 32 na
Holders / 13Fedgar-13f, yfinancefirecrawl (HKEX), ragwindedgar-13f (works for ADRs), yfinancenone resolved — factor 34 na
Third-party textrag, firecrawl, defeatbeta, yfinancerag, firecrawlwind, rag, firecrawlrag, firecrawl, defeatbetarag, firecrawl

Fundamentals

US
stockanalysis, defeatbeta, yfinance, sec-edgar
HK
stockanalysis
China A-share
wind
Other ADR / foreign
stockanalysis, defeatbeta, yfinance
Taiwan
twse

Prices

US
stockanalysis, yfinance, defeatbeta
HK
stockanalysis, yfinance
China A-share
wind
Other ADR / foreign
stockanalysis, yfinance
Taiwan
twse

PIT consensus

US
firecrawl → earnings-history pages; wind monthly where covered
HK
firecrawl → earnings-history pages
China A-share
wind, monthly
Other ADR / foreign
firecrawl → earnings-history pages
Taiwan
firecrawl → earnings-history pages

Segment / KPI

US
sec-edgar, stockanalysis
HK
stockanalysis, firecrawl
China A-share
wind, firecrawl
Other ADR / foreign
firecrawl (20-F), stockanalysis
Taiwan
twse (monthly revenue), firecrawl (公開資訊觀測站)

Filing-text

US
sec-edgar, rag
HK
firecrawl (HKEX), rag
China A-share
firecrawl, rag
Other ADR / foreign
firecrawl (20-F/6-K), rag
Taiwan
firecrawl (公開資訊觀測站), rag

Transcript

US
defeatbeta, stockanalysis, rag
HK
stockanalysis, rag
China A-share
stockanalysis, rag
Other ADR / foreign
defeatbeta, stockanalysis, rag
Taiwan
firecrawl (法說會), rag

Insider

US
Form 4 XML via sec-edgar
HK
none resolved — factor 32 na
China A-share
none resolved — factor 32 na
Other ADR / foreign
Form 4 XML where the issuer is a Section 16 registrant; na for foreign private issuers
Taiwan
none resolved — factor 32 na

Holders / 13F

US
edgar-13f, yfinance
HK
firecrawl (HKEX), rag
China A-share
wind
Other ADR / foreign
edgar-13f (works for ADRs), yfinance
Taiwan
none resolved — factor 34 na

Third-party text

US
rag, firecrawl, defeatbeta, yfinance
HK
rag, firecrawl
China A-share
wind, rag, firecrawl
Other ADR / foreign
rag, firecrawl, defeatbeta
Taiwan
rag, firecrawl

Factor model

36 factors across 7 categories

Factors are grouped by economic mechanism. Each row shows trajectory type, data and peer requirements, weight, scoring rubric, calculation summary, and MCP support.

9% of framework

Dislocation & De-rating

How far it fell, how far the multiple de-rated versus its own history, and whether the fall has matured.

#FactorTypeDataPeerWtRubric (0/1/2)
1Drawdown depth and excesshow far the price has fallen, absolute and relative to its own market.`dd` = 1 − P_t / max(P) over trailing 60m (min 36m), on split- and dividend-adjusted prices. `excess_dd` = `dd` − benchmark drawdown over the same window, in pp. The benchmark is the name's economic-exposure index rather than its listing venue's (for a China ADR: MSCI China / HSCEI, not the S&P 500), resolved from revenue geography by a fixed rule and recorded as `inputs.benchmark`.MCP data types: PricesSnapshotHistorical / observedpeer3Score on `dd`, then apply the excess test: 0: `dd` < 35% · 1: `dd` 35–55% · 2: `dd` ≥ 55%. Excess adjustment: subtract 1 (floor 0) when `excess_dd` < 10pp — the fall is the market's, not the name's. Add nothing for high excess; depth already carries it.
2Multiple de-rating versus own historywhere the multiple sits in its own distribution.`ps_pctile` = percentile of the current P/S (market cap ÷ TTM revenue) in the name's own trailing 7y (min 5y) monthly distribution, each month using only the revenue reported by that month. `ps_ratio` = `ps_now` ÷ `ps_median`. Score = max of the two bands. Rank alone understates a deep derating once the crash fills the bottom of the window — a name at 0.2× its own median can sit above the 10th percentile because other crash months sit below it. Magnitude alone understates a stable-multiple name that is at a record low by rank but still near its typical level. Taking the better of the two closes each failure mode without inventing a third. P/S is the basis, not a ladder. Revenue is positive for every operating company in every period, so the measure is always defined and the distribution never has to exclude months — where P/E, EV/EBITDA and EV/EBIT all go `n.m.` for exactly the loss-making names this framework screens. Report `ps_now`, `ps_median`, `ps_ratio` and `ps_min` alongside the percentile so the de-rating is legible without opening the series.MCP data types: FundamentalsPricesThird-party textSnapshot + trajectoryHistorical / observedcompany3Two legs, take the better band. Rank: 0: `ps_pctile` > 40th · 1: ≤ 40th · 2: ≤ 10th. Magnitude: 0: `ps_now/ps_median` > 0.70 · 1: ≤ 0.70 · 2: ≤ 0.35.
3Decline maturity and base-buildingwhether the fall is still in progress or has stopped.Months since the trailing-12m low. `off_low` = P_t / P_low − 1 — the rise off the bottom, and the only retrace measure used. The peak-relative measure `(P_t − P_low)/(P_peak − P_low)` is deliberately not used: after a 90% decline, retracing 20% of the fall requires a ~250% rally, so the two definitions cannot agree on exactly the population this framework screens. Band 1 is the residual of the other two, so the three bands partition every possible pair. "No new 12m low for six months" is the same fact as `months_since_low` ≥ 6 when the low is searched over the trailing twelve months, so it is not a third test. Rewards late entry — the "dare to be last in the world" principle.MCP data types: PricesTrajectoryHistorical / observedcompany30: `months_since_low` < 3 · 1: neither band 0 nor band 2 — the low is ≥ 3 months old but short of both ≥ 6 months and `off_low` ≥ 20% · 2: `months_since_low` ≥ 6 and `off_low` ≥ 20%
#13%

Drawdown depth and excess

how far the price has fallen, absolute and relative to its own market.

Type
Snapshot
Data
Historical / observed
Peer
peer

Rubric: Score on `dd`, then apply the excess test: 0: `dd` < 35% · 1: `dd` 35–55% · 2: `dd` ≥ 55%. Excess adjustment: subtract 1 (floor 0) when `excess_dd` < 10pp — the fall is the market's, not the name's. Add nothing for high excess; depth already carries it.

`dd` = 1 − P_t / max(P) over trailing 60m (min 36m), on split- and dividend-adjusted prices. `excess_dd` = `dd` − benchmark drawdown over the same window, in pp. The benchmark is the name's economic-exposure index rather than its listing venue's (for a China ADR: MSCI China / HSCEI, not the S&P 500), resolved from revenue geography by a fixed rule and recorded as `inputs.benchmark`.

MCP data types: Prices

#23%

Multiple de-rating versus own history

where the multiple sits in its own distribution.

Type
Snapshot + trajectory
Data
Historical / observed
Peer
company

Rubric: Two legs, take the better band. Rank: 0: `ps_pctile` > 40th · 1: ≤ 40th · 2: ≤ 10th. Magnitude: 0: `ps_now/ps_median` > 0.70 · 1: ≤ 0.70 · 2: ≤ 0.35.

`ps_pctile` = percentile of the current P/S (market cap ÷ TTM revenue) in the name's own trailing 7y (min 5y) monthly distribution, each month using only the revenue reported by that month. `ps_ratio` = `ps_now` ÷ `ps_median`. Score = max of the two bands. Rank alone understates a deep derating once the crash fills the bottom of the window — a name at 0.2× its own median can sit above the 10th percentile because other crash months sit below it. Magnitude alone understates a stable-multiple name that is at a record low by rank but still near its typical level. Taking the better of the two closes each failure mode without inventing a third. P/S is the basis, not a ladder. Revenue is positive for every operating company in every period, so the measure is always defined and the distribution never has to exclude months — where P/E, EV/EBITDA and EV/EBIT all go `n.m.` for exactly the loss-making names this framework screens. Report `ps_now`, `ps_median`, `ps_ratio` and `ps_min` alongside the percentile so the de-rating is legible without opening the series.

MCP data types: FundamentalsPricesThird-party text

#33%

Decline maturity and base-building

whether the fall is still in progress or has stopped.

Type
Trajectory
Data
Historical / observed
Peer
company

Rubric: 0: `months_since_low` < 3 · 1: neither band 0 nor band 2 — the low is ≥ 3 months old but short of both ≥ 6 months and `off_low` ≥ 20% · 2: `months_since_low` ≥ 6 and `off_low` ≥ 20%

Months since the trailing-12m low. `off_low` = P_t / P_low − 1 — the rise off the bottom, and the only retrace measure used. The peak-relative measure `(P_t − P_low)/(P_peak − P_low)` is deliberately not used: after a 90% decline, retracing 20% of the fall requires a ~250% rally, so the two definitions cannot agree on exactly the population this framework screens. Band 1 is the residual of the other two, so the three bands partition every possible pair. "No new 12m low for six months" is the same fact as `months_since_low` ≥ 6 when the low is searched over the trailing twelve months, so it is not a third test. Rewards late entry — the "dare to be last in the world" principle.

MCP data types: Prices

16% of framework

Revision Cycle & Expectations Reset

The number-cutting (*kanshuzi*) engine: down-revision streak, cut magnitude, guidance misses, cut deceleration, and the post-reset beat.

#FactorTypeDataPeerWtRubric (0/1/2)
4Reset persistencehow long the numbers have been coming down.`n_down` = how many of the last 8 quarter-over-quarter changes in TTM EBIT were negative, on quarters visible at the as-of date by report date. Measures persistence, not depth — factor 16 scores depth, and a company can be deeply depressed after one bad quarter or mildly depressed after eight declining ones.MCP data types: FundamentalsTrajectoryHistorical + expectationcompany30: `n_down` ≤ 3 · 1: 4–5 · 2: ≥ 6
5Growth-centre resethow much of the top line has gone.`rev_reset` = TTM revenue ÷ its own trailing 5-year peak TTM revenue − 1. Revenue rather than earnings, which keeps this independent of factors 4 and 16: a franchise can hold its top line while margins collapse, and the two cases deserve different scores. The peak must come from at least 3 years of revenue history. A maximum taken over a shorter stretch is a recent level rather than a peak, and because a subset maximum is never above the true one, the reset it produces is never more negative than the truth — so a short-history band is a floor: band 2 stands, and bands 0 and 1 go `unscored` rather than report the length of the fetch as a fact about the company.MCP data types: FundamentalsTrajectoryHistorical + expectationcompany20: `rev_reset` > −10% · 1: −30% to −10% · 2: ≤ −30%
6Negative-surprise streakthe company-side mirror of the cuts, independent of the trajectory.Consensus-versus-actual per quarter from the quarterly proxy, each quarter dated from the transcript index and never from the page being parsed. A quarter is `usable` only if it carries an explicit beat/miss determination and passes the tie-out test below. The floor is 4 rather than 6 because a half-covered ledger still discriminates a serial misser from a serial beater, and returning nothing discriminates neither.MCP data types: FundamentalsPIT consensusTranscriptTrajectoryHistorical + expectationcompany30: `n_miss` ≤ 1 · 1: `n_miss` ≥ 2 and `max_consec_miss` < 3 · 2: `max_consec_miss` ≥ 3. Coverage floor: `unscored` below 4 usable quarters of the last 8.
7Inflection — has the fall stopped?the most decisive input in the framework, and a hard gate.`d_recent` = change in TTM EBIT over the last 2 reported quarters, as a share of TTM revenue; `d_prior` = the change over the 2 before those, scaled the same way. The base EBIT is deliberately not the denominator: on this population earnings cross zero, and a ratio taken across that crossing is dominated by how close the base landed to zero. EBIT of 200, then 0.6, then −100 is a decline that halved, yet against the base it reads −1.00 then −168 and scores KNIFE. Revenue is positive in every period for every operating company, the same reason factor 2 prices off sales, so each window is a change in margin points. Three bands partition every possible pair, so this can never return `unscored` for a company that reports. Score 0 fires hard gate G5. Where true monthly consensus exists (A-shares) it is recorded alongside and may fire G5 on its own — it can tighten the gate, never loosen it.MCP data types: FundamentalsPIT consensusTrajectoryHistorical + expectationcompany40 (KNIFE): `d_recent` < 0 and `d_recent` ≤ `d_prior` — still falling and not decelerating · 1: `d_recent` < 0 and `d_recent` > `d_prior` — still falling but decelerating · 2: `d_recent` ≥ 0 — stopped falling
8Post-reset beat deliveredthe bar was reset and then cleared.The reset quarter is the most recent miss in the usable ledger; count clean beats (actual ≥ consensus) after it. Distinct from factor 7 by construction: factor 7 asks whether the company's own results have stopped deteriorating, this asks whether it is clearing the bar others set. A company can do either without the other. `unscored` only when the ledger contains no miss at all, in which case there was no reset to clear.MCP data types: PIT consensusTranscriptTrajectoryHistorical + expectationcompany40: no beat since the reset quarter · 1: exactly 1 clean beat since · 2: ≥ 2 clean beats since
#43%

Reset persistence

how long the numbers have been coming down.

Type
Trajectory
Data
Historical + expectation
Peer
company

Rubric: 0: `n_down` ≤ 3 · 1: 4–5 · 2: ≥ 6

`n_down` = how many of the last 8 quarter-over-quarter changes in TTM EBIT were negative, on quarters visible at the as-of date by report date. Measures persistence, not depth — factor 16 scores depth, and a company can be deeply depressed after one bad quarter or mildly depressed after eight declining ones.

MCP data types: Fundamentals

#52%

Growth-centre reset

how much of the top line has gone.

Type
Trajectory
Data
Historical + expectation
Peer
company

Rubric: 0: `rev_reset` > −10% · 1: −30% to −10% · 2: ≤ −30%

`rev_reset` = TTM revenue ÷ its own trailing 5-year peak TTM revenue − 1. Revenue rather than earnings, which keeps this independent of factors 4 and 16: a franchise can hold its top line while margins collapse, and the two cases deserve different scores. The peak must come from at least 3 years of revenue history. A maximum taken over a shorter stretch is a recent level rather than a peak, and because a subset maximum is never above the true one, the reset it produces is never more negative than the truth — so a short-history band is a floor: band 2 stands, and bands 0 and 1 go `unscored` rather than report the length of the fetch as a fact about the company.

MCP data types: Fundamentals

#63%

Negative-surprise streak

the company-side mirror of the cuts, independent of the trajectory.

Type
Trajectory
Data
Historical + expectation
Peer
company

Rubric: 0: `n_miss` ≤ 1 · 1: `n_miss` ≥ 2 and `max_consec_miss` < 3 · 2: `max_consec_miss` ≥ 3. Coverage floor: `unscored` below 4 usable quarters of the last 8.

Consensus-versus-actual per quarter from the quarterly proxy, each quarter dated from the transcript index and never from the page being parsed. A quarter is `usable` only if it carries an explicit beat/miss determination and passes the tie-out test below. The floor is 4 rather than 6 because a half-covered ledger still discriminates a serial misser from a serial beater, and returning nothing discriminates neither.

MCP data types: FundamentalsPIT consensusTranscript

#74%

Inflection — has the fall stopped?

the most decisive input in the framework, and a hard gate.

Type
Trajectory
Data
Historical + expectation
Peer
company

Rubric: 0 (KNIFE): `d_recent` < 0 and `d_recent` ≤ `d_prior` — still falling and not decelerating · 1: `d_recent` < 0 and `d_recent` > `d_prior` — still falling but decelerating · 2: `d_recent` ≥ 0 — stopped falling

`d_recent` = change in TTM EBIT over the last 2 reported quarters, as a share of TTM revenue; `d_prior` = the change over the 2 before those, scaled the same way. The base EBIT is deliberately not the denominator: on this population earnings cross zero, and a ratio taken across that crossing is dominated by how close the base landed to zero. EBIT of 200, then 0.6, then −100 is a decline that halved, yet against the base it reads −1.00 then −168 and scores KNIFE. Revenue is positive in every period for every operating company, the same reason factor 2 prices off sales, so each window is a change in margin points. Three bands partition every possible pair, so this can never return `unscored` for a company that reports. Score 0 fires hard gate G5. Where true monthly consensus exists (A-shares) it is recorded alongside and may fire G5 on its own — it can tighten the gate, never loosen it.

MCP data types: FundamentalsPIT consensus

#84%

Post-reset beat delivered

the bar was reset and then cleared.

Type
Trajectory
Data
Historical + expectation
Peer
company

Rubric: 0: no beat since the reset quarter · 1: exactly 1 clean beat since · 2: ≥ 2 clean beats since

The reset quarter is the most recent miss in the usable ledger; count clean beats (actual ≥ consensus) after it. Distinct from factor 7 by construction: factor 7 asks whether the company's own results have stopped deteriorating, this asks whether it is clearing the bar others set. A company can do either without the other. `unscored` only when the ledger contains no miss at all, in which case there was no reset to clear.

MCP data types: PIT consensusTranscript

21% of framework

Franchise Integrity

Is the moat permanently broken? Prior quality, pricing power through the trough, and four permanence vectors.

#FactorTypeDataPeerWtRubric (0/1/2)
9Prior franchise quality (imported)was this actually a compounder before the fall?`compounder_score` = the composite from the most recent Compounder Identifier v2 scorecard on file for this entity, whatever its as-of date — including when that date is after the fallen-angel as-of. Compounder scores are long-horizon franchise reads, not shock-sensitive PIT series; never exclude them as `pit_leakage` or leave the factor `unscored` because the assessment post-dates the run. Never rebuilt here — `fallen-angel-archivist` reads it from the saved-scorecard archive. Record `compounder_asof` alongside the score; where that date falls after the drawdown began, flag `compounder_basis: post_shock`; where it falls after the fallen-angel as-of, flag `as_of_vs_run: post_asof` and `pit_exempt: true` — both are flags, not refusals. No separate pre-versus-now comparison is attempted: most names have been run once.MCP data types: Imported scorecardSnapshotHistorical / observedpeer40: `compounder_score` < 45 · 1: 45–64 · 2: ≥ 65
10Pricing power through the troughdid the realized price hold while volumes fell?Primary basis, always available: `gm_change` = gross margin now minus its pre-shock 5-year median, in pp. A company forced to discount loses gross margin; one whose realized price held does not. Gross margin is a reported field for every non-financial issuer, which is why it is the basis rather than the fallback. What the margin cannot do on its own is separate a discount from a cost base that did not shrink with the volume, and this factor is defined as gross margin down more than volume and mix explain, so band 0 — the claim that this company was forced to discount — needs one of two further readings and yields to either. Peer leg: `gm_change_vs_cohort_median` runs the identical calculation over the `scoper.json` cohort, ≥ 3 members qualifying, and subtracts the median member; a category where every participant lost the same margin to the same input cost is not one where every participant lost pricing power. Price leg: where ASP, take-rate, ARPU or same-store price is disclosed, record `disclosed_price_change` and `price_series_type`; where a volume series is disclosed instead, `implied_price_change` is revenue per unit against its own pre-shock median, with `volume_change` beside it. A disclosed fall > 5% caps at 1, since cost relief can mask a price cut; a price that held floors at 1, since a margin that fell while the price held fell on cost. The implied price is mix-contaminated by construction — a company exiting its cheapest line reads as having raised price — so it blocks band 0 and can never reach band 2. Every supporting reading is optional and its absence never blocks a score; where none exists, band 0 records that it never separated the two.MCP data types: FundamentalsSegment / KPITranscriptTrajectoryHistorical / observedpeer40: `gm_change` ≤ −5pp · 1: −5pp to −2pp · 2: ≥ −2pp · capped at 1 when a disclosed realized price fell > 5% · floored at 1 when band 0 is not settled, meaning `gm_change_vs_cohort_median` ≥ −2pp or a realized price held within 2% of its pre-shock median
11Competitive displacementpermanence vectoris a better challenger permanently taking the business?Primary: dated share observations inside `scoper.competitive_arena`, read for what their length supports. The arena is named by `fa-scoper` as of the as-of date; a series that does not stamp that definition (a surviving segment, an abolished segment, any other denominator) is refused. Two observations establish a direction and score the full rubric capped at 1; ≥ 3 spanning ≥ 3 years lift the cap and are what band 0 requires, since a challenger permanently taking the business is not a claim two points support. Full share histories are rare enough that admitting only those sent almost every name to the fallback. Fallback when no share direction exists, and still the common path: `rel_rev_growth` = company revenue CAGR − peer-cohort median CAGR over the trough window; 2 if ≥ −2pp, otherwise 1. It cannot reach band 0 — a cohort-median growth gap names no challenger, which is what band 0 asserts — and is flagged `basis: relative_growth` but not capped, since growing at or above the cohort under the same shock is evidence of no displacement, and capping it made score 2 unreachable and stranded factor 18's evidence gate. The trough window must bracket the reference date as periods, not as labels — the base year must close before it and most of the closing year must follow it — since a May fiscal year makes "2021" a year that ended before a mid-2021 shock. A gap measured where the shock never landed is `unscored`, not 0. Where no annual window can bracket the shock, which is every name whose fiscal year closes within months of its own peak, the gap is rebuilt on trailing-twelve-month quarters and `growth_basis` becomes `ttm_quarterly`; quarters are admitted by publication date and never by period end, peers are anchored to the subject's window rather than their own latest filing, a member staggered more than 45 days off the subject leaves, and the two trailing years must not overlap. Without a quarterly series the annual refusal stands.MCP data types: FundamentalsSegment / KPIThird-party textTrajectoryHistorical / observedpeer/industry30: arena share down ≥ 5pp or ≥ 25% relative, with a single identified challenger capturing it, on a series long enough to show the fall held · 1: share down < 5pp, losses spread across many players, or a share direction too short to have held · 2: share flat or up over the trough window
12Policy and regulatory permanencepermanence vectortemporary scare or regime change?Policy-Permanence Diagnostic — eight named checks, each answered ANGEL / TRAP / `unknown` with a primary citation and a `source_type` drawn from the classes listed for that check. `score_diagnostic_checklists.py` converts the pass rate over answered checks to the band; at full coverage this is the published 6-of-8 and 3-of-8. Unanswered, unknown, unsourced and off-record checks leave the denominator rather than counting against the name — a checklist scored out of a fixed eight makes "we could not find out" identical to "the answer is no" on a factor that fires G4. `na` when shock type is not policy/regulatory and there is no regulatory overhang.MCP data types: Filing-textThird-party textSnapshot + trajectoryHistorical + expectationcompany30: the business model is banned or structurally capped with no compliant path, or under 3/8 of the answered checks pass · 1: 3/8 to under 6/8 of answered pass · 2: ≥ 6/8 of answered pass and the company has operated compliantly under the new regime for ≥ 2 consecutive reporting periods · `unscored`: fewer than 4 of the 8 checks could be answered, or a disqualifier contradicts a passing check
13Secular demand durabilitypermanence vectoris the category itself dying?Primary basis, always available: `cohort_cagr` = aggregate revenue CAGR of the peer cohort with the company excluded, annualized over the trough window. The cohort is already built for factor 11, so this needs no new source, and a category in genuine secular decline shrinks every participant in it. The window length travels with the number as `cohort_cagr_years`: a window shorter than 3 years fixes the direction but not that it is secular and caps the factor at 1, and a window that closes before the reference date or opens after it measures a category the shock never touched and leaves the factor `unscored` — the same guard factor 11 applies to its own fallback. It also inherits factor 11's trailing-year rebuild: where no annual window brackets the shock the aggregate is recomputed on trailing-twelve-month quarters, which is almost always a window under 3 years and so carries the cap. Below three surviving cohort members the aggregate is withheld entirely, since one company is not a category. Where a category unit-volume series or a 3-year forward category forecast exists it is recorded and overrides the proxy, with `basis: category_volume` on the scorecard. Demand-side permanence only — do not re-score competitive share here, and a policy action that bans supply is an input to factor 12 rather than evidence of a dying category.MCP data types: FundamentalsSegment / KPIThird-party textTrajectoryHistorical + expectationpeer/industry30: `cohort_cagr` ≤ −3%/yr · 1: −3% to +3%/yr (maturing) · 2: ≥ +3%/yr · capped at 1 when the window spans fewer than 3 years · `unscored` when the window does not bracket the reference date
14AI and technology disruption verdictpermanence vectorwhich layer does the technology hit?AI-Disruption Diagnostic — twelve named tests scored from primary evidence, each carrying a citation and a `source_type` drawn from the classes that can answer that test. Bands are taken over answered tests on the same rule as factor 12, and are the published 4-of-12 and 9-of-12 at full coverage. Tests about the subject are answered by its own filings and calls; `no_ai_native_competitor` and `customers_not_rebuilding_internally` ask what the rest of the market is doing and admit no filing, because an issuer does not report the absence of its own competitors — and those two are also the pair that fires the disqualifier, so a pass there taken from a record that could not have found the thing switches the disqualifier off. Score 0 fires hard gate G4. Never score the category; score the name.MCP data types: TranscriptThird-party textSnapshot + trajectoryHistorical + expectationpeer30 DISRUPTED: any existential vector verified (job-to-be-done disappears; or both the customer-rebuild and AI-native-competitor tests fail), or ≤ 4/12 of the answered tests pass · 1 DEFENSIVE: over 4/12 to under 9/12 of answered pass, the job persists, pressure is production-layer only, and the annuity-runoff floor supports today's price · 2 BENEFICIARY: ≥ 9/12 of answered pass and monetization evidence exists — AI-product pricing uplift ≥ 10% or growth re-acceleration ≥ 3pp · `unscored`: fewer than 6 of the 12 tests could be answered
15Short-seller ledger net assessmentwhat survives independent verification?Every short and bear report in the last 24 months logged in the Short-Seller Ledger and tested independently. Routing rule: fraud, accounting, and solvency allegations route to factor 22, not here; moat and demand allegations route to 11–14.MCP data types: Third-party textSnapshotHistorical / observedcompany10: ≥ 1 new structural allegation independently verified and un-refuted · 1: allegations mixed, partially verified, or the rebuttal is incomplete · 2: no reports, or every allegation refuted with primary evidence or shown to be repackaged old news
#94%

Prior franchise quality (imported)

was this actually a compounder before the fall?

Type
Snapshot
Data
Historical / observed
Peer
peer

Rubric: 0: `compounder_score` < 45 · 1: 45–64 · 2: ≥ 65

`compounder_score` = the composite from the most recent Compounder Identifier v2 scorecard on file for this entity, whatever its as-of date — including when that date is after the fallen-angel as-of. Compounder scores are long-horizon franchise reads, not shock-sensitive PIT series; never exclude them as `pit_leakage` or leave the factor `unscored` because the assessment post-dates the run. Never rebuilt here — `fallen-angel-archivist` reads it from the saved-scorecard archive. Record `compounder_asof` alongside the score; where that date falls after the drawdown began, flag `compounder_basis: post_shock`; where it falls after the fallen-angel as-of, flag `as_of_vs_run: post_asof` and `pit_exempt: true` — both are flags, not refusals. No separate pre-versus-now comparison is attempted: most names have been run once.

MCP data types: Imported scorecard

#104%

Pricing power through the trough

did the realized price hold while volumes fell?

Type
Trajectory
Data
Historical / observed
Peer
peer

Rubric: 0: `gm_change` ≤ −5pp · 1: −5pp to −2pp · 2: ≥ −2pp · capped at 1 when a disclosed realized price fell > 5% · floored at 1 when band 0 is not settled, meaning `gm_change_vs_cohort_median` ≥ −2pp or a realized price held within 2% of its pre-shock median

Primary basis, always available: `gm_change` = gross margin now minus its pre-shock 5-year median, in pp. A company forced to discount loses gross margin; one whose realized price held does not. Gross margin is a reported field for every non-financial issuer, which is why it is the basis rather than the fallback. What the margin cannot do on its own is separate a discount from a cost base that did not shrink with the volume, and this factor is defined as gross margin down more than volume and mix explain, so band 0 — the claim that this company was forced to discount — needs one of two further readings and yields to either. Peer leg: `gm_change_vs_cohort_median` runs the identical calculation over the `scoper.json` cohort, ≥ 3 members qualifying, and subtracts the median member; a category where every participant lost the same margin to the same input cost is not one where every participant lost pricing power. Price leg: where ASP, take-rate, ARPU or same-store price is disclosed, record `disclosed_price_change` and `price_series_type`; where a volume series is disclosed instead, `implied_price_change` is revenue per unit against its own pre-shock median, with `volume_change` beside it. A disclosed fall > 5% caps at 1, since cost relief can mask a price cut; a price that held floors at 1, since a margin that fell while the price held fell on cost. The implied price is mix-contaminated by construction — a company exiting its cheapest line reads as having raised price — so it blocks band 0 and can never reach band 2. Every supporting reading is optional and its absence never blocks a score; where none exists, band 0 records that it never separated the two.

MCP data types: FundamentalsSegment / KPITranscript

#113%

Competitive displacement

permanence vector

is a better challenger permanently taking the business?

Type
Trajectory
Data
Historical / observed
Peer
peer/industry

Rubric: 0: arena share down ≥ 5pp or ≥ 25% relative, with a single identified challenger capturing it, on a series long enough to show the fall held · 1: share down < 5pp, losses spread across many players, or a share direction too short to have held · 2: share flat or up over the trough window

Primary: dated share observations inside `scoper.competitive_arena`, read for what their length supports. The arena is named by `fa-scoper` as of the as-of date; a series that does not stamp that definition (a surviving segment, an abolished segment, any other denominator) is refused. Two observations establish a direction and score the full rubric capped at 1; ≥ 3 spanning ≥ 3 years lift the cap and are what band 0 requires, since a challenger permanently taking the business is not a claim two points support. Full share histories are rare enough that admitting only those sent almost every name to the fallback. Fallback when no share direction exists, and still the common path: `rel_rev_growth` = company revenue CAGR − peer-cohort median CAGR over the trough window; 2 if ≥ −2pp, otherwise 1. It cannot reach band 0 — a cohort-median growth gap names no challenger, which is what band 0 asserts — and is flagged `basis: relative_growth` but not capped, since growing at or above the cohort under the same shock is evidence of no displacement, and capping it made score 2 unreachable and stranded factor 18's evidence gate. The trough window must bracket the reference date as periods, not as labels — the base year must close before it and most of the closing year must follow it — since a May fiscal year makes "2021" a year that ended before a mid-2021 shock. A gap measured where the shock never landed is `unscored`, not 0. Where no annual window can bracket the shock, which is every name whose fiscal year closes within months of its own peak, the gap is rebuilt on trailing-twelve-month quarters and `growth_basis` becomes `ttm_quarterly`; quarters are admitted by publication date and never by period end, peers are anchored to the subject's window rather than their own latest filing, a member staggered more than 45 days off the subject leaves, and the two trailing years must not overlap. Without a quarterly series the annual refusal stands.

MCP data types: FundamentalsSegment / KPIThird-party text

#123%

Policy and regulatory permanence

permanence vector

temporary scare or regime change?

Type
Snapshot + trajectory
Data
Historical + expectation
Peer
company

Rubric: 0: the business model is banned or structurally capped with no compliant path, or under 3/8 of the answered checks pass · 1: 3/8 to under 6/8 of answered pass · 2: ≥ 6/8 of answered pass and the company has operated compliantly under the new regime for ≥ 2 consecutive reporting periods · `unscored`: fewer than 4 of the 8 checks could be answered, or a disqualifier contradicts a passing check

Policy-Permanence Diagnostic — eight named checks, each answered ANGEL / TRAP / `unknown` with a primary citation and a `source_type` drawn from the classes listed for that check. `score_diagnostic_checklists.py` converts the pass rate over answered checks to the band; at full coverage this is the published 6-of-8 and 3-of-8. Unanswered, unknown, unsourced and off-record checks leave the denominator rather than counting against the name — a checklist scored out of a fixed eight makes "we could not find out" identical to "the answer is no" on a factor that fires G4. `na` when shock type is not policy/regulatory and there is no regulatory overhang.

MCP data types: Filing-textThird-party text

#133%

Secular demand durability

permanence vector

is the category itself dying?

Type
Trajectory
Data
Historical + expectation
Peer
peer/industry

Rubric: 0: `cohort_cagr` ≤ −3%/yr · 1: −3% to +3%/yr (maturing) · 2: ≥ +3%/yr · capped at 1 when the window spans fewer than 3 years · `unscored` when the window does not bracket the reference date

Primary basis, always available: `cohort_cagr` = aggregate revenue CAGR of the peer cohort with the company excluded, annualized over the trough window. The cohort is already built for factor 11, so this needs no new source, and a category in genuine secular decline shrinks every participant in it. The window length travels with the number as `cohort_cagr_years`: a window shorter than 3 years fixes the direction but not that it is secular and caps the factor at 1, and a window that closes before the reference date or opens after it measures a category the shock never touched and leaves the factor `unscored` — the same guard factor 11 applies to its own fallback. It also inherits factor 11's trailing-year rebuild: where no annual window brackets the shock the aggregate is recomputed on trailing-twelve-month quarters, which is almost always a window under 3 years and so carries the cap. Below three surviving cohort members the aggregate is withheld entirely, since one company is not a category. Where a category unit-volume series or a 3-year forward category forecast exists it is recorded and overrides the proxy, with `basis: category_volume` on the scorecard. Demand-side permanence only — do not re-score competitive share here, and a policy action that bans supply is an input to factor 12 rather than evidence of a dying category.

MCP data types: FundamentalsSegment / KPIThird-party text

#143%

AI and technology disruption verdict

permanence vector

which layer does the technology hit?

Type
Snapshot + trajectory
Data
Historical + expectation
Peer
peer

Rubric: 0 DISRUPTED: any existential vector verified (job-to-be-done disappears; or both the customer-rebuild and AI-native-competitor tests fail), or ≤ 4/12 of the answered tests pass · 1 DEFENSIVE: over 4/12 to under 9/12 of answered pass, the job persists, pressure is production-layer only, and the annuity-runoff floor supports today's price · 2 BENEFICIARY: ≥ 9/12 of answered pass and monetization evidence exists — AI-product pricing uplift ≥ 10% or growth re-acceleration ≥ 3pp · `unscored`: fewer than 6 of the 12 tests could be answered

AI-Disruption Diagnostic — twelve named tests scored from primary evidence, each carrying a citation and a `source_type` drawn from the classes that can answer that test. Bands are taken over answered tests on the same rule as factor 12, and are the published 4-of-12 and 9-of-12 at full coverage. Tests about the subject are answered by its own filings and calls; `no_ai_native_competitor` and `customers_not_rebuilding_internally` ask what the rest of the market is doing and admit no filing, because an issuer does not report the absence of its own competitors — and those two are also the pair that fires the disqualifier, so a pass there taken from a record that could not have found the thing switches the disqualifier off. Score 0 fires hard gate G4. Never score the category; score the name.

MCP data types: TranscriptThird-party text

#151%

Short-seller ledger net assessment

what survives independent verification?

Type
Snapshot
Data
Historical / observed
Peer
company

Rubric: 0: ≥ 1 new structural allegation independently verified and un-refuted · 1: allegations mixed, partially verified, or the rebuttal is incomplete · 2: no reports, or every allegation refuted with primary evidence or shown to be repackaged old news

Every short and bear report in the last 24 months logged in the Short-Seller Ledger and tested independently. Routing rule: fraud, accounting, and solvency allegations route to factor 22, not here; moat and demand allegations route to 11–14.

MCP data types: Third-party text

18% of framework

Normalization & Reversibility

The bounce-back: depression versus a credible normal, the shock decomposition, and what must be true.

#FactorTypeDataPeerWtRubric (0/1/2)
16Earnings depression versus a credible normalhow much earning power is currently absent.`normalized_ebit` = median EBIT margin of the 5 fiscal years ending before the shock × TTM revenue. `depression_ratio` = TTM EBIT ÷ `normalized_ebit`, so a loss-making trough gives a negative ratio and lands in band 2 without a special case. Margin-and-revenue is used rather than per-share earnings because it avoids share-count, tax-rate and buyback noise, and both inputs come from the same statement. Requires ≥ 3 pre-shock fiscal years; fewer is `unscored`. When factor 17 scores 0, the normalized margin is haircut to the post-shock structural level before the ratio is taken.MCP data types: FundamentalsSnapshot + trajectoryHistorical / observedcompany40: `depression_ratio` ≥ 0.9 · 1: 0.5 ≤ ratio < 0.9 · 2: ratio < 0.5
17Shock decomposition — reversible versus structuralthe framework's central judgment, expressed as arithmetic.`build_shock_decomposition_bridge.py` decomposes the peak-to-trough EBIT decline by identity wherever the statements allow it, since segment EBIT is not a reported quantity and a driver table splitting a decline across named causes is therefore a construction however carefully it is cited. EBIT is revenue × gross margin − operating expenses, so given `revenue`, `gross_profit` and `operating_expenses` on the peak and trough rows the fall is exactly three legs: the revenue no longer sold valued at the old margin, the margin no longer taken on what is still sold, and the change in the cost of running the place. They reconcile because they cannot do otherwise, `attribution_basis` becomes `identity`, and any supplied driver table is not read — two decompositions of one decline is the disagreement this removes. What is left is classification, each leg settled by a reading the run already has: lost revenue splits on `recovery_base`'s surviving share and inherits factor 18's 60% coverage floor; the margin leg splits on whether the realized price held, using factor 10's disclosed or implied price change, or a sourced disclosure that the fall was not price, because a gross margin falls the same way whether a company discounted or absorbed the cost of capacity its volumes left behind and only the second returns with volume; the cost leg is credited only for the reduction already delivered between the issuer's own worst period and the trough. Anything unsettled counts structural, so silence never earns reversibility. A leg that offsets the decline — revenue standing higher at the trough than at the peak — is recorded as `offsetting`, out of both shares and inside the sum. Where the legs do not close, the gap is named against the endpoint whose gross profit and operating expenses do not produce the EBIT printed beside them, rather than plugged. Where the statement lines are absent the older route still runs: the LLM attributes the decline to buckets — volume/cycle, price/mix, input cost, one-off/regulatory, share loss, structural mix shift — classifying each reversible or structural with a citation per bucket, unattributed residual counts structural, `attribution_basis` is `asserted` and the factor caps at 1. The denominator is derived, not asserted: given `ebit_series` the peak is the highest TTM EBIT published on or before the reference date and the trough the lowest after it; a supplied endpoint that disagrees with the series raises, and a run with no series is marked `decline_basis: asserted` and capped at 1. On the driver route each driver may split its impact. Cost exit lags revenue loss, so a decline measured at the trough is measured at the one point where the damage is fully booked and the mitigation is not — the charges are taken, the stranded cost base is still carried, the wind-down is still losing money. `transition_impact` carves that out of the row and is reversible whatever the steady-state remainder was called, but only against a sourced `exit_program` with a disclosed date, and only until that programme has run longer than 8 reporting periods, past which a transition is the run rate. The rows travel to the scorecard as `shock_drivers` so the band can be taken apart and each driver disputed on its own. The structural mix shift bucket is where a permanently reset unit economic lands — a contractual, regulatory or structural change to cost or price that cannot revert — so the separate "is the margin ceiling still there" factor of the first draft had no independent job and was removed into this weight. Factor 16 already reads the same verdict from the other side: when this factor scores 0, the normalized margin is haircut to the post-shock structural level before the depression ratio is taken.MCP data types: Segment / KPIFiling-textTranscriptSnapshotHistorical / observedpeer50: `reversible_share` < 40% · 1: 40–70% · 2: ≥ 70% and the largest single driver is exogenous (policy, cycle, one-off) with a documented reversal mechanism · capped at 1 when either side of the ratio was asserted rather than derived — a decline not reconciled against a published EBIT series, or a decline apportioned to named drivers rather than decomposed by the income-statement identity
18What must you believedoes the thesis require repairing the franchise?Buffett's turnarounds-seldom-turn test, made checkable, and read off the company rather than off other factors' verdicts. `build_recovery_base.py` splits the pre-shock revenue base by what became of each line: `surviving` (still operating lawfully), `abolished` (structurally removed, nobody has it), `displaced` (a named competitor holds it now), `unknown`. The denominator is the pre-shock base and never today's revenue, because measured against today's revenue every completed shock reads as fully surviving — the line that died stopped contributing before the measurement was taken. Three rules push the same way: a line called surviving without a source becomes `unknown`, a line called displaced with no named competitor becomes `unknown`, and an under-attributed base books the shortfall `unknown` rather than surviving. `unknown` leaves coverage rather than any band, and below 60% coverage the factor is unscored instead of banded off a base nobody could split. The three shares are mutually exclusive claims about one base, so the thesis label — `competitive_recapture`, `rebuild`, `redeployment`, `price_sentiment_only` — falls out of them rather than being chosen. This factor takes no cap from factors 10 or 11: a rival holding the business is its own `displaced_share` evidence, and a margin is not a statement about whether a line still exists.MCP data types: Segment / KPIFiling-textTranscriptThird-party textSnapshotHistorical + expectationcompany30: `displaced_share` ≥ 20% — a named competitor holds it and it must be won back — or `surviving_share` < 40%, so most of the base must be recreated · 1: `surviving_share` 40–70% (redeployment: a material slice was abolished and must be replaced) · 2: `surviving_share` ≥ 70% with nothing displaced · capped at 1 when the split is reconstructed rather than disclosed · `unscored` when coverage < 60%
19Margin/KPI inflection with named self-help driversis the turn mechanical or hopeful?`gap_closed` = (current margin − trough margin) / (normalized margin − trough margin). "Margin should improve" with no named mechanism is a 0 by construction. Two classes of evidence are read besides the promise, both harder to assert than it rather than looser. Delivery: a driver may carry a sourced baseline, target and observed value, dated at or before the as-of date; `delivery_progress` is how far the observation has travelled from baseline toward target, and ≥ 30% is a programme visibly executing rather than an announcement. A driver delivering reaches band 1 on its own, because delivery is stronger evidence than a margin quarter. The expansion run is the longest available on three bases, reported first: year-on-year margin; the seasonal step, where a quarter's sequential change beats the median of the same fiscal step in ≥ 2 prior years — visible one quarter after a trough rather than four, when the year-ago base is still pre-shock by construction; and margin excluding disclosed exit charges, which strips the wind-down cost of the business being exited exactly as factor 17's `transition_impact` does. A run resting only on that adjusted basis caps the factor at 1. A trailing-twelve-month series is deliberately not one of the bases: its sequential change is `Q_t − Q_{t-4}`, which is the year-on-year signal under another name.MCP data types: FundamentalsFiling-textTranscriptTrajectoryHistorical + expectationcompany30: nothing delivered and no quantified driver with a quarter of expansion · 1: ≥ 1 driver measurably delivering against its own stated target (`delivery_progress` ≥ 30%), or ≥ 1 quantified driver and 1 quarter of expansion · 2: ≥ 2 quantified drivers each with a disclosed KPI, ≥ 1 of them delivering, ≥ 2 consecutive quarters of expansion, `gap_closed` ≥ 30% · capped at 1 when the only run is on margin excluding disclosed exit charges
20Identifiable catalyst and timelineis there a dated mechanism?Distinct from factor 19: this is exogenous, 19 is management-controlled — and that split is now enforced rather than described. Every row declares a `catalyst_class`; `policy`, `regulatory_decision`, `contract`, `capacity`, `index_inclusion`, `litigation` and `counterparty_action` count, while `management_selfhelp` keeps its row as evidence and counts nothing, since scoring it here scored the same fact at factor 19 as well. An unclassified row cannot be shown to be outside the company's control and does not count. A resolved catalyst is not an absent one: an event dated before the as-of date used to be invisible, so a policy deadline that had already passed scored as though no catalyst existed — yet an event that has fired, with a sourced `outcome`, is more settled than one twelve months out, because the contingency is gone rather than pending. Within 12 months that reaches band 1; band 2 stays reserved for a forward dated mechanism, which is what re-rates the name next. An elapsed date with no recorded outcome is not a resolution, it is a date nobody followed up on. Boundary with factor 12: that factor scores whether the policy damage is permanent, this one scores whether the contingency has been removed.MCP data types: TranscriptThird-party textSnapshotHistorical + expectationcompany30: no exogenous catalyst, or "cheap, eventually" — a ledger holding only self-help rows is a 0 here · 1: exogenous catalyst named but undated or beyond 24 months, or one already resolved inside the trailing 12 months with a recorded outcome · 2: ≥ 1 exogenous catalyst with a specific dated milestone 6–18 months out and a named mechanism connecting it to earnings or to the multiple
#164%

Earnings depression versus a credible normal

how much earning power is currently absent.

Type
Snapshot + trajectory
Data
Historical / observed
Peer
company

Rubric: 0: `depression_ratio` ≥ 0.9 · 1: 0.5 ≤ ratio < 0.9 · 2: ratio < 0.5

`normalized_ebit` = median EBIT margin of the 5 fiscal years ending before the shock × TTM revenue. `depression_ratio` = TTM EBIT ÷ `normalized_ebit`, so a loss-making trough gives a negative ratio and lands in band 2 without a special case. Margin-and-revenue is used rather than per-share earnings because it avoids share-count, tax-rate and buyback noise, and both inputs come from the same statement. Requires ≥ 3 pre-shock fiscal years; fewer is `unscored`. When factor 17 scores 0, the normalized margin is haircut to the post-shock structural level before the ratio is taken.

MCP data types: Fundamentals

#175%

Shock decomposition — reversible versus structural

the framework's central judgment, expressed as arithmetic.

Type
Snapshot
Data
Historical / observed
Peer
peer

Rubric: 0: `reversible_share` < 40% · 1: 40–70% · 2: ≥ 70% and the largest single driver is exogenous (policy, cycle, one-off) with a documented reversal mechanism · capped at 1 when either side of the ratio was asserted rather than derived — a decline not reconciled against a published EBIT series, or a decline apportioned to named drivers rather than decomposed by the income-statement identity

`build_shock_decomposition_bridge.py` decomposes the peak-to-trough EBIT decline by identity wherever the statements allow it, since segment EBIT is not a reported quantity and a driver table splitting a decline across named causes is therefore a construction however carefully it is cited. EBIT is revenue × gross margin − operating expenses, so given `revenue`, `gross_profit` and `operating_expenses` on the peak and trough rows the fall is exactly three legs: the revenue no longer sold valued at the old margin, the margin no longer taken on what is still sold, and the change in the cost of running the place. They reconcile because they cannot do otherwise, `attribution_basis` becomes `identity`, and any supplied driver table is not read — two decompositions of one decline is the disagreement this removes. What is left is classification, each leg settled by a reading the run already has: lost revenue splits on `recovery_base`'s surviving share and inherits factor 18's 60% coverage floor; the margin leg splits on whether the realized price held, using factor 10's disclosed or implied price change, or a sourced disclosure that the fall was not price, because a gross margin falls the same way whether a company discounted or absorbed the cost of capacity its volumes left behind and only the second returns with volume; the cost leg is credited only for the reduction already delivered between the issuer's own worst period and the trough. Anything unsettled counts structural, so silence never earns reversibility. A leg that offsets the decline — revenue standing higher at the trough than at the peak — is recorded as `offsetting`, out of both shares and inside the sum. Where the legs do not close, the gap is named against the endpoint whose gross profit and operating expenses do not produce the EBIT printed beside them, rather than plugged. Where the statement lines are absent the older route still runs: the LLM attributes the decline to buckets — volume/cycle, price/mix, input cost, one-off/regulatory, share loss, structural mix shift — classifying each reversible or structural with a citation per bucket, unattributed residual counts structural, `attribution_basis` is `asserted` and the factor caps at 1. The denominator is derived, not asserted: given `ebit_series` the peak is the highest TTM EBIT published on or before the reference date and the trough the lowest after it; a supplied endpoint that disagrees with the series raises, and a run with no series is marked `decline_basis: asserted` and capped at 1. On the driver route each driver may split its impact. Cost exit lags revenue loss, so a decline measured at the trough is measured at the one point where the damage is fully booked and the mitigation is not — the charges are taken, the stranded cost base is still carried, the wind-down is still losing money. `transition_impact` carves that out of the row and is reversible whatever the steady-state remainder was called, but only against a sourced `exit_program` with a disclosed date, and only until that programme has run longer than 8 reporting periods, past which a transition is the run rate. The rows travel to the scorecard as `shock_drivers` so the band can be taken apart and each driver disputed on its own. The structural mix shift bucket is where a permanently reset unit economic lands — a contractual, regulatory or structural change to cost or price that cannot revert — so the separate "is the margin ceiling still there" factor of the first draft had no independent job and was removed into this weight. Factor 16 already reads the same verdict from the other side: when this factor scores 0, the normalized margin is haircut to the post-shock structural level before the depression ratio is taken.

MCP data types: Segment / KPIFiling-textTranscript

#183%

What must you believe

does the thesis require repairing the franchise?

Type
Snapshot
Data
Historical + expectation
Peer
company

Rubric: 0: `displaced_share` ≥ 20% — a named competitor holds it and it must be won back — or `surviving_share` < 40%, so most of the base must be recreated · 1: `surviving_share` 40–70% (redeployment: a material slice was abolished and must be replaced) · 2: `surviving_share` ≥ 70% with nothing displaced · capped at 1 when the split is reconstructed rather than disclosed · `unscored` when coverage < 60%

Buffett's turnarounds-seldom-turn test, made checkable, and read off the company rather than off other factors' verdicts. `build_recovery_base.py` splits the pre-shock revenue base by what became of each line: `surviving` (still operating lawfully), `abolished` (structurally removed, nobody has it), `displaced` (a named competitor holds it now), `unknown`. The denominator is the pre-shock base and never today's revenue, because measured against today's revenue every completed shock reads as fully surviving — the line that died stopped contributing before the measurement was taken. Three rules push the same way: a line called surviving without a source becomes `unknown`, a line called displaced with no named competitor becomes `unknown`, and an under-attributed base books the shortfall `unknown` rather than surviving. `unknown` leaves coverage rather than any band, and below 60% coverage the factor is unscored instead of banded off a base nobody could split. The three shares are mutually exclusive claims about one base, so the thesis label — `competitive_recapture`, `rebuild`, `redeployment`, `price_sentiment_only` — falls out of them rather than being chosen. This factor takes no cap from factors 10 or 11: a rival holding the business is its own `displaced_share` evidence, and a margin is not a statement about whether a line still exists.

MCP data types: Segment / KPIFiling-textTranscriptThird-party text

#193%

Margin/KPI inflection with named self-help drivers

is the turn mechanical or hopeful?

Type
Trajectory
Data
Historical + expectation
Peer
company

Rubric: 0: nothing delivered and no quantified driver with a quarter of expansion · 1: ≥ 1 driver measurably delivering against its own stated target (`delivery_progress` ≥ 30%), or ≥ 1 quantified driver and 1 quarter of expansion · 2: ≥ 2 quantified drivers each with a disclosed KPI, ≥ 1 of them delivering, ≥ 2 consecutive quarters of expansion, `gap_closed` ≥ 30% · capped at 1 when the only run is on margin excluding disclosed exit charges

`gap_closed` = (current margin − trough margin) / (normalized margin − trough margin). "Margin should improve" with no named mechanism is a 0 by construction. Two classes of evidence are read besides the promise, both harder to assert than it rather than looser. Delivery: a driver may carry a sourced baseline, target and observed value, dated at or before the as-of date; `delivery_progress` is how far the observation has travelled from baseline toward target, and ≥ 30% is a programme visibly executing rather than an announcement. A driver delivering reaches band 1 on its own, because delivery is stronger evidence than a margin quarter. The expansion run is the longest available on three bases, reported first: year-on-year margin; the seasonal step, where a quarter's sequential change beats the median of the same fiscal step in ≥ 2 prior years — visible one quarter after a trough rather than four, when the year-ago base is still pre-shock by construction; and margin excluding disclosed exit charges, which strips the wind-down cost of the business being exited exactly as factor 17's `transition_impact` does. A run resting only on that adjusted basis caps the factor at 1. A trailing-twelve-month series is deliberately not one of the bases: its sequential change is `Q_t − Q_{t-4}`, which is the year-on-year signal under another name.

MCP data types: FundamentalsFiling-textTranscript

#203%

Identifiable catalyst and timeline

is there a dated mechanism?

Type
Snapshot
Data
Historical + expectation
Peer
company

Rubric: 0: no exogenous catalyst, or "cheap, eventually" — a ledger holding only self-help rows is a 0 here · 1: exogenous catalyst named but undated or beyond 24 months, or one already resolved inside the trailing 12 months with a recorded outcome · 2: ≥ 1 exogenous catalyst with a specific dated milestone 6–18 months out and a named mechanism connecting it to earnings or to the multiple

Distinct from factor 19: this is exogenous, 19 is management-controlled — and that split is now enforced rather than described. Every row declares a `catalyst_class`; `policy`, `regulatory_decision`, `contract`, `capacity`, `index_inclusion`, `litigation` and `counterparty_action` count, while `management_selfhelp` keeps its row as evidence and counts nothing, since scoring it here scored the same fact at factor 19 as well. An unclassified row cannot be shown to be outside the company's control and does not count. A resolved catalyst is not an absent one: an event dated before the as-of date used to be invisible, so a policy deadline that had already passed scored as though no catalyst existed — yet an event that has fired, with a sourced `outcome`, is more settled than one twelve months out, because the contingency is gone rather than pending. Within 12 months that reaches band 1; band 2 stays reserved for a forward dated mechanism, which is what re-rates the name next. An elapsed date with no recorded outcome is not a resolution, it is a date nobody followed up on. Boundary with factor 12: that factor scores whether the policy damage is permanent, this one scores whether the contingency has been removed.

MCP data types: TranscriptThird-party text

13% of framework

Survival & Downside Floor

Balance sheet, forensic integrity, asset/cash floor, distribution, tradability.

#FactorTypeDataPeerWtRubric (0/1/2)
21Balance-sheet survival and refinancing wallcan it live to see the recovery?MRQ balance sheet plus the disclosed maturity schedule. Score 0 fires hard gate G2 (AVOID at any price). The interest-cover clause only binds debt that has to be serviced. It asks whether earnings can cover a coupon, but a company holding more cash than borrowings — `maturity_basis: not_binding`, or simply net cash — can retire the debt instead, so earnings have no service requirement to meet. Left armed, the clause fired G2 on any name in this population with a loss year and a coupon, since `interest_cover` is `ebit / interest_expense` and trough EBIT is negative by construction for the companies this framework screens. A burn wall sits beside the debt wall. Every other test here is about debt, so a company with little of it could not fail whatever its cash was doing, and the factor could not separate four years of funded burn from three quarters. `runway_months` = (net cash + committed undrawn facilities) ÷ (|`trough_year_fcf`| ÷ 12) — net cash rather than the gross pool, so a dollar is not counted for both the wall and the burn, and the trough year's burn rather than the latest, because a survival test measured at any but the worst observed year measures the wrong year. `trough_year_fcf` is spendable free cash flow, not the reported print: reported FCF minus the change in the same restricted, escrow and client balances net cash already excludes from the stock. The stock and the flow have to describe the same cash, or a name whose excluded balances swung would fail the burn wall on money it cannot spend — the only test in this factor a company with no debt can fail. `compute_trough_fcf.py` applies that identity and picks the year; the agent copies the lines and does not decide whether they apply. A series that never carries those lines is unchanged. A pack that stripped them from net cash but left reported FCF as the burn is mixed-basis and the trough is refused, which cannot fire the wall. The 24-month wall is the same horizon the debt schedule already uses; the top band asks for twice it, so the cash outlasts the recovery even if it takes twice as long as the thesis underwrites. A burn nobody measured leaves `runway_months` null, which cannot fire the wall and cannot reach the top band either. The schedule is resolved by a three-step ladder, stopping at the first step that answers: (a) not-binding shortcut — when cash plus short-term investments exceeds total borrowings the wall cannot bind arithmetically, so no schedule is fetched and the factor scores on the balance sheet alone with `maturity_schedule: not_binding`; (b) XBRL — SEC `companyconcept` for `LongTermDebtMaturitiesRepaymentsOfPrincipalInNextTwelveMonths` and `…InYearTwo`, summed to give debt due within 24 months. Every fact carries its own `filed` date, so point-in-time selection is a filter, not a reconstruction, and history runs deep (16–46 facts on the leveraged names tested). Covers US-GAAP filers only; foreign private issuers reporting under IFRS return 404; (c) filing text — `firecrawl` on the primary filing document, reading the 20-F Item 5.F Tabular Disclosure of Contractual Obligations table or the 10-K long-term-debt maturity footnote. Both are bucketed `< 1 year / 1–3 / 3–5 / > 5`, which is what the 24-month test needs. `sec-edgar` cannot read either. Only when all three fail is the factor `unscored`, which blocks G2 evaluation and caps the tier at SATELLITE.MCP data types: FundamentalsFiling-textSnapshotHistorical + expectationcompany40: net debt/EBITDA > 4.0× or (cash + undrawn facilities + 24m expected FCF) < debt maturing within 24 months or interest cover < 1.5× on debt that cannot be retired from cash or `runway_months` < 24 · 1: net debt/EBITDA 1.0–4.0×, maturity coverage 1.0–1.5×, or net cash whose funded burn falls short of 48 months · 2: net cash, no maturity requiring refinancing within 24 months, and either FCF ≥ 0 in the trough fiscal year or `runway_months` ≥ 48
22Forensic and accounting integrityis the reported picture real?Six fixed binary tests, of which four count: Beneish M > −1.78; receivables growth > revenue growth + 15pp for ≥ 2 of the last 5 year-on-year pairs; implied cash yield < 40% of the local risk-free rate (fake-cash test); auditor change or resignation within 24m without a disclosed benign reason. Altman Z and Piotroski F are reported and not counted. This factor asks whether the reported picture is real; Altman predicts bankruptcy and Piotroski grades operating quality, and neither is evidence about honesty. Both are already scored — solvency at factor 21 behind its own hard gate, quality at factors 29 and 30 — so counting them here charged one fact twice and, worse, charged it to the wrong question: Altman's EBIT/assets term collapses at the trough, which is the defining feature of every name this framework screens, so it fired on distress and read out as fraud. They keep their ledger rows with `counts: false`, the treatment the accruals test already receives for duplicating factor 30. The receivables test is windowed. Counted over a full listed history two divergent years accumulate in almost any company that ever grew, so the flag is taken over the trailing 5 pairs; the lifetime count stays visible. The clean band requires a search. `flag_count` counts only resolved tests, so zero flags over two resolved tests used to score the same as zero over five — an unexamined company graded as a clean one. Band 2 now needs ≥ 3 of the 4 counting tests resolved, alongside the clean opinion and the absence of going-concern language, both of which are read rather than computed. `fake_cash_flag` must name a triggered probe, since it caps factors 26 and 27 on its own; a flag asserted with nothing in `probes_triggered` is unresolved, not a finding. Score 0 fires hard gate G3.MCP data types: FundamentalsFiling-textThird-party textSnapshot + trajectoryHistorical / observedcompany40: ≥ 3 flags, or any independently verified fraud finding, or a regulator enforcement action on accounting · 1: 1–2 flags, each with a plausible but unverified company explanation, or 0 flags over fewer than 3 resolved tests · 2: 0 flags over ≥ 3 resolved counting tests, clean audit opinion, no going-concern language
23Asset floor versus pricehow much of the price is already backed by tangible assets.`p_tbv` = market cap ÷ tangible book value, both reported fields. This replaces the bespoke "conservative asset value" formula of the first draft, which required five hand-set haircuts, subtracted deferred revenue against cash the company had already collected (penalising every prepaid business model twice), and disagreed with its own second leg. Tangible book already excludes goodwill and intangibles, which is the conservatism the formula was reaching for. Score cap: capped at 1 when `inputs.asset_quality = impairing` — a low P/B on assets being written down is not a floor.MCP data types: FundamentalsSnapshotHistorical / observedcompany30: `p_tbv` > 2.0× · 1: 1.0–2.0× · 2: ≤ 1.0×
24Distribution sustainabilityis the yield real through the trough?`na` when the company pays no distribution. Guards the dividend-yield element of factor 2 against the yield trap.MCP data types: FundamentalsSnapshotHistorical + expectationcompany10: payout > 100% of TTM FCF, or a cut is announced or implied (payout > 100% of trough-year FCF with net debt rising) · 1: payout 60–100% of TTM FCF · 2: payout ≤ 60% of TTM FCF and covered by trough-year FCF
25Liquidity and tradabilityis there a real exit?Score 0 fires hard gate G1 → STRANDED, excluded from scoring and from every calibration set. A frozen last trade is not a price.MCP data types: PricesSnapshotHistorical / observedcompany10: suspended, frozen, delisting-noticed, or 60-day median ADV < US$1m · 1: ADV US$1–10m, or exiting a nominal position at 20% of ADV takes > 10 trading days · 2: ADV ≥ US$10m with continuous trading and no halts in 12 months
#214%

Balance-sheet survival and refinancing wall

can it live to see the recovery?

Type
Snapshot
Data
Historical + expectation
Peer
company

Rubric: 0: net debt/EBITDA > 4.0× or (cash + undrawn facilities + 24m expected FCF) < debt maturing within 24 months or interest cover < 1.5× on debt that cannot be retired from cash or `runway_months` < 24 · 1: net debt/EBITDA 1.0–4.0×, maturity coverage 1.0–1.5×, or net cash whose funded burn falls short of 48 months · 2: net cash, no maturity requiring refinancing within 24 months, and either FCF ≥ 0 in the trough fiscal year or `runway_months` ≥ 48

MRQ balance sheet plus the disclosed maturity schedule. Score 0 fires hard gate G2 (AVOID at any price). The interest-cover clause only binds debt that has to be serviced. It asks whether earnings can cover a coupon, but a company holding more cash than borrowings — `maturity_basis: not_binding`, or simply net cash — can retire the debt instead, so earnings have no service requirement to meet. Left armed, the clause fired G2 on any name in this population with a loss year and a coupon, since `interest_cover` is `ebit / interest_expense` and trough EBIT is negative by construction for the companies this framework screens. A burn wall sits beside the debt wall. Every other test here is about debt, so a company with little of it could not fail whatever its cash was doing, and the factor could not separate four years of funded burn from three quarters. `runway_months` = (net cash + committed undrawn facilities) ÷ (|`trough_year_fcf`| ÷ 12) — net cash rather than the gross pool, so a dollar is not counted for both the wall and the burn, and the trough year's burn rather than the latest, because a survival test measured at any but the worst observed year measures the wrong year. `trough_year_fcf` is spendable free cash flow, not the reported print: reported FCF minus the change in the same restricted, escrow and client balances net cash already excludes from the stock. The stock and the flow have to describe the same cash, or a name whose excluded balances swung would fail the burn wall on money it cannot spend — the only test in this factor a company with no debt can fail. `compute_trough_fcf.py` applies that identity and picks the year; the agent copies the lines and does not decide whether they apply. A series that never carries those lines is unchanged. A pack that stripped them from net cash but left reported FCF as the burn is mixed-basis and the trough is refused, which cannot fire the wall. The 24-month wall is the same horizon the debt schedule already uses; the top band asks for twice it, so the cash outlasts the recovery even if it takes twice as long as the thesis underwrites. A burn nobody measured leaves `runway_months` null, which cannot fire the wall and cannot reach the top band either. The schedule is resolved by a three-step ladder, stopping at the first step that answers: (a) not-binding shortcut — when cash plus short-term investments exceeds total borrowings the wall cannot bind arithmetically, so no schedule is fetched and the factor scores on the balance sheet alone with `maturity_schedule: not_binding`; (b) XBRL — SEC `companyconcept` for `LongTermDebtMaturitiesRepaymentsOfPrincipalInNextTwelveMonths` and `…InYearTwo`, summed to give debt due within 24 months. Every fact carries its own `filed` date, so point-in-time selection is a filter, not a reconstruction, and history runs deep (16–46 facts on the leveraged names tested). Covers US-GAAP filers only; foreign private issuers reporting under IFRS return 404; (c) filing text — `firecrawl` on the primary filing document, reading the 20-F Item 5.F Tabular Disclosure of Contractual Obligations table or the 10-K long-term-debt maturity footnote. Both are bucketed `< 1 year / 1–3 / 3–5 / > 5`, which is what the 24-month test needs. `sec-edgar` cannot read either. Only when all three fail is the factor `unscored`, which blocks G2 evaluation and caps the tier at SATELLITE.

MCP data types: FundamentalsFiling-text

#224%

Forensic and accounting integrity

is the reported picture real?

Type
Snapshot + trajectory
Data
Historical / observed
Peer
company

Rubric: 0: ≥ 3 flags, or any independently verified fraud finding, or a regulator enforcement action on accounting · 1: 1–2 flags, each with a plausible but unverified company explanation, or 0 flags over fewer than 3 resolved tests · 2: 0 flags over ≥ 3 resolved counting tests, clean audit opinion, no going-concern language

Six fixed binary tests, of which four count: Beneish M > −1.78; receivables growth > revenue growth + 15pp for ≥ 2 of the last 5 year-on-year pairs; implied cash yield < 40% of the local risk-free rate (fake-cash test); auditor change or resignation within 24m without a disclosed benign reason. Altman Z and Piotroski F are reported and not counted. This factor asks whether the reported picture is real; Altman predicts bankruptcy and Piotroski grades operating quality, and neither is evidence about honesty. Both are already scored — solvency at factor 21 behind its own hard gate, quality at factors 29 and 30 — so counting them here charged one fact twice and, worse, charged it to the wrong question: Altman's EBIT/assets term collapses at the trough, which is the defining feature of every name this framework screens, so it fired on distress and read out as fraud. They keep their ledger rows with `counts: false`, the treatment the accruals test already receives for duplicating factor 30. The receivables test is windowed. Counted over a full listed history two divergent years accumulate in almost any company that ever grew, so the flag is taken over the trailing 5 pairs; the lifetime count stays visible. The clean band requires a search. `flag_count` counts only resolved tests, so zero flags over two resolved tests used to score the same as zero over five — an unexamined company graded as a clean one. Band 2 now needs ≥ 3 of the 4 counting tests resolved, alongside the clean opinion and the absence of going-concern language, both of which are read rather than computed. `fake_cash_flag` must name a triggered probe, since it caps factors 26 and 27 on its own; a flag asserted with nothing in `probes_triggered` is unresolved, not a finding. Score 0 fires hard gate G3.

MCP data types: FundamentalsFiling-textThird-party text

#233%

Asset floor versus price

how much of the price is already backed by tangible assets.

Type
Snapshot
Data
Historical / observed
Peer
company

Rubric: 0: `p_tbv` > 2.0× · 1: 1.0–2.0× · 2: ≤ 1.0×

`p_tbv` = market cap ÷ tangible book value, both reported fields. This replaces the bespoke "conservative asset value" formula of the first draft, which required five hand-set haircuts, subtracted deferred revenue against cash the company had already collected (penalising every prepaid business model twice), and disagreed with its own second leg. Tangible book already excludes goodwill and intangibles, which is the conservatism the formula was reaching for. Score cap: capped at 1 when `inputs.asset_quality = impairing` — a low P/B on assets being written down is not a floor.

MCP data types: Fundamentals

#241%

Distribution sustainability

is the yield real through the trough?

Type
Snapshot
Data
Historical + expectation
Peer
company

Rubric: 0: payout > 100% of TTM FCF, or a cut is announced or implied (payout > 100% of trough-year FCF with net debt rising) · 1: payout 60–100% of TTM FCF · 2: payout ≤ 60% of TTM FCF and covered by trough-year FCF

`na` when the company pays no distribution. Guards the dividend-yield element of factor 2 against the yield trap.

MCP data types: Fundamentals

#251%

Liquidity and tradability

is there a real exit?

Type
Snapshot
Data
Historical / observed
Peer
company

Rubric: 0: suspended, frozen, delisting-noticed, or 60-day median ADV < US$1m · 1: ADV US$1–10m, or exiting a nominal position at 20% of ADV takes > 10 trading days · 2: ADV ≥ US$10m with continuous trading and no halts in 12 months

Score 0 fires hard gate G1 → STRANDED, excluded from scoring and from every calibration set. A frozen last trade is not a price.

MCP data types: Prices

12% of framework

Valuation & Yield

Cheapness on normalized earning power, the net-cash cushion, executed yield, return on capital against its cost, owner earnings.

#FactorTypeDataPeerWtRubric (0/1/2)
26Cheapness on normalized earning powerthe house cheapness measure.`ev_norm` = enterprise value ÷ `normalized_ebit` from factor 16, where EV = market cap − net cash. This replaces excl-cash P/E, which was `n.m.` whenever TTM earnings were negative and had no valid band at all for a loss-making company trading below net cash — the modal fallen angel. EV handles the cash cushion arithmetically instead of through a multiplier, goes negative when the market pays less than the cash, and normalized EBIT keeps the denominator positive through the trough. The negative-EV branch is a shortcut past the multiple, not past the question. Cash below the price says what you are paid to take the business; it says nothing about whether there is a business, and this factor's name promises earning power in the answer. So band 2 on a negative EV is capped at 1 unless a positive `normalized_ebit` was struck — without one the cash case is a liquidation case, and factors 23 and 27 already carry the cash. A non-positive normalized EBIT also leaves `ev_norm` null at the producer rather than yielding a negative multiple, which would otherwise clear the `< 6×` band as maximum cheapness; the scorer refuses a negative `ev_norm` for the same reason. Score cap: capped at 1 when factor 16 = 0 — a low multiple on peak-cycle earnings is the classic trap. Net cash is an input here, not a factor of its own: it already drives this EV, factor 21's maturity shortcut, and part of factor 23's tangible book, so scoring `net cash % of market cap` separately was counting one balance-sheet fact four times. It stays on the Reconciled-Metrics row as a reported number.MCP data types: FundamentalsPricesSnapshotHistorical / observedcompany40: `ev_norm` > 15× · 1: 6–15× · 2: < 6×, or enterprise value is negative
27Net cash % of market caphow much of the price you get back in cash.`net_cash` ÷ market cap, where `net_cash` = cash + short-term investments + cash-like non-current deposits − all borrowings (short- and long-term debt including project and construction debt, excluding capitalised lease obligations, which are reported separately as `lease_obligations` with a `net_cash_incl_leases` variant printed alongside). Restricted, escrow, and broker client cash excluded. When the two variants fall in different bands, the lower band is taken and the factor is flagged `basis_sensitive`. Score cap: capped at 1 when the fake-cash test in factor 22 flags — a cushion that may not exist is not a cushion. Deliberately overlapping but not redundant: factor 26 nets cash inside an earnings multiple, and factor 21 asks only whether cash covers debt at all. This asks how much of the purchase price is returned in cash, which is the form every analyst quotes and the only one of the three that distinguishes a company trading at 40% of its cash from one trading at 90% of it. Factor 23's tangible book lost one weight point when this was restored, since tangible book contains the same cash.MCP data types: FundamentalsSnapshotHistorical / observedcompany20: < 15%, or net debt · 1: 15–50% · 2: > 50%
28Executed total yielddelivered, not announced.`buyback_yield + dividend_yield` for the last reported fiscal year, both taken from the reported ratio set rather than summed off cash-flow lines. Buyback yield is signed: it goes negative on net share issuance, so dilution lands in band 0 arithmetically instead of needing a separate clause. The cash-flow repurchase line is null in years with no buyback, which reads as missing data and is why it is not the input. Authorised-but-unexecuted programs earn nothing by construction. Factor 33 scores the timing of the buyback; this scores the realised yield.MCP data types: FundamentalsTrajectoryHistorical / observedcompany20: < 2% · 1: 2–5% · 2: > 5%
29Return on capital versus its costdoes the business earn a real return on the capital tied up in it?`nopat_norm` = `normalized_ebit` from factor 16 × (1 − median pre-shock effective tax rate), so the return is measured on normal earning power rather than on a trough. `ic_core` = debt + equity − total liquid assets; `roic_core` = `nopat_norm` ÷ `ic_core`. `roic_total` runs over debt + equity − cash & equivalents, and its numerator is `nopat_norm` plus the after-tax investment income of the non-cash pool that stays inside that base — crediting the whole investment line would pay the ratio for cash it excluded, and crediting none of it charges the operating business rent on assets it is not paid for. Where that income was never supplied and the portfolio is 10% or more of the base, `roic_total_comparable` is false and the cap does not fire. The gap between the two bases is still reported: a wide one says the headline return depends on calling the portfolio non-operating. `wacc` is computed point-in-time, and its equity risk premium must arrive from `resolve_equity_risk_premium.py` with a source and a publication date, or CAPM is abandoned for the fixed bands. Where that premium is a reconstruction rather than a vintage published in time it carries an interval (`wacc_low`/`wacc_high`), and the CAPM band is kept only where the band and the cap are the same at both ends of it. Where factor 16 is `unscored`, fall back to median `roic_total` over the pre-shock fiscal years.MCP data types: FundamentalsPricesSnapshotHistorical / observedcompany20: `roic_core` < `wacc` · 1: 1.0–1.5× `wacc` · 2: ≥ 1.5× `wacc`, or `ic_core` ≤ 0 with a positive `nopat_norm` — a business running on customer float needs no capital, which is the strongest form of this factor passing. Where `ic_core` ≤ 0 and `nopat_norm` is not positive the score is 0: needing no capital and earning nothing on it are different companies. Where no `nopat_norm` was struck at all, the float band is capped at 1. Cap: held at 1 when `roic_total` < `wacc`, so a flattering capital base cannot carry a company whose whole capital earns below its cost — but only where `roic_total` is comparable to a cost of capital, which it is not while a material securities portfolio sits in its denominator with none of its income in the numerator.
30Owner-earnings bridgeis the cheap multiple on real earnings?`oe_gap` = (net income − free cash flow) ÷ \|net income\|, 5-year median. Signed, never absolute: a negative gap means cash flow exceeds accounting earnings, which is the good case and scores 2 — taking the absolute value would penalise exactly the deferred-revenue and prepayment models that convert best. Free cash flow is used rather than a hand-built owner-earnings figure because it is a reported field and needs no maintenance-capex estimate. This factor is now the framework's only accruals test; the equivalent binary flag was removed from factor 22 rather than have the same divergence between earnings and cash score twice.MCP data types: FundamentalsTrajectoryHistorical / observedcompany20: `oe_gap` > 40% · 1: 15–40% · 2: ≤ 15%
#264%

Cheapness on normalized earning power

the house cheapness measure.

Type
Snapshot
Data
Historical / observed
Peer
company

Rubric: 0: `ev_norm` > 15× · 1: 6–15× · 2: < 6×, or enterprise value is negative

`ev_norm` = enterprise value ÷ `normalized_ebit` from factor 16, where EV = market cap − net cash. This replaces excl-cash P/E, which was `n.m.` whenever TTM earnings were negative and had no valid band at all for a loss-making company trading below net cash — the modal fallen angel. EV handles the cash cushion arithmetically instead of through a multiplier, goes negative when the market pays less than the cash, and normalized EBIT keeps the denominator positive through the trough. The negative-EV branch is a shortcut past the multiple, not past the question. Cash below the price says what you are paid to take the business; it says nothing about whether there is a business, and this factor's name promises earning power in the answer. So band 2 on a negative EV is capped at 1 unless a positive `normalized_ebit` was struck — without one the cash case is a liquidation case, and factors 23 and 27 already carry the cash. A non-positive normalized EBIT also leaves `ev_norm` null at the producer rather than yielding a negative multiple, which would otherwise clear the `< 6×` band as maximum cheapness; the scorer refuses a negative `ev_norm` for the same reason. Score cap: capped at 1 when factor 16 = 0 — a low multiple on peak-cycle earnings is the classic trap. Net cash is an input here, not a factor of its own: it already drives this EV, factor 21's maturity shortcut, and part of factor 23's tangible book, so scoring `net cash % of market cap` separately was counting one balance-sheet fact four times. It stays on the Reconciled-Metrics row as a reported number.

MCP data types: FundamentalsPrices

#272%

Net cash % of market cap

how much of the price you get back in cash.

Type
Snapshot
Data
Historical / observed
Peer
company

Rubric: 0: < 15%, or net debt · 1: 15–50% · 2: > 50%

`net_cash` ÷ market cap, where `net_cash` = cash + short-term investments + cash-like non-current deposits − all borrowings (short- and long-term debt including project and construction debt, excluding capitalised lease obligations, which are reported separately as `lease_obligations` with a `net_cash_incl_leases` variant printed alongside). Restricted, escrow, and broker client cash excluded. When the two variants fall in different bands, the lower band is taken and the factor is flagged `basis_sensitive`. Score cap: capped at 1 when the fake-cash test in factor 22 flags — a cushion that may not exist is not a cushion. Deliberately overlapping but not redundant: factor 26 nets cash inside an earnings multiple, and factor 21 asks only whether cash covers debt at all. This asks how much of the purchase price is returned in cash, which is the form every analyst quotes and the only one of the three that distinguishes a company trading at 40% of its cash from one trading at 90% of it. Factor 23's tangible book lost one weight point when this was restored, since tangible book contains the same cash.

MCP data types: Fundamentals

#282%

Executed total yield

delivered, not announced.

Type
Trajectory
Data
Historical / observed
Peer
company

Rubric: 0: < 2% · 1: 2–5% · 2: > 5%

`buyback_yield + dividend_yield` for the last reported fiscal year, both taken from the reported ratio set rather than summed off cash-flow lines. Buyback yield is signed: it goes negative on net share issuance, so dilution lands in band 0 arithmetically instead of needing a separate clause. The cash-flow repurchase line is null in years with no buyback, which reads as missing data and is why it is not the input. Authorised-but-unexecuted programs earn nothing by construction. Factor 33 scores the timing of the buyback; this scores the realised yield.

MCP data types: Fundamentals

#292%

Return on capital versus its cost

does the business earn a real return on the capital tied up in it?

Type
Snapshot
Data
Historical / observed
Peer
company

Rubric: 0: `roic_core` < `wacc` · 1: 1.0–1.5× `wacc` · 2: ≥ 1.5× `wacc`, or `ic_core` ≤ 0 with a positive `nopat_norm` — a business running on customer float needs no capital, which is the strongest form of this factor passing. Where `ic_core` ≤ 0 and `nopat_norm` is not positive the score is 0: needing no capital and earning nothing on it are different companies. Where no `nopat_norm` was struck at all, the float band is capped at 1. Cap: held at 1 when `roic_total` < `wacc`, so a flattering capital base cannot carry a company whose whole capital earns below its cost — but only where `roic_total` is comparable to a cost of capital, which it is not while a material securities portfolio sits in its denominator with none of its income in the numerator.

`nopat_norm` = `normalized_ebit` from factor 16 × (1 − median pre-shock effective tax rate), so the return is measured on normal earning power rather than on a trough. `ic_core` = debt + equity − total liquid assets; `roic_core` = `nopat_norm` ÷ `ic_core`. `roic_total` runs over debt + equity − cash & equivalents, and its numerator is `nopat_norm` plus the after-tax investment income of the non-cash pool that stays inside that base — crediting the whole investment line would pay the ratio for cash it excluded, and crediting none of it charges the operating business rent on assets it is not paid for. Where that income was never supplied and the portfolio is 10% or more of the base, `roic_total_comparable` is false and the cap does not fire. The gap between the two bases is still reported: a wide one says the headline return depends on calling the portfolio non-operating. `wacc` is computed point-in-time, and its equity risk premium must arrive from `resolve_equity_risk_premium.py` with a source and a publication date, or CAPM is abandoned for the fixed bands. Where that premium is a reconstruction rather than a vintage published in time it carries an interval (`wacc_low`/`wacc_high`), and the CAPM band is kept only where the band and the cap are the same at both ends of it. Where factor 16 is `unscored`, fall back to median `roic_total` over the pre-shock fiscal years.

MCP data types: FundamentalsPrices

#302%

Owner-earnings bridge

is the cheap multiple on real earnings?

Type
Trajectory
Data
Historical / observed
Peer
company

Rubric: 0: `oe_gap` > 40% · 1: 15–40% · 2: ≤ 15%

`oe_gap` = (net income − free cash flow) ÷ \|net income\|, 5-year median. Signed, never absolute: a negative gap means cash flow exceeds accounting earnings, which is the good case and scores 2 — taking the absolute value would penalise exactly the deferred-revenue and prepayment models that convert best. Free cash flow is used rather than a hand-built owner-earnings figure because it is a reported field and needs no maintenance-capex estimate. This factor is now the framework's only accruals test; the equivalent binary flag was removed from factor 22 rather than have the same divergence between earnings and cash score twice.

MCP data types: Fundamentals

11% of framework

Alignment, Capital Return & Positioning

Management credibility, insider buying, executed buybacks, smart money, washout.

#FactorTypeDataPeerWtRubric (0/1/2)
31Management credibilitya good business can be a bad stock.Promise-to-actual ledger built by `build_management_credibility_ledger.py`. The verdict on each row stays with the reader — coming in at the bottom of a guided range is a judgement, not arithmetic — but a row has to be able to carry one: `target_period` (the period the promise covers, not the one it was uttered in), `made_on`, an `outcome_date` after the promise and at or before the as-of date, and a citation on both halves. Rows that cannot leave into `unverified` rather than scoring on an impression. Three rules exist because a fallen angel is not a healthy company. A withdrawal counts against management only when the guided business still existed: nearly every company in a policy shock, a pandemic or a credit freeze stops guiding and the honest ones stop first, so a withdrawal forced by the same event the policy factors already score is recorded without zeroing the factor, and an unclassified one caps rather than condemns. The streak runs on guided periods from the later of the shock and a sourced reset: rows aggregate to one verdict per period, so three promises about one quarter are one quarter; the misses that caused a reset are what a reset concedes, so charging them against the record built since would put the top band out of reach for exactly the population this framework exists to score; and the reset cannot be the only anchor, since a company that never published numeric guidance has no bar to reset and would be barred from the top band by a listing-venue habit rather than by anything it did. Whether it returned capital is factor 28's job. Honesty and fraud route to factor 22. Governance and alignment route to factor 36.MCP data types: FundamentalsFiling-textTranscriptThird-party textTrajectoryHistorical + expectationcompany30: guidance was withdrawn on a business that was still there to guide; or ≥ 4 missed periods in the counted window · 1: mixed — 1–3 misses, or a withdrawal nobody classified · 2: ≥ 2 guided periods met or beaten since the shock (or since a sourced reset, where one was made) · capped at 1 when a scoring row rests on a citation nobody can reopen, or a withdrawal was never classified
32Insider open-market buying at lowsaction, not opinion.Scored only for issuers that file Form 4; `na` and weight-redistributed otherwise. Section 16 exempts foreign private issuers, so most ADRs — EDU included as of 2022 — file nothing to read, and an absent filing is not the same as an absent purchase. HKEX DI and A-share disclosures were carried in the first draft as equivalent sources; neither resolved to a usable series in testing, so claiming them would have let the factor score noise. Scoring runs off the Form 4 XML itself (`ownershipDocument`), which gives reporting-owner name, transaction date, transaction code, shares, price, and acquired/disposed per line. The `sec-edgar` MCP's `analyze_form4_transactions` returns filing stubs only — accession, URL, filing date, no transaction detail — so it locates filings but cannot score them. Count only transaction code `P` (open-market purchase) against `S`; codes `A` (grant), `M` (option exercise), and `F` (tax withholding) are excluded from the numerator, as are 10b5-1 sales. Filings are selected by filing date ≤ as-of, so the 12-month window is point-in-time.MCP data types: InsiderTrajectoryHistorical / observedcompany30: net insider selling over the last 12 months · 1: no material insider transactions · 2: net open-market purchases ≥ US$2m or ≥ 0.2% of shares outstanding, by ≥ 2 distinct insiders (or the chair/CEO alone ≥ US$5m), executed at prices in the bottom third of the 52-week range
33Buyback executed at depressed pricesbuying cheap, not buying high.`na` when no buyback was executed in the last 12 months — the absence of a buyback is already scored by factor 28, and this factor grades execution quality, not whether a programme exists. Net issuance likewise scores through 29 rather than being punished twice here. Executed dollars from cash-flow statement and monthly HKEX/SEC repurchase disclosures, against the daily VWAP series. Score cap: capped at 1 when net debt/EBITDA > 3× — a leveraged buyback at a trough is a different signal.MCP data types: FundamentalsPricesTrajectoryHistorical / observedcompany20: buyback executed above the 12-month VWAP · 1: executed at or below VWAP but < 1% of shares · 2: ≥ 2% of shares retired in the last 12 months at an average price below the 12-month VWAP
34Smart-money accumulationconfirmation, never a thesis.13F positions of the CIKs in `backend/app/skills/fa-ownership-flow/quality_investor_list.json` — a versioned, dated file keyed by CIK and never by manager name; absent the file the factor is `unscored`, because "a name I recognise" is the discretion this framework removes. `na` off US listings: 13F reaches US-listed securities and nothing else, and scoring 0 or 1 where the filing does not exist would penalise exactly the names on which the evidence is available. Five guards, each of which returned a wrong answer during the EDU test: resolve list entries by CIK only (name matching credited "Miller Value Partners" for a "Value Partners" query); union the issuer's full CUSIP history, since a CUSIP change hides the earlier holdings; admit a filing period only once its 45-day deadline has passed at the as-of date, not once the period has ended; drop the newest period when its filing count is below 80% of the trailing four-period median, or a half-filed quarter reads as a mass exit; and count a position only when it is ≥ 0.1% of shares outstanding or ≥ US$5m at period close, so a rounding stake cannot move the band. Put and call lines are excluded, and a CIK claimed by two list entries counts once.MCP data types: Holders / 13FTrajectoryHistorical / observedpeer1`na` unless the issuer is US-listed (ordinary or ADR) · 0: aggregate material holdings of the list down > 25% across the two visible filing periods · 1: no material change · 2: ≥ 2 list filers initiated or increased a material position by ≥ 25% across the two visible filing periods, or a list filer filed a 13D
35Positioning washouthas the register turned over, or is the marginal seller still there?Measure: `float_turnover_12m = Σ volume(t−12m .. t) / free_float_shares`, and `turnover_ratio = float_turnover_12m ÷ median(float_turnover_12m sampled monthly over t−4y .. t−1y)` — how many times faster the free float changed hands than this name's own normal rate. Free float cancels to first order between numerator and denominator, so a stale float count does not bias the ratio, which is what makes this computable from a price history in every market. A forced-basket sale, an index deletion or a tax-loss flush all register here as volume, so none of them has to be separately identified and argued. `unscored` when fewer than four years of price history exist.MCP data types: PricesTrajectoryHistorical / observedcompany10: `turnover_ratio` < 1.25× · 1: 1.25× – 2.0× · 2: ≥ 2.0×
36Governance and minority alignmentare the people making the promises aligned with the minority?Live governance flags from `build_management_credibility_ledger.py`, each carrying a `tier` from a closed class list. Tier-S conduct zeroes the factor on its own; Tier-A (pledged control shares, related-party dealing) zeroes it only on affirmative evidence — `material: true`, `arm_s_length: false`, `ordinary_course: false`, or a related-party amount ≥ 10% of revenue or assets. A disclosed, arm's-length, ordinary-course related-party dealing is a live flag and scores 1, not 0: it must be watched, but it is not self-dealing. The related-party magnitude test factor 22 used to carry lives here, so one related-party fact has one home. Honesty and fraud route to factor 22; whether capital was returned is factor 28's job.MCP data types: Filing-textThird-party textSnapshotHistorical / observedcompany10: a live Tier-S flag (minority expropriation, undisclosed guarantee, restatement, auditor resignation, executive investigation), or a related-party / pledged-shares flag affirmatively shown material or not arm's-length · 1: a live related-party or pledged-shares flag that is disclosed, arm's-length, ordinary-course and immaterial · 2: no live governance flags
#313%

Management credibility

a good business can be a bad stock.

Type
Trajectory
Data
Historical + expectation
Peer
company

Rubric: 0: guidance was withdrawn on a business that was still there to guide; or ≥ 4 missed periods in the counted window · 1: mixed — 1–3 misses, or a withdrawal nobody classified · 2: ≥ 2 guided periods met or beaten since the shock (or since a sourced reset, where one was made) · capped at 1 when a scoring row rests on a citation nobody can reopen, or a withdrawal was never classified

Promise-to-actual ledger built by `build_management_credibility_ledger.py`. The verdict on each row stays with the reader — coming in at the bottom of a guided range is a judgement, not arithmetic — but a row has to be able to carry one: `target_period` (the period the promise covers, not the one it was uttered in), `made_on`, an `outcome_date` after the promise and at or before the as-of date, and a citation on both halves. Rows that cannot leave into `unverified` rather than scoring on an impression. Three rules exist because a fallen angel is not a healthy company. A withdrawal counts against management only when the guided business still existed: nearly every company in a policy shock, a pandemic or a credit freeze stops guiding and the honest ones stop first, so a withdrawal forced by the same event the policy factors already score is recorded without zeroing the factor, and an unclassified one caps rather than condemns. The streak runs on guided periods from the later of the shock and a sourced reset: rows aggregate to one verdict per period, so three promises about one quarter are one quarter; the misses that caused a reset are what a reset concedes, so charging them against the record built since would put the top band out of reach for exactly the population this framework exists to score; and the reset cannot be the only anchor, since a company that never published numeric guidance has no bar to reset and would be barred from the top band by a listing-venue habit rather than by anything it did. Whether it returned capital is factor 28's job. Honesty and fraud route to factor 22. Governance and alignment route to factor 36.

MCP data types: FundamentalsFiling-textTranscriptThird-party text

#323%

Insider open-market buying at lows

action, not opinion.

Type
Trajectory
Data
Historical / observed
Peer
company

Rubric: 0: net insider selling over the last 12 months · 1: no material insider transactions · 2: net open-market purchases ≥ US$2m or ≥ 0.2% of shares outstanding, by ≥ 2 distinct insiders (or the chair/CEO alone ≥ US$5m), executed at prices in the bottom third of the 52-week range

Scored only for issuers that file Form 4; `na` and weight-redistributed otherwise. Section 16 exempts foreign private issuers, so most ADRs — EDU included as of 2022 — file nothing to read, and an absent filing is not the same as an absent purchase. HKEX DI and A-share disclosures were carried in the first draft as equivalent sources; neither resolved to a usable series in testing, so claiming them would have let the factor score noise. Scoring runs off the Form 4 XML itself (`ownershipDocument`), which gives reporting-owner name, transaction date, transaction code, shares, price, and acquired/disposed per line. The `sec-edgar` MCP's `analyze_form4_transactions` returns filing stubs only — accession, URL, filing date, no transaction detail — so it locates filings but cannot score them. Count only transaction code `P` (open-market purchase) against `S`; codes `A` (grant), `M` (option exercise), and `F` (tax withholding) are excluded from the numerator, as are 10b5-1 sales. Filings are selected by filing date ≤ as-of, so the 12-month window is point-in-time.

MCP data types: Insider

#332%

Buyback executed at depressed prices

buying cheap, not buying high.

Type
Trajectory
Data
Historical / observed
Peer
company

Rubric: 0: buyback executed above the 12-month VWAP · 1: executed at or below VWAP but < 1% of shares · 2: ≥ 2% of shares retired in the last 12 months at an average price below the 12-month VWAP

`na` when no buyback was executed in the last 12 months — the absence of a buyback is already scored by factor 28, and this factor grades execution quality, not whether a programme exists. Net issuance likewise scores through 29 rather than being punished twice here. Executed dollars from cash-flow statement and monthly HKEX/SEC repurchase disclosures, against the daily VWAP series. Score cap: capped at 1 when net debt/EBITDA > 3× — a leveraged buyback at a trough is a different signal.

MCP data types: FundamentalsPrices

#341%

Smart-money accumulation

confirmation, never a thesis.

Type
Trajectory
Data
Historical / observed
Peer
peer

Rubric: `na` unless the issuer is US-listed (ordinary or ADR) · 0: aggregate material holdings of the list down > 25% across the two visible filing periods · 1: no material change · 2: ≥ 2 list filers initiated or increased a material position by ≥ 25% across the two visible filing periods, or a list filer filed a 13D

13F positions of the CIKs in `backend/app/skills/fa-ownership-flow/quality_investor_list.json` — a versioned, dated file keyed by CIK and never by manager name; absent the file the factor is `unscored`, because "a name I recognise" is the discretion this framework removes. `na` off US listings: 13F reaches US-listed securities and nothing else, and scoring 0 or 1 where the filing does not exist would penalise exactly the names on which the evidence is available. Five guards, each of which returned a wrong answer during the EDU test: resolve list entries by CIK only (name matching credited "Miller Value Partners" for a "Value Partners" query); union the issuer's full CUSIP history, since a CUSIP change hides the earlier holdings; admit a filing period only once its 45-day deadline has passed at the as-of date, not once the period has ended; drop the newest period when its filing count is below 80% of the trailing four-period median, or a half-filed quarter reads as a mass exit; and count a position only when it is ≥ 0.1% of shares outstanding or ≥ US$5m at period close, so a rounding stake cannot move the band. Put and call lines are excluded, and a CIK claimed by two list entries counts once.

MCP data types: Holders / 13F

#351%

Positioning washout

has the register turned over, or is the marginal seller still there?

Type
Trajectory
Data
Historical / observed
Peer
company

Rubric: 0: `turnover_ratio` < 1.25× · 1: 1.25× – 2.0× · 2: ≥ 2.0×

Measure: `float_turnover_12m = Σ volume(t−12m .. t) / free_float_shares`, and `turnover_ratio = float_turnover_12m ÷ median(float_turnover_12m sampled monthly over t−4y .. t−1y)` — how many times faster the free float changed hands than this name's own normal rate. Free float cancels to first order between numerator and denominator, so a stale float count does not bias the ratio, which is what makes this computable from a price history in every market. A forced-basket sale, an index deletion or a tax-loss flush all register here as volume, so none of them has to be separately identified and argued. `unscored` when fewer than four years of price history exist.

MCP data types: Prices

#361%

Governance and minority alignment

are the people making the promises aligned with the minority?

Type
Snapshot
Data
Historical / observed
Peer
company

Rubric: 0: a live Tier-S flag (minority expropriation, undisclosed guarantee, restatement, auditor resignation, executive investigation), or a related-party / pledged-shares flag affirmatively shown material or not arm's-length · 1: a live related-party or pledged-shares flag that is disclosed, arm's-length, ordinary-course and immaterial · 2: no live governance flags

Live governance flags from `build_management_credibility_ledger.py`, each carrying a `tier` from a closed class list. Tier-S conduct zeroes the factor on its own; Tier-A (pledged control shares, related-party dealing) zeroes it only on affirmative evidence — `material: true`, `arm_s_length: false`, `ordinary_course: false`, or a related-party amount ≥ 10% of revenue or assets. A disclosed, arm's-length, ordinary-course related-party dealing is a live flag and scores 1, not 0: it must be watched, but it is not self-dealing. The related-party magnitude test factor 22 used to carry lives here, so one related-party fact has one home. Honesty and fraud route to factor 22; whether capital was returned is factor 28's job.

MCP data types: Filing-textThird-party text

Gates & sizing

Hard gates and the sizing ladder

Gates are computed by assign_sizing_tier.py from factor scores and hard data. No gate reads prose. Sizing is a gate ladder, not score-proportional — an unresolved caveat holds the tier down however cheap the stock is.

#GateTestFailure ⇒
G0Candidacy`dd` < 20% and `depression_ratio` > 0.9 — nothing has fallen and nothing is depressedNOT-A-CANDIDATE — composite reported for the record, never ranked against candidates
G1Tradabilityfactor 25 = 0STRANDED — not scored, excluded from calibration
G2Survivalfactor 21 = 0AVOID at any price
G3Integrityfactor 22 = 0AVOID
G4Permanenceany of factors 11, 12, 13, 14 = 0AVOID
G5Knifefactor 7 = 0 (cuts still accelerating)WAIT — tier ≤ TRACKING
G5bKnife not testablefactor 7 unscored — fewer than 5 reported quarterstier ≤ SATELLITE — CORE is never awarded on an untested knife check
G6Archetypearchetype = momentum_narrativetier ≤ TRACKING
G7Verificationany load-bearing metric UNVERIFIEDtier ≤ SATELLITE
G8Persona0 of 4 personas vote BUYtier ≤ TRACKING
G9Risk-rewardexpected return < 0 or upside:downside < 1.5tier ≤ TRACKING
G0 · Candidacy

`dd` < 20% and `depression_ratio` > 0.9 — nothing has fallen and nothing is depressed

Failure
NOT-A-CANDIDATE — composite reported for the record, never ranked against candidates
G1 · Tradability

factor 25 = 0

Failure
STRANDED — not scored, excluded from calibration
G2 · Survival

factor 21 = 0

Failure
AVOID at any price
G3 · Integrity

factor 22 = 0

Failure
AVOID
G4 · Permanence

any of factors 11, 12, 13, 14 = 0

Failure
AVOID
G5 · Knife

factor 7 = 0 (cuts still accelerating)

Failure
WAIT — tier ≤ TRACKING
G5b · Knife not testable

factor 7 unscored — fewer than 5 reported quarters

Failure
tier ≤ SATELLITE — CORE is never awarded on an untested knife check
G6 · Archetype

archetype = momentum_narrative

Failure
tier ≤ TRACKING
G7 · Verification

any load-bearing metric UNVERIFIED

Failure
tier ≤ SATELLITE
G8 · Persona

0 of 4 personas vote BUY

Failure
tier ≤ TRACKING
G9 · Risk-reward

expected return < 0 or upside:downside < 1.5

Failure
tier ≤ TRACKING
Load-bearing metrics (G7): net cash, the TTM earnings basis, executed buyback dollars, normalized EPS, and the 24-month debt-maturity schedule. Each must carry a primary-filing citation; an aggregator figure not tied to a filing is UNVERIFIED.

Sizing ladder

CORE

Full-conviction size. All gates pass · composite ≥ 70 · Franchise Integrity category ≥ 65% (after the permanence floor) · factor 8 = 2 · factor 26 = 2 · upside:downside ≥ 2.5 · expected return ≥ +25% · zero UNVERIFIED load-bearing metrics · ≥ 2 of 4 personas vote BUY on independent grounds

SATELLITE

Mid size. All gates pass · composite ≥ 55 · Survival category ≥ 60% · upside:downside ≥ 1.8 · at most one live structural caveat

TRACKING

Nominal monitoring. G1–G4 pass · composite ≥ 40

AVOID / WAIT

None. Any of G2–G5 fails, or composite < 40

STRANDED

n/a. G1 fails — reported for completeness, never scored

NOT-A-CANDIDATE

n/a. G0 fails — the name has not fallen. Composite computed and stored, excluded from ranking and calibration

Architecture

Subagents gather; scripts compute; LLMs adjudicate

The orchestrator owns workflow control but never calls MCPs directly. Eight evidence agents run in parallel after scoping, writing durable JSON artifacts under /mnt/session/outputs/. Skill scripts compute measurable inputs; the scorer adjudicates judgment-heavy factors; a challenger and four independent-context persona judges review before the analyst confirm gate.

0

Entry & routing

User request → orchestrator; optional prior fetch; then scoper.

User request
Orchestratorfallen-angel-orchestrator
Optional — fetch prior run
Archivistfallen-angel-archivist
Prior assessmentsaved scorecard
Fresh assessment
Scoperfa-scoper
Scope packbenchmark, peers, window, archetype, na_factorsscoper.json
1

Parallel evidence collection

Eight agents fan out from scoper.json into session evidence packs.

fallen-angel-archivistcompounder importcompounder_import.json
reset-series-agentreset_pack.json
market-price-agentprice_pack.json
fundamentals-agentfinancial_series.json
statement-auditorforensics.json
filing-transcript-agentevidence_disclosures.json
third-party-text-agentevidence_thirdparty.json
ownership-flow-agentownership_pack.json
Evidence packsJSON under /mnt/session/outputs/
2

Calculate & score

Evidence feeds moat diagnostics and factor scripts; scorer emits the scorecard and B/N/B pack.

Diagnostics branch
Moat challengerdiagnostics → factors 12, 14diagnostics.json
Script branch
Factor scriptscalc_fa_factors · apply_business_model_law · build_factor_matrix
Factor matrixfactor_matrix.json
Scorerfa-scorer
From scorer
Scorecardscorecard_fa.json
From scorer
B/N/B enginebuild_bnb_scenarios.py
Scenario packbnb.json
3

Challenge, judge & confirm

Scorecard fans to challenger and personas; bnb.json also enters the challenger; resolved objections feed Munger and the confirm gate.

scorecard_fa.json · bnb.json → challenger
Challengerfa-challenger
Objectionschallenge.json
Rebuttalfa-scorerrebuttal.json
Resolveresolve_challenge.pychallenge_resolved.json

conceded rows → one targeted rescore back to fa-scorer · unanswered_high → persona-munger

scorecard_fa.json → personas
Persona judgesbuffett · munger · hohn · duan
Verdictspersona_verdicts.json
Analyst confirm gatechallenge_resolved · bnb.json · persona_verdicts · 0/1/2 overrides
4

Aggregate, tier, audit & verdict

Confirmed scores become composite and sizing tier; auditor signs off.

Final scoringscore_fallen_angel.py · assign_sizing_tier.py · G0–G9
Auditorscorecard-fa-auditor
Final verdictverdict_fa.md
Optional

When the user asks to save, fallen-angel-archivist writes the finished assessment to Synology NAS — never overwriting prior runs.

Subagents

Specialized roles with clear MCP ownership

SubagentRoleMCPsSkillsOutput
fallen-angel-orchestratorCoordinates the workflow, the confirm gate, and the final summary. Never calls MCPs.fallen-angel-scoping, fa-scoringUser-facing tier + composite summary
fa-scoperResolves entity, listing, archetype, competitive arena, peer cohort, return benchmark, drawdown window, and na_factors by fixed rules — no analyst input.yfinance, wind, stockanalysis, defeatbeta, sec-edgarfallen-angel-scoping, company-scoping, fa-screening, pit-calendarscoper.json
reset-series-agentRealized TTM trajectory by report date, reset persistence and inflection, surprise ledger, reset-quarter detection. Owns the point-in-time calendar. It does not compute ROIC or WACC; those are valuation measures and belong to the fundamentals-agent.stockanalysis, defeatbeta, firecrawl, windfa-reset-enginereset_pack.json, surprise_ledger.json
market-price-agentPrice history, drawdowns, excess versus benchmark, multiple percentiles, base-building, VWAP, volume series, and the liquidity profile that gate G1 reads.yfinance, wind, stockanalysis, firecrawlfa-dislocationprice_pack.json
fundamentals-agentEverything computed from the financial statements: series, core metrics, normalization, shock decomposition, net cash, spendable trough-year FCF, EV, asset floor, distribution cover, yield, ROIC against WACC, owner earnings, the peer-cohort series, Reconciled-Metrics row.wind, stockanalysis, defeatbeta, yfinance, sec-edgarcompounder-v2-fundamentals, fa-normalization, fa-valuation-engine, fa-franchisefinancial_series.json, core_metrics.json, normalization_pack.json, valuation_pack.json, franchise_pack.json, reconciled_metrics.json
statement-auditorForensic tests, fake-cash test, cross-statement consistency, and verification that the cash pile is real.sec-edgar, stockanalysis, wind, firecrawlfa-forensicsforensics.json
filing-transcript-agentPrice/volume/mix bridge, named self-help drivers and KPIs, the dated-catalyst ledger, the promise-versus-delivery ledger behind management credibility, guidance language deltas, concentration, governance.sec-edgar, stockanalysis, defeatbeta, firecrawlpublic-disclosure-extraction, fa-normalizationevidence_disclosures.json, catalyst_ledger.json, credibility_ledger.json
third-party-text-agentShort-seller and bear reports, regulator notices, broker share series, category data, expert-call excerpts, forced-selling events. Sole collector of the four Policy-Permanence checks no issuer filing can answer.rag, firecrawlbroker-research-retrieval, compounder-web-intel, fa-diagnosticsevidence_thirdparty.json, short_seller_ledger.json, policy_observations.json, share_observations.json
ownership-flow-agentForm 4 insider transactions, executed buybacks versus VWAP, 13F position changes among the maintained quality-investor list, and free-float turnover against its own history. Every number it reports comes back from a script; it may not read a 13F or a Form 4 and form a view. Where the issuer files neither, it returns `na` rather than a neutral score.edgar-13f, sec-edgar, yfinance, firecrawlfa-ownership-flowownership_pack.json
moat-challengerScores factors 12 and 14 by running the Policy-Permanence and AI-Disruption diagnostics test by test, in a context that has never seen the bull case, and reads the pricing-power and cohort series behind factors 10, 11 and 13 without seeing how they were framed. Holds no connector of its own, so it reads the policy observations the third-party text agent gathered for the checks its own sources cannot reach.fa-diagnostics, fa-franchisediagnostics.json
fa-scorerBuilds the factor matrix, assigns 0/1/2 against numeric rubrics with cited evidence, runs the aggregator and the B/N/B engine.fa-factor-calculation, fa-scoringscorecard_fa.json, bnb.json
fa-challengerBear-case audit: double counting, cohort gaming, expectation overreach, B/N/B legs that breach the name's own multiple history, evidence that fails the cited rubric clause. Every objection carries a primary citation and names the input or admitted basis it disputes.fa-challenge, fa-scoring, compounder-v2-evidence-checkschallenge.json
persona-buffett · persona-munger · persona-hohn · persona-duanFour master cards, each an independent-context subagent with no shared scratchpad. Each verdict is computed from rules the card declared in advance; the card writes the sentence and never the verdict. Verdicts are reported, never averaged into the composite.fa-personaspersona_verdicts.json
scorecard-fa-auditorIndependent recompute plus opening each cited source and confirming it says what the scorer claims it says.fa-scoringaudit.json
fallen-angel-archivistThe only subagent with NAS access. Imports the Compounder v2 composite for factor 9, fetches prior runs, saves finished assessments, and maintains the PIT consensus snapshot store.synologycompounder-archive, fallen-angel-archive, fa-compounder-importcompounder_import.json, NAS artifacts
fallen-angel-orchestrator

Coordinates the workflow, the confirm gate, and the final summary. Never calls MCPs.

MCPs
Skills
fallen-angel-scoping, fa-scoring
Output
User-facing tier + composite summary
fa-scoper

Resolves entity, listing, archetype, competitive arena, peer cohort, return benchmark, drawdown window, and na_factors by fixed rules — no analyst input.

MCPs
yfinance, wind, stockanalysis, defeatbeta, sec-edgar
Skills
fallen-angel-scoping, company-scoping, fa-screening, pit-calendar
Output
scoper.json
reset-series-agent

Realized TTM trajectory by report date, reset persistence and inflection, surprise ledger, reset-quarter detection. Owns the point-in-time calendar. It does not compute ROIC or WACC; those are valuation measures and belong to the fundamentals-agent.

MCPs
stockanalysis, defeatbeta, firecrawl, wind
Skills
fa-reset-engine
Output
reset_pack.json, surprise_ledger.json
market-price-agent

Price history, drawdowns, excess versus benchmark, multiple percentiles, base-building, VWAP, volume series, and the liquidity profile that gate G1 reads.

MCPs
yfinance, wind, stockanalysis, firecrawl
Skills
fa-dislocation
Output
price_pack.json
fundamentals-agent

Everything computed from the financial statements: series, core metrics, normalization, shock decomposition, net cash, spendable trough-year FCF, EV, asset floor, distribution cover, yield, ROIC against WACC, owner earnings, the peer-cohort series, Reconciled-Metrics row.

MCPs
wind, stockanalysis, defeatbeta, yfinance, sec-edgar
Skills
compounder-v2-fundamentals, fa-normalization, fa-valuation-engine, fa-franchise
Output
financial_series.json, core_metrics.json, normalization_pack.json, valuation_pack.json, franchise_pack.json, reconciled_metrics.json
statement-auditor

Forensic tests, fake-cash test, cross-statement consistency, and verification that the cash pile is real.

MCPs
sec-edgar, stockanalysis, wind, firecrawl
Skills
fa-forensics
Output
forensics.json
filing-transcript-agent

Price/volume/mix bridge, named self-help drivers and KPIs, the dated-catalyst ledger, the promise-versus-delivery ledger behind management credibility, guidance language deltas, concentration, governance.

MCPs
sec-edgar, stockanalysis, defeatbeta, firecrawl
Skills
public-disclosure-extraction, fa-normalization
Output
evidence_disclosures.json, catalyst_ledger.json, credibility_ledger.json
third-party-text-agent

Short-seller and bear reports, regulator notices, broker share series, category data, expert-call excerpts, forced-selling events. Sole collector of the four Policy-Permanence checks no issuer filing can answer.

MCPs
rag, firecrawl
Skills
broker-research-retrieval, compounder-web-intel, fa-diagnostics
Output
evidence_thirdparty.json, short_seller_ledger.json, policy_observations.json, share_observations.json
ownership-flow-agent

Form 4 insider transactions, executed buybacks versus VWAP, 13F position changes among the maintained quality-investor list, and free-float turnover against its own history. Every number it reports comes back from a script; it may not read a 13F or a Form 4 and form a view. Where the issuer files neither, it returns `na` rather than a neutral score.

MCPs
edgar-13f, sec-edgar, yfinance, firecrawl
Skills
fa-ownership-flow
Output
ownership_pack.json
moat-challenger

Scores factors 12 and 14 by running the Policy-Permanence and AI-Disruption diagnostics test by test, in a context that has never seen the bull case, and reads the pricing-power and cohort series behind factors 10, 11 and 13 without seeing how they were framed. Holds no connector of its own, so it reads the policy observations the third-party text agent gathered for the checks its own sources cannot reach.

MCPs
Skills
fa-diagnostics, fa-franchise
Output
diagnostics.json
fa-scorer

Builds the factor matrix, assigns 0/1/2 against numeric rubrics with cited evidence, runs the aggregator and the B/N/B engine.

MCPs
Skills
fa-factor-calculation, fa-scoring
Output
scorecard_fa.json, bnb.json
fa-challenger

Bear-case audit: double counting, cohort gaming, expectation overreach, B/N/B legs that breach the name's own multiple history, evidence that fails the cited rubric clause. Every objection carries a primary citation and names the input or admitted basis it disputes.

MCPs
Skills
fa-challenge, fa-scoring, compounder-v2-evidence-checks
Output
challenge.json
persona-buffett · persona-munger · persona-hohn · persona-duan

Four master cards, each an independent-context subagent with no shared scratchpad. Each verdict is computed from rules the card declared in advance; the card writes the sentence and never the verdict. Verdicts are reported, never averaged into the composite.

MCPs
Skills
fa-personas
Output
persona_verdicts.json
scorecard-fa-auditor

Independent recompute plus opening each cited source and confirming it says what the scorer claims it says.

MCPs
Skills
fa-scoring
Output
audit.json
fallen-angel-archivist

The only subagent with NAS access. Imports the Compounder v2 composite for factor 9, fetches prior runs, saves finished assessments, and maintains the PIT consensus snapshot store.

MCPs
synology
Skills
compounder-archive, fallen-angel-archive, fa-compounder-import
Output
compounder_import.json, NAS artifacts

Scripts & skills

Scripts compute inputs; LLMs adjudicate judgment

Drawdowns, reset trajectories, forensics, net cash, ROIC/WACC, ownership flows, and tier assignment belong in Python scripts. Franchise permanence, shock decomposition, and persona judgment still require LLM adjudication on filings, transcripts, and third-party text — always against a numeric rubric or a named checklist.

fallen-angel-scopingfa-scoper
  • benchmark_map.json

    Region and listing-venue to index-ticker map.

  • resolve_benchmark.py

    Geographic revenue split, 60% test, domicile fallback.

pit-calendarfa-scoper
  • resolve_pit_calendar.py

    Fiscal year end, the annual and interim publication boundaries for any venue, and the periods that ended with no report yet published. Shared with Compounder v2.

fa-compounder-importarchivist
  • import_compounder_score.py

    Newest Compounder v2 composite from the archivist's NAS lookup, the analyst and proxy fallbacks, and whether the archive was searched at all.

fa-reset-enginereset-series-agent
  • compute_reset_trajectory.py

    TTM revenue and EBIT by report date; reset persistence and inflection.

  • build_surprise_ledger.py

    Match each quarter's actual to pre-report consensus, date it from the transcript index, and anchor factor 8 on the most recent miss, counting clean beats since.

  • detect_reset_quarter.py

    The quarter the earnings base re-set — the largest negative step in TTM EBIT. Reported alongside factor 8 and never its anchor: a company can miss a quarter in which earnings rose, and factor 8 asks whether it is clearing the bar others set.

  • fetch_quarterly_consensus_proxy.py

    Per-quarter consensus versus actual; the point-in-time anchor outside A-shares.

  • validate_beatmiss_tieout.py

    Reject any extracted quarter whose beat/miss delta does not tie out.

  • snapshot_pit_consensus.py

    Nightly capture of consensus EPS, revenue and targets into the NAS store. No factor depends on it; it exists so the realized-results proxy can be cross-checked later.

  • build_pit_consensus_series.py

    Assemble the as-known-then monthly series from the store plus Wind history; the tightening overlay on gate G5, never the scoring basis.

  • build_reset_pack.py

    Merge into the single reset pack the factor calculator reads by key name.

fa-dislocationmarket-price-agent
  • compute_drawdown_excess.py

    Drawdown, excess versus benchmark, peak and trough dates.

  • compute_multiple_percentiles.py

    P/S percentile over the trailing seven years of monthly observations, reported with the current, median and minimum readings (factor 2).

  • compute_base_building.py

    Months since the 12-month low, rise off the low, new-low recency (factor 3).

  • compute_positioning_series.py

    Free-float turnover against its own three-year median, and the 12-month VWAP (factors 33, 35).

  • compute_liquidity_profile.py

    60-day median dollar ADV, halt and delisting status, free-float share count (factor 25, gate G1).

  • build_price_pack.py

    Merge into the single price pack the factor calculator reads by key name.

fa-normalizationfundamentals-agent, filing-transcript-agent
  • compute_normalized_earnings.py

    Pre-shock median margin, normalized EPS, depression ratio.

  • build_shock_decomposition_bridge.py

    Peak-to-trough EBIT bridge with the decline derived from the published series; reversible_share and the driver rows behind it.

  • build_recovery_base.py

    Splits the pre-shock revenue base into what still operates, what was abolished and what a named rival now holds (factor 18).

  • track_selfhelp_kpis.py

    Named drivers, YoY margin expansion, gap_closed.

  • build_catalyst_ledger.py

    One row per catalyst with mechanism, citation, date, and whether it falls inside the horizon; undated catalysts flagged (factor 20).

  • build_management_credibility_ledger.py

    Each public commitment joined to what was delivered, with citations for both halves, plus the governance and alignment facts (factors 31, 36).

  • build_normalization_pack.py

    Merge into the normalization and disclosures packs the factor calculator reads by key name.

fa-valuation-enginefundamentals-agent
  • compute_net_cash.py

    Cash, investments, borrowings; lease variant; also emits net_cash_pct_mcap, which is factor 27's band. Restricted, escrow and client balances are named and never added. Feeds factors 21, 23, 26, 27.

  • compute_trough_fcf.py

    Spendable trough-year FCF: reported FCF minus the change in the same excluded balances net cash already stripped from the stock, then the worst remaining year (factor 21's burn wall, factor 24).

  • resolve_debt_maturity_schedule.py

    Debt due within 24 months via the not-binding shortcut, then SEC XBRL, then the filing table (factor 21, gate G2).

  • compute_ev_normalized.py

    Enterprise value and EV / normalized EBIT (factor 26).

  • compute_asset_floor.py

    Price to tangible book, plus the impairment read that drives the score cap (factor 23).

  • compute_distribution_sustainability.py

    Distributions against TTM free cash flow and any announced or implied cut; na when nothing is paid (factor 24).

  • compute_executed_yield.py

    Dividends and buybacks executed LTM (factor 28).

  • compute_roic.py

    NOPAT, invested capital variants, ROIC series, and the after-tax portfolio income that keeps roic_total's numerator describing the same company as its denominator (factor 29).

  • resolve_equity_risk_premium.py

    The country premium from the Damodaran vintages, chosen on the publication date read out of each workbook rather than its filename — the archive names files by data year and publishes them the January after. A vintage published after the as-of date is rebuilt onto the monthly mature premium rather than used or discarded (factor 29).

  • compute_wacc.py

    Point-in-time beta regressed by hand, R², and the resolved ERP — a premium arriving without a source is dropped, since beta multiplies it. Below the R² floor no benchmark explains the name, so the WACC comes back null and factor 29 scores against the fixed 8%/15% bands rather than a stand-in hurdle chosen to reproduce them (factor 29).

  • build_owner_earnings_bridge.py

    Owner earnings gap (factor 30).

  • build_reconciled_metrics_row.py

    Mandatory evidence row; feeds gate G7.

  • build_valuation_pack.py

    Merge into the single valuation pack the factor calculator reads by key name; two artifacts disagreeing about a field is reported rather than resolved. A trough still on reported FCF while net cash stripped excluded balances is refused as mixed-basis.

fa-forensicsstatement-auditor
  • compute_forensic_scores.py

    Beneish M, Altman Z and Piotroski F, all computed from the statements.

  • run_cash_authenticity_tests.py

    Implied cash yield against the local risk-free rate; interest-income reconciliation.

  • build_forensic_flag_ledger.py

    Seven fixed forensic tests with thresholds (factor 22, gate G3).

fa-ownership-flowownership-flow-agent
  • quality_investor_list.json

    The investors factor 34 watches, keyed by CIK rather than name. Only institutions that actually file a 13F are on it, each stamped with the filer name and a recent filing, so a name that resolves to nothing is a known absence rather than a silent zero.

  • compute_insider_net_buying.py

    Open-market purchases net of sales, counting transaction code P against S. Grants, option exercises, tax withholding and 10b5-1 sales are excluded, and the count of excluded scheduled sales is reported so a reader can see how much of the net figure rests on that exclusion (factor 32).

  • compute_buyback_execution.py

    Shares retired versus 12-month VWAP (factor 33).

  • compute_smart_money_delta.py

    Quality-investor list position changes (factor 34). The script owns every guard in the rubric — US-listing check, CIK-only resolution, CUSIP history, filing-deadline dating, under-filed-quarter rejection, materiality floor — and the agent passes the returned band through unchanged.

  • build_ownership_pack.py

    Merge into the single ownership pack the factor calculator reads by key name.

fa-franchisemoat-challenger, fundamentals-agent
  • compute_pricing_power.py

    Gross-margin change plus disclosed-price enrichment (factor 10).

  • build_peer_cohort_series.py

    Cohort revenue series for factors 11 and 13.

  • build_franchise_pack.py

    Merge into the single franchise pack the factor calculator reads by key name.

fa-diagnosticsmoat-challenger, third-party-text-agent
  • score_diagnostic_checklists.py

    Policy-Permanence and AI-Disruption pass rates over answered checks → factors 12, 14.

  • build_short_seller_ledger.py

    Allegation ledger with routing to factors 15, 22, 11–14. Routed allegations leave a count behind, so factor 15 never reads an emptied ledger as a clean record.

  • build_diagnostics_pack.py

    Merge into the diagnostics pack the factor calculator reads by key name.

fa-factor-calculationfa-scorer
  • calc_fa_factors.py

    Compute deterministic factors; prepare judgment-heavy inputs.

  • apply_business_model_law.py

    Mark na by business model, archetype, listing, disclosure.

  • build_factor_matrix.py

    One row per factor with band or adjudication prompt.

fa-scoringfa-scorer, auditor
  • factors_fa.json

    The catalog itself — every factor's weight, rubric, na rule and score cap. Generated from this framework document, and a test fails if the document, the catalog and this page ever disagree.

  • score_fallen_angel.py

    Apply 0/1/2, weights, caps, permanence floor; produce composite.

  • build_bnb_scenarios.py

    Deterministic Bull/Neutral/Bear/Tail engine. Every multiple is one the market actually paid for this name, every floor is the balance sheet net of the burn it funds, and a leg whose inputs are absent is unresolved rather than a constant.

  • assign_sizing_tier.py

    Evaluate gates G0–G9 and assign the ladder tier. G9 fails on an unresolved scenario ladder, not only on the two computed figures.

  • audit_scorecard_fa.py

    Independent recompute and evidence-link validation.

fa-challengefa-challenger, fa-scorer
  • validate_challenge.py

    Enforce objection schema; every challenge needs a primary citation and must name the input or admitted basis it disputes.

  • resolve_challenge.py

    Join objections to rebuttals; compute unanswered_high for Munger.

fa-personaspersona judges
  • decide_persona_verdicts.py

    Decide all four persona verdicts from the rules each card declared in advance.

fa-screeningfa-scoper
  • screen_kanshuzi.py

    Drawdown and liquidity screen over a market, for the candidate-pool mode that finds names to run rather than scoring one.

  • rank_two_stage.py

    Cheap factors first, then the full pack only on survivors, so a sweep does not pay for evidence on names it will discard.

  • build_candidate_pool.py

    The ranked pool with the reason each name entered.

fallen-angel-archivefallen-angel-archivist
  • save_fa_assessment.py

    Write the finished run to NAS — assessment, scorecard, scenario pack and challenge — on the Compounder v2 folder-per-ticker convention.

  • fetch_prior_fa_runs.py

    Prior assessments for the same entity, newest first, for the run-over-run delta; empty list on a first run.

Scoring

A 0–100 composite and a sizing tier

Each factor contributes score / 2 × weight to a 0–100 composite score, which ranks candidates. The sizing tier is assigned separately by the gate ladder — so a high composite can still be held at TRACKING when a knife or risk-reward gate fails.

Composite

Each factor contributes score / 2 × weight to a 0–100 composite, rescaled over the applicable weight base. The composite ranks candidates; the tier decides what to do about them.

No confidence multipliers

Weights are set directly. Insufficient evidence produces status: unscored, which removes the weight from the base and drags coverage — never a quietly softened contribution.

No tilts

Value-trap and permanence live in gate G4 and the permanence floor; falling-knife in factor 7 and gate G5; catalyst clarity in factor 20; cyclical-versus-structural in factor 17; forced-selling in factor 35.

Franchise permanence floor

Franchise Integrity category percentage is capped at min(score) / 2 across the permanence vectors (11–14). A vector at 0 fires gate G4. An unscored vector enters the minimum at a neutral 1 — a vector nobody could determine is not one that came back clean — and only na_inapplicable leaves it.

B/N/B engine

Recovery is earnings normalization × multiple re-rating. Four legs from computed inputs; probabilities derived from Franchise Integrity and Revision Cycle category percentages. Feeds gate G9 and CORE/SATELLITE risk-reward thresholds — never the composite.

Challenge–rebuttal

Every objection and rebuttal carries a primary citation. unanswered_high = 0 is required for Munger's BUY. Conceded rows trigger one targeted rescore.

Analyst confirm gate

The frontend renders every factor with proposed 0/1/2, computed inputs, evidence, objections with both citations, B/N/B legs, and persona verdicts. Overrides are recorded; a tier-changing override opens an Adjudication Ledger row.

Analyst confirm gate: the frontend renders every factor with its proposed 0/1/2, computed inputs, evidence, challenger objections with both citations, B/N/B legs, and persona verdicts. Analysts may override individual factors before score_fallen_angel.py and assign_sizing_tier.py run for the final time.

Personas

Four independent taste cards

Each persona is an independent-context subagent with no shared scratchpad. Verdicts are BUY / NO / TOO-HARD / NO-VOTE, reported alongside the score and never averaged into it. They act only through gate G8 and the CORE requirement of ≥ 2 BUY votes. Deterministic disqualifiers are checked before the card reasons at all.

Buffett

Decision function

Circle of competence in one paragraph; moat over growth; owner earnings over reported EPS; would you hold it if the market closed for five years?

Disqualifiers

Auto-NO if factor 18 = 0. Auto-TOO-HARD if factor 17 = 0 or coverage < 60%.

BUY requires

Factors 9 ≥ 1, 10 ≥ 1, 21 ≥ 1, 26 = 2

Munger

Decision function

Inversion first — kill the thesis before buying; incentive-bias audit; lollapalooza of independent moat sources.

Disqualifiers

Auto-NO if any permanence vector = 0, factor 22 = 0, or factor 36 = 0 (the incentive-bias / alignment audit). Auto-TOO-HARD if factor 12 or 14 is unscored.

BUY requires

unanswered_high = 0 in challenge_resolved.json — every high-severity objection either conceded and rescored, or refuted on primary evidence — and a kill thesis logged with its own citation

Hohn

Decision function

Irreplaceable infrastructure — would unlimited capital still fail to displace it in ten years? Pricing power is the test.

Disqualifiers

Auto-NO if factor 10 = 0.

BUY requires

Factors 10 = 2, 11 ≥ 1, and the imported compounder scorecard's moat.structural_barriers ≥ 1

Duan Yongping

Decision function

Business model first; buying a stock is buying the company; dare to be last in the world — buy after the proof, not before it; honesty and staying within one's duty.

Disqualifiers

Auto-NO if factor 31 = 0 (credibility — did they keep their promises).

BUY requires

Factors 9 ≥ 1, 30 ≥ 1 (owner earnings clean), 8 ≥ 1 (the proof has been delivered)

Diagnostics

Checklists whose pass count sets a factor band

Diagnostics are evidence, never a separate score. The pass count converts to the factor's 0/1/2 band; named disqualifiers can force a 0 regardless of pass count.

Policy-Permanence Diagnostic

Factor 12. Eight checks, each answered ANGEL, TRAP or unknown with a primary citation and a source class: localized versus national spread; the regulator's true objective versus the headline; enforcement sustainability; symbolic signals; GR-channel intelligence; leading-indicator exit feed; documented compliance restructuring; confirmed stability period. Every check names the records that can answer it, and four of the eight are answerable nowhere in an issuer's own filings — a verdict cited to a record that cannot hold the answer is unknown, not a failure. The band is the pass rate over the answered checks, so a question nobody could settle costs the name nothing. A banned or structurally capped business model with no compliant path forces 0, unless it contradicts a check that passed.

AI-Disruption Diagnostic

Factor 14. Twelve tests including moat-layer localization, customers rebuilding internally, AI-native competitors, whether the job-to-be-done disappears, incumbent offense shipping and monetizing, and the bear-case annuity floor. Each carries a citation and a source class, split by who the test is about: the subject's own filings and calls answer the tests about the subject, while the customer-rebuild and AI-native-competitor tests ask what the rest of the market is doing and admit no filing at all. Those two are also the pair that forces a 0, and both are phrased as negatives — so a pass taken from a record that could never have found the thing switches the disqualifier off rather than merely inflating the count. The band is the pass rate over the answered tests. Score 0 fires hard gate G4. Score the name, never the category.

Short-Seller Ledger

Factor 15. Every short and bear report in the last 24 months logged with novelty, independent verification, rebuttal, severity, and routing. Fraud and solvency allegations route to factor 22; moat and demand allegations route to factors 11–14.