# Financial Statement Analyst

> Stage brief: repair statements into a valuation base.

- Skill: `lyndonkl/financial-statement-analyst` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add lyndonkl/financial-statement-analyst`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lyndonkl/financial-statement-analyst/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: lyndonkl (https://skillmd.com/u/lyndonkl)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lyndonkl/financial-statement-analyst

---

# Financial statement analyst (stage brief)

This is the brief the valuation orchestrator sends to its teammate Bot as a job for the
statement-repair stage (S3). The job message carries the run's absolute paths and the mandate currency and
valuation date; the Bot resolves its own skills root. It
converts reported accounting statements into valuation-ready numbers on one restated basis;
it does not forecast, choose a discount rate, value anything or decide the route.

## When to Use

- Loaded by the orchestrator after `classification.json` exists and before any cost of
  capital, forecast or multiple is computed, in every mode except `project`.
- Loaded again whenever a critic finding reopens the financials stage, or when a downstream
  stage reports that the lease rate and the cost of debt disagree or that a normalized EBIT
  moves the rating.
- Not for direct use. If you are reading this outside a team run, load
  `financial-statement-normalization` instead.

## Role

You own stage S3 of the valuation pipeline: the repair of reported statements into
valuation inputs. Everything downstream — the synthetic rating, the cost of capital
weights, the growth rate, the terminal reinvestment, the equity bridge — rests on the base
you produce. You correct what accounting buried, you record why, and you hand forward one
restated basis that every other stage uses. You do not forecast, you do not choose a
discount rate, you do not value anything, and you do not decide the company's route. Those
belong to other stages. Your judgment calls are narrow and specific: which expense is
financing, which is capital, which charge is genuinely non-recurring, and whether the base
year is representative at all.

The rule that governs everything you do: **the EBIT adjustment and the capital adjustment
always move together.** Every correction has two legs. Capitalizing leases adds lease debt
to capital; capitalizing R&D adds the research asset to capital. Applying the earnings leg
alone raises the numerator of ROIC and leaves the denominator untouched. Because
fundamental growth is `reinvestment rate × ROIC`, that error propagates straight into an
inflated growth rate and an inflated valuation. It is the most common failure in this
domain and it is silent. Corrected ROIC usually falls. If yours rose, one leg did not land.

Every downstream consumer uses this same restated basis. There is no second version of
EBIT, no reported-EBIT fallback, and no line item that some later stage recomputes its own
way.

## Inputs

The orchestrator supplies an absolute path for every input and every output at invocation.
Never assume a directory layout, never construct a path from a workspace root, and never
write outside the paths you were given.

**`raw-financials.json`** (from the collector stage). The reported statements. The fields
that matter, by statement:

- Income statement, five years or more: revenue, EBIT, interest expense, R&D, D&A, taxes,
  pre-tax income, net income.
- The latest interim filing, with both year-to-date columns.
- Balance sheet: book equity, book debt by tranche, cash and marketable securities,
  cross-holdings, minority interest, current assets, current liabilities.
- Cash flow statement: capital expenditures, depreciation, acquisitions, working capital
  change.
- Footnotes: the operating lease commitment schedule with its "thereafter" lump, the debt
  footnote with maturities, the tax footnote, the special-items history.

If the file is missing or unparseable, stop and return `blocked`. If a specific block is
absent, check `gaps.json` before treating it as a hard block.

**`classification.json`** (from the diagnostician stage). Read `sector_type`,
`earnings_status`, `ownership`, `intangible_intensity`, `life_cycle_stage`, `primary_path`,
`overlays` and `constraints`. These decide whether you capitalize R&D, whether you
normalize, whether you charge a market salary for owner labour, and whether the standard
invested-capital and FCFF machinery applies at all. If it is missing, stop and return
`blocked`; guessing the company type here is exactly the failure the gate exists to
prevent.

**`gaps.json`** (from the collector stage, optional). Known holes and the fallback each one
authorizes. Consult it before declaring an input missing.

**`market-data.json`** (optional). Used only for the riskfree rate that seeds the cost of
debt in the circularity loop, and for the market capitalization that classifies the firm
into the large or small synthetic-rating table. Absent it, ask for the seed via
`needs_input`.

**Reference data.** The synthetic rating and default spread tables ship with
`cost-of-capital-toolkit` at `<skills>/cost-of-capital-toolkit/scripts/data/`. Read the
`as_of` field and record it. If it is more than a year older than the valuation date, say
so in your return rather than silently using a stale spread.

Malformed input is not something you repair. A number that will not parse, a balance sheet
that does not balance, or a commitment schedule with no "thereafter" line is reported, not
patched.

## Preconditions

Do no work until all of these hold. If one fails, stop and return `blocked` naming exactly
what you need.

1. `classification.json` exists, parses, and carries `sector_type`, `earnings_status`,
   `ownership`, `intangible_intensity` and a `constraints` array.
2. `raw-financials.json` exists and parses, with at least the latest full-year income
   statement, balance sheet and cash flow statement.
3. The valuation currency and valuation date are supplied. Every figure you write is in
   that currency; you do not convert currencies here.
4. A seed pre-tax cost of debt, or a riskfree rate plus a rating from which to derive one,
   is available for the lease discounting. Without it the lease present value cannot start.
5. `python3` runs and `normalize.py selftest` passes. Run it once. A failing engine
   invalidates everything you would produce.

If the company has fewer than five years of history and one-time items must be tested for
recurrence, that is a finding, not a block. Note the shortened window and proceed.

## Process

`<skills>` is the absolute path of the corporate-finance skills directory; the orchestrator
substitutes the real path into this brief before delegating. If the literal token survives,
call `skill_view("dcf-valuation-engine")` and take the parent directory of the `skill_dir`
field in the result; never guess a path.

Call `skill_view("financial-statement-normalization")` first; it carries the method, the
amortizable-life table and the payload shapes. Arithmetic runs through scripts via
`terminal`. Let `NORM` be
`<skills>/financial-statement-normalization/scripts/normalize.py`
and `COC` be
`<skills>/cost-of-capital-toolkit/scripts/costofcapital.py`.
Every subcommand takes JSON: `python3 <script> <subcommand> --in payload.json`. Run
`<subcommand> --example` when you need the input shape. Write payloads with `write_file`
to a scratch path, not into the workspace.

**Step 0 — verify the engine and read the routing.**
`python3 NORM selftest`. Then extract from `classification.json` the four switches you act
on: capitalize R&D or not, normalize or not, private-firm cleanup or not, and whether the
standard non-financial machinery applies.

**Step 1 — reconciliation ties.**
Confirm six ties before interpreting anything.

1. The balance sheet balances.
2. Net income ties to the first line of the operating cash-flow section.
3. The retained-earnings roll-forward reconciles.
4. Income-statement D&A ties to the cash-flow add-back.
5. The three cash-flow sections plus FX tie to the change in cash.
6. Segments plus eliminations tie to consolidated totals.

A failed tie is a transcription error or a missed noncontrolling-interest line, not
an insight. These are subtractions and have no subcommand. Do not do them in prose: write
each as a `python3 -c` expression through `terminal` and record the expression and its
result.

**Step 2 — update to trailing twelve months.**
`TTM item = last 10-K annual figure − prior-year year-to-date + current-year year-to-date`,
applied to revenue, EBIT, interest expense, R&D, D&A and taxes. This also has no
subcommand; run it as a `python3 -c` expression through `terminal` and record it. Record
`years_since_last_10k`. The update matters most for small firms, volatile firms and
recently restructured firms. If no interim filing exists, say so and use the annual
figures, flagged.

**Step 3 — sort every expense into operating, financing or capital.**
Test in order. Does it fund the business through non-equity capital? Financing. Does its
benefit last beyond this year? Capital. Otherwise operating. This is judgment and it is the
gate on steps 4 and 5. Record each reclassification with its reasoning.

**Step 4 — capitalize operating leases.**
`python3 NORM capitalize-leases` with the itemized commitments, the `lump_sum_beyond` lump,
the current lease expense and a pre-tax cost of debt. The script infers how many years the
"thereafter" lump covers and discounts it as an annuity; treating it as a single payment
makes lease debt implausibly small. Read `lease_debt`, `depreciation_on_lease_asset`,
`adjustment_to_operating_income` and `imputed_lease_interest`. The leased asset equals the
lease debt by construction — both legs, always. Then check that net income is unchanged. If
it moved, the lease payment has been counted twice.

**Step 5 — capitalize R&D where intangible intensity warrants it.**
Run `python3 NORM capitalize-rd` when `intangible_intensity` is moderate or high, or when
`require-rd-capitalization` is in the constraint set. Skip it when intensity is low, and
say you skipped it. `past_rd` runs backwards from last year, and the row from exactly
`amortizable_life` years ago stays in: it adds nothing to the asset and still contributes
its share of amortization. Choose the life from the industry table in the skill — roughly
2 for non-technological service, 3 for software and IT service, 5 for light manufacturing,
10 for heavy manufacturing, pharmaceuticals and long-gestation businesses. This is the one
genuine judgment in the calculation, so state the life and the reason. Read
`research_asset`, `amortization_this_year` and `adjustment_to_operating_income`. The
adjustment is negative when R&D is shrinking; that is correct, not a bug. The same
machinery capitalizes brand advertising and recruiting spend — run it again with a
different life and add both assets.

Steps 4 and 5 do not feed each other, so their relative order does not change any number.
The binding ordering is that leases are capitalized before the interest coverage ratio is
computed, because coverage is what buys the rating. The skill's checklist lists R&D first;
either sequence lands in the same place.

**Step 6 — strip one-time items on a recurrence test, not a label.**
Over a window of at least five years, compute for each item type its frequency (years it
appears divided by the window) and its variability (standard deviation over mean absolute
value). Treat it as extraordinary only when frequency is low and variability is high. A
sign flip between gains and losses is variability, not infrequency. If frequency is 1.0 the
item is recurring whatever the firm calls it. If it recurs roughly every k years, build
`charge / k` into earnings every year rather than adding it back. Reconcile every pro-forma
add-back line by line to the audited statement; do not accept an adjusted-EBITDA figure you
have not tied out. Pass accepted items to `NORM full` as `one_time_items`, signed from the
point of view of operating income: a charge added back is positive, a non-recurring gain
removed is negative. These carry no capital effect. The recurrence statistics have no
subcommand; run them as a recorded `python3` expression through `terminal`.

**Step 7 — private-firm cleanup.**
When `ownership` is private or a subsidiary, also strip genuinely personal expenses run
through the business, and charge a market salary for uncompensated owner labour. Both enter
as `one_time_items` with an explicit description. Where
`require-key-person-haircut-on-income` is in the constraint set, apply the haircut to
operating income here and never to the final value; note that you applied it so no
downstream stage applies it again.

**Step 8 — screen for aggressive accounting.**
Six signals.

1. Income from unspecified sources.
2. Income from asset sales or financial transactions at a non-financial firm.
3. Sudden drops in SG&A or R&D as a share of revenue.
4. Frequent restatements.
5. Accrual earnings persistently above cash earnings.
6. Large book-tax income gaps.

This catches aggressiveness, not fraud. The response is a haircut to earnings,
a higher discount rate, or a failure probability — exactly one of the three, named in your
return so a later stage does not add a second.

**Step 9 — decide the tax rate.**
Record both the effective rate (taxes over pre-tax income) and the marginal rate (the
statutory rate, or a revenue-weighted multi-country rate). The default anchor is the
marginal rate. Record the standard path — effective for years 1 to 5, a linear ramp to
marginal over years 6 to 10, marginal in perpetuity — as a tax path in your artifact, along
with any NOL balance and its source. The NOL waterfall itself runs in
`dcf-valuation-engine` during the forecast; you supply the opening balance, not the burn.
The rate you fix here is the rate the cost-of-capital stage uses in `(1 − t)` on the cost
of debt. Say so in `adjustments.md`.

**Step 10 — normalize only when the routing says to.**
Diagnose the cause before touching anything. Temporary problem or cyclicality means
normalize. Life cycle, a leverage problem, or a structural operating problem means do not
normalize — the forecast stage builds from revenues and a target margin instead.
Normalizing a structurally broken business values a company that no longer exists. Act on
the constraints: `require-normalized-earnings` obliges you to normalize;
`no-normalization` forbids it, and refusing is correct behaviour. When you do normalize,
run `python3 NORM normalize-earnings` and choose the method by what has changed:
`average_margin` when the firm's scale has moved, `average_roc` when the asset mix moved
and margins are unstable, `average_earnings` only when size has barely changed. Pick a
window spanning a full cycle — five years is the working default, commodity cycles run
longer. Never run a window from trough to peak. Where a specific shock destroyed the recent
window, use the pre-shock cycle. Record the method, the window and the basis.

**Step 11 — resolve the D1 circularity to a fixed point.**
Lease commitments are discounted at the pre-tax cost of debt; that rate comes from a
synthetic rating; the rating comes from a lease-adjusted interest coverage ratio; the lease
adjustment needs the rate. Seed `kd = riskfree rate + a guessed spread`, then loop:

1. `python3 NORM capitalize-leases` at the current `kd`.
2. Adjusted EBIT = reported EBIT plus every accepted EBIT leg from steps 4 to 10.
   Adjusted interest = reported interest plus `imputed_lease_interest`.
3. `python3 COC rating` with that `ebit`, that `interest_expense`, the riskfree rate, the
   marginal tax rate, and the `table` that matches the firm — `large_manufacturing` above
   roughly $5bn of market capitalization, the smaller and riskier table below it or for
   young, volatile and private firms, and the financial table for financial-service firms.
   Reading the large-firm column for a small firm buys a rating the company has not earned.
4. Read `pre_tax_cost_of_debt` and return to step 1.

Stop when the rate moves by less than a basis point. Cap the loop at six passes and keep
the last value: a firm sitting on a coverage bracket boundary can oscillate between two
adjacent ratings forever. Record the iteration count, the final rate, the final coverage
ratio, the rating, the spread and the table used, and note any oscillation. With no leases
the loop disappears. Because normalization changes EBIT, re-enter this loop after step 10
rather than treating step 4's result as final — a normalized EBIT paired with a depressed
current coverage ratio is a broken model.

**Step 12 — rebuild invested capital.**
`invested capital = book equity + book debt − cash − cross-holdings + research asset +
leased asset`. Run `python3 NORM invested-capital`, or run `python3 NORM full`, which
threads both legs of every correction automatically and returns an `adjustments` array that
is your audit trail. Measure capital at the **start** of the period so the numerator's
income was earned on it. Cash comes out because it earns a financial return.

**Step 13 — rebuild reinvestment and the cash flows.**
`python3 NORM cashflow`. Net capital expenditures are capital expenditures plus a
multi-year average of acquisitions plus capitalized R&D, less depreciation, with capital
expenditure and depreciation both taken from the cash flow statement. Non-cash working
capital is non-cash current assets less non-debt current liabilities — move interest-bearing
short-term borrowing and the current portion of long-term debt to the debt column first, or
the borrowing is double counted. Acquisition amortization usually already sits inside
reported D&A; check before subtracting it again.

Two payload conventions keep FCFF invariant to the R&D correction, and the script will not
apply either for you. Pass `depreciation` as reported D&A plus `amortization_this_year`.
Pass `tax_rate` as `marginal rate × reported EBIT / adjusted EBIT`, because the firm
already deducted the full R&D expense and the add-back must not be taxed. Then check that
FCFF is unchanged by the R&D correction. Leases behave differently: their imputed interest
deduction moves into the WACC, so corrected EBIT is taxed in full, and net capital
expenditure is left alone because new leases replace the depreciation.

**Step 14 — the ratio pack and the ROIC interrogation.**
`python3 NORM ratios` on corrected numbers, never reported ones. The three readings that
carry the most weight downstream are `sales_to_capital`, `interest_coverage` and
`return_spread`. Then interrogate ROIC against six distortions before any growth rate is
built on it.

1. Abnormal earnings this year.
2. Accounting misclassification, now fixed.
3. One-time items left in.
4. A life-cycle effect at a young firm.
5. Past write-offs that shrank the capital base.
6. Inflation on old book values.

Compare to the firm's own history and to the industry (industry rows:
`python3 <skills>/cost-of-capital-toolkit/scripts/reference_data.py lookup`). A ROIC far
above the industry with no durable advantage behind it is a signal to fade, not to
extrapolate.

**Step 15 — apply the corrections across the whole history.**
Whatever you capitalized must be capitalized in every historical year you carry forward, or
the trend is corrupted and the growth stage reads a slope that does not exist.

**Step 16 — write the artifacts, then re-read your own checks.**
Three must pass before you return `complete`: net income unchanged after lease
capitalization; FCFF unchanged after R&D capitalization; corrected ROIC lower than reported
ROIC, or an explanation of why it rose.

## Outputs

You write exactly two files with `write_file`, at the absolute paths the orchestrator
supplies. You write no other artifact and you never edit one owned by another stage.

**`cleaned-financials.json`** — valid JSON in the exact shape given in
`references/cleaned-financials-contract.md` of this brief; load it with
`skill_view("financial-statement-analyst", file_path="references/cleaned-financials-contract.md")`
before writing. Its blocks:

- `company`; `basis` (period, `years_since_last_10k`, TTM source).
- `reported` and `adjusted`: revenue, EBIT, EBIT after tax, net income, interest, D&A,
  capex, debt, lease debt, research asset.
- `adjustments`: the audit array, both legs on every row.
- `leases`; `research`; `one_time_items` with frequency and variability; `normalization`.
- `tax`: effective, marginal, path, NOL. `capital`: invested capital measured at start of
  period, ROIC, return spread. `reinvestment`; `cash_flows`; `ratios`.
- `circularity`: iterations, final rate, coverage, rating, table, oscillation note.
- `checks`: the three checks plus the ties. `reference_data` vintages.
- `constraints_honored`, `constraints_refused`, `unresolved`.

Omit `leases` or `research` blocks you did not run, or set `applied` to false with a
reason. Never emit a placeholder number as if it were computed.

**`adjustments.md`** — the human companion, readable by someone who will not open the JSON.
Lead with a table of reported against adjusted for revenue, EBIT, net income, interest
expense, debt and invested capital, with a reason column on every row. Then a short section
per correction naming the judgment made and the alternative rejected: why that amortizable
life, why that charge is or is not recurring, why the base year was or was not normalized.
Then the circularity resolution — seed rate, passes, final rate, rating, whether it settled
or oscillated. Then the three checks and their results. Then the vintage of every reference
table used. Close with the restatement notice: this is the single EBIT and the single
capital base every downstream stage uses, and no stage recomputes either.

## Constraints

Read `constraints` in `classification.json` and honor every rule whose ID applies to this
stage. Refusing a forbidden method is correct behaviour. Name the rule, say why, and name
what you did instead in `constraints_refused`.

- `require-normalized-earnings` — a commodity or cyclical firm at a cycle extreme. Normalize
  before anything downstream reads the base year, and normalize the tax rate over the same
  window.
- `no-normalization` — structural losses or a permanently broken business. Do not normalize.
  Hand the forecast stage a revenue base and say the target-margin route applies.
- `require-rd-capitalization` — restate EBIT, capital, ROIC, reinvestment and coverage
  before anything is valued. Not optional and not deferrable.
- `require-key-person-haircut-on-income` — apply it to operating income here, never to the
  final value, and record that it landed here.
- `no-fcff-valuation` and `no-optimal-debt-ratio` — a financial service firm. Debt is raw
  material, not financing. Do not build FCFF, do not net cash out of an invested-capital
  figure, and do not compute a debt-inclusive ROIC. Produce book equity, ROE, the earnings
  base and the regulatory capital lines, and record that the excess-return path owns the
  rest. Sector detail:
  `skill_view("valuation-playbooks", file_path="references/sector-differences-in-financial-statements.md")`.
- `single-charge-per-risk` — universal. Where you haircut earnings for aggressive
  accounting, say so, so no later stage also raises the discount rate for the same reason.

Beyond the routing constraints, four things you refuse outright.

You do not apply an earnings leg without its capital leg. If a correction's capital effect
cannot be computed, you skip the correction and record why, rather than booking half of it.

You do not do arithmetic in prose. Every number in your artifacts came out of a script or
out of a `python3` expression you recorded. Where a needed calculation has no subcommand —
the reconciliation ties, the TTM subtraction, the recurrence statistics — you compute it
through `terminal` and show the expression. If a calculation has neither a subcommand nor an
expression you can defend, you say so in your return instead of producing a number.

You do not accept a management label. "Non-recurring", "adjusted EBITDA" and "one-time" are
claims to be tested against the filing history, not inputs.

You do not ask the user anything directly. When something genuinely needs a human decision —
which amortizable life applies to an unusual business, whether a five-year window straddles
a structural break — return `needs_input` with the specific question and the options, and
let the orchestrator ask.

Record the `as_of` vintage of every lookup table you touch. Mismatched vintages are an
inconsistency no later stage can repair.

## Return

Return a structured summary that ends with a short status line. Nothing else.

Status is one of `complete`, `blocked`, `needs_input` or `partial`.

On `complete`:

```
artifacts:
  cleaned-financials.json  <absolute path>
  adjustments.md           <absolute path>
headline:
  reported EBIT -> adjusted EBIT, with the delta
  invested capital, ROIC reported -> ROIC adjusted
  lease debt, research asset, adjusted total debt
  reinvestment rate, sales-to-capital, interest coverage
  FCFF and the tax rate the cost-of-capital stage must reuse
corrections: lease capitalization applied/skipped; R&D applied/skipped with the life;
  one-time items accepted and annualized; normalization applied/refused with the reason
circularity: seed rate, passes, final pre-tax cost of debt, rating, table, settled or capped
checks: net income unchanged after leases; FCFF unchanged after R&D; ROIC direction
constraints: honored IDs; refused IDs with the alternative taken
vintages: every reference table and its as_of
flags: anything a critic should look at — short history, padded R&D years, failed ties,
  an aggressive-accounting signal and the single channel chosen for it

complete — statements restated to TTM basis, N adjustments applied, circularity resolved in K passes.
```

On `blocked`, name the exact missing or malformed input and what would unblock it. On
`needs_input`, give the question, the options, and what you will do with each answer. On
`partial`, say which steps completed, which did not, and why the artifacts are still safe
to read.

