{"root":"stock-diligence","files":{"README.md":{"l":"md","n":20,"s":[[1,"# stock-diligence plugin\n\nInstitutional stock diligence for Claude Cowork. Install the .plugin file; the skills below appear as separate entries and can be called on their own.\n\n| Skill | What it produces | Typical request |\n|---|---|---|\n| stock-diligence-report | The full pack: the report (standard about 50 pages, or extensive with no page limit), the DCF workbook, the assumptions memo, filings pack, earnings digest, industry primer, tear sheet, manifest | \"Full diligence on SPOT\" / \"extensive full report on SPOT\" / \"express diligence on SPOT\" |\n| dcf-only | The linked three-statement DCF workbook (four scenarios, both terminal methods, sensitivities, checks), the assumptions memo, the comps tab | \"Build a Street DCF for SPOT with the assumptions memo\" |\n| event-attribution | Why the stock moved over five years, with sources, benchmarks, chapters, catalysts and risks | \"Why did SPOT fall in February 2026?\" |\n| technicals-check | Technical positioning against ten years of history with a verdict | \"Is SPOT stretched technically?\" |\n| comps-valuation | Peer multiples chosen for the business model, own-history statistics, peer-implied value | \"Comps for SPOT, which multiple should I use?\" |\n| insider-buyback-check | Buybacks, Form 4 ledger with plan signals, holders, short interest, capacity, interpretation | \"Are Spotify insiders selling? What about the buyback?\" |\n| business-understanding | Section 1: how the company makes money, the product ledger, margin levers, moat, insight ledger | \"How does Spotify actually make money and what is underappreciated?\" |\n| statements-analysis | Five years and eight quarters of statements with margin bridges, quality of earnings, guidance ledger | \"Margin bridge and quality of earnings for SPOT\" |\n| street-view | Consensus, targets, revisions, named brokers, theses with deciding questions | \"What does the street assume for SPOT?\" |\n| audit-panel | Five auditors (partner, banking MD, research partner, red team, QA) on an existing document | \"Audit this memo\" |\n| refresh-diligence | Update an existing workspace for a new quarter or price | \"Refresh the SPOT diligence for Q3\" |\n| internal-two-pager | Two-page write-up (overview, sector thesis, key metrics, risks, analyst) that fills both pages, from the package or from your inputs | \"Write a two-pager on MU\" |\n\nModes inside the main skill: standard (about 50 pages, cap 100), extensive (no limit), express (three analysts, QA auditor only), refresh. All skills share the same rules, references, scripts and QA gates, which live under `skills/stock-diligence-report/`."]]},"skills/stock-diligence-report/SKILL.md":{"l":"md","n":206,"s":[[1,"---\nname: stock-diligence-report\ndescription: \"Produces a full, information-dense stock diligence pack for any listed company: a 50-100 page Word report (business understanding deep enough to write an investment idea, revenue by geography and segment, five-year and eight-quarter dissection of all three statements with margin-bridge tables, quality of earnings, valuation vs its own history and vs peers with multiples chosen for the business model, technicals vs history, street consensus and the assumptions behind named broker views, five years of attributed price moves, insider transactions and buybacks, investment theses with deciding questions) plus a linked three-statement DCF workbook with sensitivities, a multi-page assumptions memo, filings pack, earnings digest, industry primer, tear sheet. Use whenever the user asks to diligence, deep-dive, research, underwrite, model, or write an idea or memo on a stock or ticker, even for one piece (a DCF, a margin analysis, why a stock dropped, comps, an insider check): every piece follows these conventions.\"\n---\n\n# Stock diligence report\n\n## What this skill produces\n\nOne request (\"full diligence on SPOT\") yields an output folder with:\n\n1. `<TICKER>_diligence_report.docx` (and `.pdf`): the master report, 50-100 pages, structured and judged by `references/report-frame.md`.\n2. `<TICKER>_DCF.xlsx`: linked three-statement DCF with four scenarios, both terminal methods, live sensitivity grids, market-implied expectations and a checks tab that reads ALL OK (`references/dcf-build-process.md`).\n3. `<TICKER>_DCF_assumptions_memo.docx`: multi-page prose memo, one subsection per projection lever (`references/assumptions-memo.md`).\n4. `filings/`: annual report, four interims, proxy, material current reports, decks, latest transcript, with `README_reading_order.md`.\n5. `<TICKER>_earnings_digest.docx`: last 4-8 calls, one page each.\n6. `<TICKER>_industry_primer.docx`: how money flows in the industry.\n7. `<TICKER>_tear_sheet.pdf`: one page.\n8. `MANIFEST.md`: files, as-of dates, sources, tools that failed, data gaps.\n\nPresent every file with `present_files` (or SendUserFile in Cowork). A file that is written but not presented is unreachable.\n\n## Non-negotiable rules (paste these into every subagent prompt)\n\n- Never invent a number. Every figure traces to a filing, deck, transcript, data tool or reputable third party, with basis (as reported, standardised, derived, estimated), source and as-of date beside it. Unknown → \"n/d\" plus an Appendix F entry. Recalled → \"[verify]\" plus an Appendix F entry.\n- Data order: SEC EDGAR and company IR (filings, decks, releases, transcripts) → yfinance in the sandbox (`scripts/data_pull.py`) → reputable secondary (StockAnalysis, Trading Economics, trade press). The LSEG connector is not used: the account carries no data licence (probed September 2026), so do not spend calls on it (`references/data-sources.md`).\n- Standardised figures in analysis tables, as-reported in the appendix; state which, and reconcile differences above 2%.\n- Theses from VIC, Substack or letters are summarised and attributed, never reproduced; VIC is reachable only through Claude in Chrome with the user signed in; never enter credentials. Quotes under 15 words, one per source.\n- Writing: numbers over adjectives, one claim per sentence with its figure, explain every organisation and product on first mention, no em dashes, no \"Label: content\" constructions, no stacked aphorisms. Standards and examples in `references/analyst-craft.md`.\n- Before creating files read `/mnt/skills/public/docx/SKILL.md` (report, memo, digest, primer) and `/mnt/skills/public/xlsx/SKILL.md` (workbook).\n\n## The lens that runs through every section\n\nRead the expectations embedded in the price first (market-implied growth and exit multiple, reverse-DCF grid, peer-implied value), then judge which evidence would revise them. Every narrative claim must reconcile to a number and every forecast line to a mechanism. Details in `references/analyst-craft.md`; it is short, read it before Phase 3.\n\n## Orchestration: use subagents\n\nThis skill runs in Claude Cowork, where subagents are available. Use them; do not run the research and audit phases sequentially in one context. The plan, the role prompts and the shared `work/` layout are in `references/cowork-orchestration.md`; read it before Phase 1. The division of labour is fixed:\n\n- The orchestrator (you) does Phase 0, Phase 1 data acquisition, Phase 2 scripts, the merge, the content spec, the render and the manifest. Nothing else.\n- Phase 3 research runs as five subagents in parallel: business analyst, events analyst, street analyst, statements analyst, valuation analyst. Each gets the non-negotiable rules verbatim, only the reference files it needs, only its input paths, and writes one file under `work/research/` (the valuation analyst also produces the workbook, memo and comps under `work/model/`).\n- Phase 6 audit runs once: five subagents in parallel after the first render (the QA auditor plus investment partner, banking managing director, equity research partner, red team), each returning at most ten findings ranked by importance. Each auditor reads only the sections its lens needs (partner: 0, 7, 12; banking MD: 4, 11; research partner: 5, 6, 9; red team: the whole document; QA auditor: the checklist against the whole document). Apply the findings, re-render once, and stop. A second round is allowed only if the first round found a FAIL-class error (a wrong number, a broken identity, a missing section); never a third.\n- Subagents return a file path and a five-line summary; the orchestrator merges from the files, never from chat text, and resolves conflicts by source rank (filing beats deck beats standardised feed).\n\nOnly if subagents are unavailable on the surface in use, fall back to the single-agent order at the end of `references/cowork-orchestration.md` and say so in MANIFEST.md.\n\n## Frames: what is fixed, what is generated, what is cached\n\nThree kinds of frame keep repeated work out of every run:\n\n- Code frames (scripts): `data_pull.py` (all market and filing-adjacent data), `event_scan.py`, `technicals.py`, `margin_bridge.py`, `flow_diagram.py`, `build_exhibits.py` (every numeric exhibit and chart in the report, from the data folder, with no model tokens), `dcf_validate.py` (recalc, checks, scenarios, toggles, snapshot in one command), `assemble_report.py` (merges exhibits and research blocks in the fixed order), `report_renderer.js`. The model never rewrites what these do.\n- Content frames: `report_skeleton.json` fixes the section order and where each exhibit sits; subagents write research as JSON blocks under fixed section keys (schema in `references/cowork-orchestration.md`); the role prompts are fixed text with brackets.\n- State frames: `work/` is the company's persistent workspace. Keep it with the deliverables. A rerun (next quarter, new price) is a refresh: rerun `data_pull.py`, the scripts and `build_exhibits.py`; ask the subagents to update only the section keys affected by new data (the latest quarter, events since the last run, consensus, targets, insiders); everything else is reused. A refresh should take a fraction of a first build.\n\n## Workflow\n\nCopy this checklist into the working notes and check items off:\n\n```\nBuild progress\n- [ ] Phase 0: scope confirmed, defaults recorded in manifest notes\n- [ ] Phase 1: data acquired to work/data, work/filings, work/transcripts; gaps recorded\n- [ ] Phase 2: event_scan.py, technicals.py, margin_bridge.py run; outputs in work/\n- [ ] Phase 3: five research subagents spawned in parallel; research files written (business, events, street, statements, valuation)\n- [ ] Phase 4: DCF workbook validated (zero errors, ALL OK, grid centre = price); memo rendered; comps done\n- [ ] Phase 5: content spec built; report rendered; landscape pages verified; contact sheets inspected\n- [ ] Phase 6: MANIFEST.md; audit round 1 (five subagents); fixes; scoped round 2 if triggered; round 3 only after a FAIL; all files presented\n```\n\n### Phase 0: scope\n\nConfirm ticker, listing, reporting currency, fiscal year end, latest filed period, and the EDGAR CIK (or the IR site for non-SEC filers). Defaults when unspecified: 5 fiscal years in the report body and 10 in the workbook; 8 quarters; 5-year event window; 10-year valuation and technical history (5-year fallback); 4-6 peers; charts embedded as images. Do not ask clarifying questions the defaults can answer; record the assumption in MANIFEST.md.\n\n### Phase 1: data acquisition\n"]]},"skills/stock-diligence-report/report_skeleton.json":{"l":"json","n":22,"s":[[1,"[\n {\"h1\": \"0. Tear sheet\", \"items\": [\"exhibit:tear_market\", \"exhibit:tear_capital\", \"research:0.capital\", \"exhibit:tear_valuation\", \"exhibit:tear_street\", \"exhibit:scenario_ladder\", \"research:0\"]},\n {\"h1\": \"1. What the company does\", \"items\": [\"research:1\"]},\n {\"h1\": \"2. Revenue by geography and segment\", \"items\": [\"research:2\"]},\n {\"h1\": \"3. Stock performance, top to bottom\", \"items\": [\"exhibit:price_charts\", \"exhibit:returns_short\", \"exhibit:returns_long\", \"exhibit:drawdowns\", \"exhibit:risk_stats\", \"research:3\"]},\n {\"h1\": \"4. Three statements, annual\", \"items\": [\"exhibit:is_annual\", \"exhibit:is_common_size\", \"exhibit:is_growth\", \"exhibit:bridge_rev\", \"exhibit:bridge_gp\", \"exhibit:bridge_oi\", \"exhibit:waterfall\", \"research:4.bridges\", \"exhibit:bs_annual\", \"exhibit:bs_ratios\", \"exhibit:cf_annual\", \"exhibit:cash_conversion\", \"exhibit:quality_checks\", \"research:4\"]},\n {\"h1\": \"5. Quarterly results\", \"items\": [\"exhibit:quarterly_is\", \"exhibit:quarterly_growth\", \"research:5\"]},\n {\"h1\": \"6. Management commentary and guidance\", \"items\": [\"research:6\"]},\n {\"h1\": \"7. Valuation\", \"items\": [\"exhibit:tear_valuation\", \"research:7.history\", \"exhibit:peers_multiples\", \"exhibit:peers_profile\", \"research:7.peers\", \"exhibit:model_summary\", \"exhibit:scenario_ladder\", \"exhibit:dcf_toggles\", \"research:7\"]},\n {\"h1\": \"8. Technical positioning against history\", \"items\": [\"exhibit:technicals\", \"exhibit:technical_charts\", \"research:8\"]},\n {\"h1\": \"9. Street consensus and what the street assumes\", \"items\": [\"exhibit:consensus\", \"research:9\"]},\n {\"h1\": \"10. Key events, catalysts and risks\", \"items\": [\"exhibit:events_chart\", \"exhibit:events_table\", \"research:10\", \"exhibit:events_runs\", \"research:10.chapters\"]},\n {\"h1\": \"11. Insider activity and share buybacks\", \"items\": [\"research:11.buybacks\", \"exhibit:insiders\", \"exhibit:holders\", \"research:11\"]},\n {\"h1\": \"12. Investment theses and the deciding questions\", \"items\": [\"research:12\"]},\n {\"h1\": \"Appendix A. Statements as reported\", \"items\": [\"research:A\"]},\n {\"h1\": \"Appendix B. Margin bridges\", \"items\": [\"research:B\"]},\n {\"h1\": \"Appendix C. Glossary\", \"items\": [\"research:C\"]},\n {\"h1\": \"Appendix D. Methodology\", \"items\": [\"research:D\"]},\n {\"h1\": \"Appendix E. Sources\", \"items\": [\"research:E\"]},\n {\"h1\": \"Appendix F. Data gaps and tools that failed\", \"items\": [\"research:F\"]},\n {\"h1\": \"Appendix G. Estimation methods\", \"items\": [\"research:G\"]}\n]"]]},"skills/stock-diligence-report/references/report-frame.md":{"l":"md","n":240,"s":[[1,"# Report frame: the section-by-section playbook\n\n## Contents\n- How to use this file\n- Global standards (density, evidence, the expectations lens, writing, formatting)\n- Section 0: Cover, tear sheet, executive summary\n- Section 1: What the company does (pointer plus the writing standard)\n- Section 2: Revenue by geography and segment\n- Section 3: Stock performance\n- Section 4: Annual statements, margin bridges, quality of earnings\n- Section 5: Quarterly results\n- Section 6: Management commentary and guidance\n- Section 7: Valuation (own history, peers, DCF summary, football field)\n- Section 8: Technical positioning\n- Section 9: Street consensus and what the street assumes\n- Section 10: Key events, catalysts and risks\n- Section 11: Insider activity and buybacks\n- Section 12: Investment theses and the deciding questions\n- Appendices\n- Done-when checklist for the whole report\n\n## How to use this file\n\nRead the global standards once, then the section you are about to write. Each section gives: the purpose (what the reader must be able to do afterwards), the inputs (files under `work/`), the steps in order, the exact tables and charts, the analytical questions the prose must answer, the writing standard with a weak and a good example, and the done-when checks. Build each section from the content builder (`scripts/example_spot_report/`) so that every number in the prose is computed from the same data as the tables.\n\nThe reference instance is the SPOT report from the September 2026 test runs. Where this file says \"SPOT: ...\", that is the calibration example, not a template to copy.\n\n## Global standards\n\n**Length modes.** Two modes, chosen by the request:\n\n- Standard (default): about 50 pages, acceptable 30-70, hard cap 100 (`scripts/qa_gates.py` fails the build above 100). Per-section budgets in pages: 0 tear sheet 3; 1 business 9; 2 geography 2; 3 performance 3; 4 statements 8; 5 quarterly 3; 6 guidance 2; 7 valuation 6; 8 technicals 2; 9 street 4; 10 events 5; 11 insiders 3; 12 theses 4; appendices A 6, B-F one each, G 3. Compression rules per section are given below under \"Standard-mode cut\"; substance is preserved by moving detail to the companion files (workbook, memo, filings pack, machine-readable bridges), never by dropping the analysis.\n- Extensive full report (the user says \"extensive\", \"full-length\" or \"no page limit\"): no budgets and no cap; every product gets a card, every broker a paragraph, every flagged move its narrative, every period its bridge, both statements as reported in full. The 174-page SPOT run of September 2026 is the reference for this mode. Write the mode on the cover and in the manifest.\n\n**Density.** Tables, bridges, ledgers and charts with captions, each followed by a short paragraph that says what it means. A section is finished when a reader could reconstruct the analysis from the tables and when the \"so what\" is stated in plain words. A section that restates the data it shows is not finished. Never pad; stop when the section's questions are answered.\n\n**Evidence.** Every number carries its basis (as reported, standardised, derived, estimated), its source and its as-of date in the table note. Standardised providers reclassify: S&P showed SPOT's FY2023 operating income as −199m against −446m as reported, so state the basis on every table and reconcile when the two differ by more than 2%. A figure that cannot be sourced is \"n/d\" and goes to Appendix F; an estimate is labelled with its method. Recalled figures may be used only with a \"[verify]\" flag and an Appendix F entry.\n\n**The expectations lens.** The report exists to answer one question: what does the price already assume, and what evidence would revise those expectations up or down. Every analytical section ends by connecting to that question (the approach associated with Mauboussin and Rappaport's expectations investing: read the expectations implied by the price first, then judge the likelihood of revisions). Narratives must reconcile to numbers and numbers to narratives (Damodaran's discipline): a story about pricing power must show up as ARPU and gross margin; a margin forecast must have a product-level mechanism behind it.\n\n**Writing.** Prose, not compressed labels: a thesis or an insight is three short paragraphs (the argument with its evidence; what must be true and how it shows in the model; the deciding question and what refutes it), never a string of \"Claim: ... Must be true: ... Deciding question: ...\". Numbers over adjectives. One claim per sentence, with the figure that supports it. Explain every organisation, product and term as if the reader has never heard of it. No em dashes. No \"Label: content\" sentence constructions. No stacked aphorisms. Plain verbs. Weak: \"Spotify has strong pricing power.\" Good: \"Spotify raised the US Individual price to $12.99 in February 2026, its second increase in two years, and reported Premium ARPU up 7% at constant currency the following quarter with no unexpected churn.\"\n\n**Formatting and production.** Follow `presentation.md` (portrait only; tables of at most six columns with numbers right and text left; key-value panels and cards instead of wide grids; quarters as rows; one message per chart; statements as filed as page images in the appendix). Build through `scripts/report_renderer.js` from a JSON spec produced by a content builder in the pattern of `scripts/example_spot_report/report_v2a.py` and `report_v2b.py`.\n\n## Section 0: Cover, tear sheet, executive summary (2-3 pages)\n\n**Purpose.** A portfolio manager reads only this and knows the price-implied expectations, the debate, and what would change the view.\n\n**Inputs.** `work/data/info.json`, `work/data/estimates.json`, the workbook snapshot (`scen.json`), `work/data/risk_stats.json`, `work/data/short_interest.json`.\n"]]},"skills/stock-diligence-report/references/analyst-craft.md":{"l":"md","n":113,"s":[[1,"# Analyst craft: the standards behind a top-tier diligence report\n\n## Contents\n- Where these standards come from\n- The expectations lens (read the price first)\n- Narrative and numbers (make the story reconcile)\n- Measuring the moat (a checklist that produces evidence, not adjectives)\n- Quality of earnings and forensic red flags (formulas and thresholds)\n- How sell-side initiations are organised, and what to borrow\n- How buy-side write-ups are judged (the VIC standard)\n- Unit economics templates by business model\n- Writing rules with examples\n- Self-audit questions before a section is called finished\n\n## Where these standards come from\n\nThe frameworks below are the ones professional analysts actually use. They are summarised, not reproduced: expectations investing (Mauboussin and Rappaport), narrative-and-numbers valuation discipline (Damodaran), moat measurement (Mauboussin's \"Measuring the Moat\" line of work and Porter's forces), quality-of-earnings and forensic accounting checks (the Schilit and O'Glove traditions), the CFA Institute research-report ordering used by initiation reports, and the standards applied on Value Investors Club and in buy-side investment committees. Use them as lenses in every section; do not cite them as authorities in the report.\n\n## The expectations lens (read the price first)\n\n1. Start from the price, not from a forecast. Solve for the revenue growth, terminal margin and horizon that the current price implies using the reverse-DCF grid and the market-implied growth cell in the workbook. State the result in the executive summary (SPOT: $543 implies about 12% revenue growth for ten years with a 23-24% terminal operating margin, or 3.6% perpetual growth on the Street cash flows).\n2. Identify the value triggers: the operating levers that would revise those expectations (sales growth, operating margin, investment needs, cost of capital) and, for each, the KPI that reveals a revision and the date it is reported.\n3. Judge the probability and size of revisions rather than the \"fairness\" of the price: which direction is the evidence pointing, and how large would the revision be in value terms (use the sensitivity grids to translate).\n4. Treat consensus as the market's expectations only when the price agrees with it; when the DCF Street case and the price differ, the gap is the first thing to explain.\n\n## Narrative and numbers (make the story reconcile)\n\nEvery narrative claim in Section 1 has to appear as a number somewhere in Sections 4, 5 or 7, and every forecast line in the DCF has to have a narrative mechanism in Section 1. Run the reconciliation both ways before finishing: pricing power → ARPU growth and gross margin path; marketplace lever → gross margin above the royalty-implied level; saturation → net adds by region; reinvestment → opex ratios and capex. A number without a story is a plug; a story without a number is marketing.\n\n## Measuring the moat (evidence, not adjectives)\n\nWork through the checklist and write only what the evidence supports:\n\n- Industry structure: concentration on both sides (suppliers and buyers), entry barriers, substitutes, rivalry; who captures the industry's profit pool and how that has shifted over five years.\n- Sources of advantage, each with the evidence: scale economies (unit cost falling with volume; per-user cost trend), network effects (does value to users rise with users; two-sided effects), switching costs (churn evidence after price increases; retention of cohorts), intangible assets (brand pricing premium, data, licences), cost advantages (input contracts, distribution), regulatory position.\n- Sustainability: how long the advantage has persisted (ROIC above cost of capital for how many years), what would erode it, and whether reinvestment is required to keep it.\n- Bargaining power, quantified: supplier concentration (top three suppliers' share of cost of revenue), contract cycle length, take-rate or royalty trend, customer concentration.\n- Management's capital allocation record: returns on past investments, acquisitions written off, buyback timing versus price.\n\nOutput: a moat table with source of advantage, evidence, trend, and the model line it protects. Anything that cannot be evidenced is listed as \"asserted, not evidenced\".\n\n## Quality of earnings and forensic red flags\n\nCompute each with the formula stated; report the value and a one-line reading; flag when the threshold is breached for two consecutive years.\n\n| Check | Formula | Flag when |\n|---|---|---|\n| Cash conversion | CFO / net income | < 1.0 two years running |\n| Accruals ratio | (net income − CFO) / total assets | > 5% or rising |\n| Receivables vs revenue | growth in receivables − growth in revenue | > 10 pts |\n| Inventory vs revenue | growth in inventory − growth in revenue | > 10 pts (where relevant) |\n| Deferred revenue vs revenue | growth in deferred revenue − growth in subscription revenue | < −10 pts for subscription models |\n| Capex vs D&A | capex / D&A | < 0.7 (under-investment) or > 2 without growth |\n| Capitalised costs | capitalised development or content / opex | rising share |\n| Tax rate | effective vs statutory | gap > 10 pts without explanation |\n| One-offs | recurring \"non-recurring\" items | present in 3 of 5 years |\n| Goodwill | goodwill / total assets | > 30% |\n| Related parties | note disclosures | any material item |\n| Share count | diluted shares change vs buybacks | dilution despite buybacks |\n| Guidance pattern | beats or misses by mechanism | systematic miss without a stated mechanism |\n| Interest coverage | EBIT / interest expense | < 3x |\n| Leverage | net debt / EBITDA | > 3x, or rising into a downturn |\n\nCompany-specific mechanisms belong here too: for SPOT, share-price-linked social charges inside opex, fair-value swings on the Exchangeable Notes inside finance income, and deferred-tax-asset recognition that took the FY2025 tax rate to near zero.\n\n## How sell-side initiations are organised, and what to borrow\n"]]},"skills/stock-diligence-report/references/technicals.md":{"l":"md","n":14,"s":[[1,"# Technical positioning versus history\n\n`python3 scripts/technicals.py TICKER --years 10 --out work/technicals` produces technicals.csv, bollinger.png and momentum.png.\n\nMetrics and what each says:\n- 1/3/6/12-month returns with z-scores and percentiles against the stock's own rolling distribution: how unusual the recent move is for this stock.\n- Bollinger (20,2) daily %B and bandwidth, and weekly %B: where price sits inside its recent range and whether the range is compressed (bandwidth low often precedes a large move).\n- Distance to 50 and 200-day moving averages, z-scored; recent golden and death crosses.\n- RSI(14): momentum exhaustion; above 70 or below 30 is stretched for most names, but compare to the stock's own history.\n- 12-1 momentum: the academic momentum factor; negative means the stock is in the losers' bucket.\n- Realised volatility 30 and 90 days versus history; compare with implied volatility from the vol surface when entitled.\n- Monthly MACD (12,26,9) regime: the same regime logic as the screener dashboard.\n\nVerdict paragraph: state where the stock sits versus its own history in three or four sentences (stretched, neutral or washed out on each horizon), name the one or two metrics that disagree, and relate the picture to the event chapters. No trading recommendations."]]},"skills/stock-diligence-report/references/data-sources.md":{"l":"md","n":39,"s":[[1,"# Data sources, tool map and fallbacks\n\n## Order of preference\n\n1. SEC EDGAR and company IR: 10-K/10-Q/8-K, 20-F/6-K for foreign private issuers, proxy statements, Forms 4 and 144, 13F; XBRL for as-reported figures; decks, releases, transcripts from the IR site. This is the primary source.\n2. Company IR sites for decks, transcripts and press releases where EDGAR lacks them (non-SEC filers).\n3. yfinance in the sandbox (`scripts/data_pull.py`): prices, statements (4-5 years annual, 4-5 quarters), estimates (FY1/FY2 revenue and EPS, low/high), price targets (low/mean/median/high), recommendation counts, peer info, `earnings_dates` (dates with EPS estimate, actual and surprise), `insider_transactions` (Form 4 rows: insider, position, date, shares, value, price range; available for many foreign private issuers), `institutional_holders` (top 10 13F with quarter-on-quarter change), `info` fields `sharesShort`, `sharesShortPriorMonth`, `shortPercentOfFloat`, `shortRatio`, `heldPercentInstitutions`, `heldPercentInsiders`. Known defects: `freeCashflow` is unreliable (SPOT showed $1.5bn vs €3.3bn company-defined); EV for ADRs can be wrong (TME); trailing P/E distorted by one-offs.\n4. Reputable secondary: StockAnalysis (S&P Global standardised annual statements with the as-reported toggle; its quarterly views are JavaScript-rendered and a fetch returns annual data only), Macrotrends, Trading Economics (government yields), trade press (for music: Music Business Worldwide, Digital Music News, Variety) for broker targets, stated theses and results summaries with guidance-versus-actual detail.\n\n## Specific lookups\n\n- Risk-free rate in the model currency: 10-year government yield on the valuation date (Bund for EUR, UST for USD).\n- Beta: yfinance `info['beta']` (5Y monthly vs S&P 500) cross-checked against a bottom-up peer beta.\n- Options, RSUs, share counts, lease IBR, contingencies, minimum guarantees: notes to the latest interim filing.\n- Segment KPIs: the quarterly shareholder deck or the equivalent KPI supplement.\n- Long-term targets: investor day materials.\n\n## LSEG connector\n\nNot used. Probed on 5 September 2026 after the approval prompts were accepted: every family (search and pricing, QA fundamentals and IBES, news, transcripts, analytics) returned an entitlement error, so the account carries no data licence. Do not spend calls on LSEG tools; if the licence changes, the data chain can be revisited.\n\n## Sources that block automation\n\nValue Investors Club returns ROBOTS_DISALLOWED to fetch tools. Read it only through Claude in Chrome with the user signed in; summarise and attribute; never enter credentials. The same applies to any paywalled research.\n\n## What each report section needs, and the first place to look\n\n| Section | Data | Source |\n|---|---|---|\n| 0, 3 | prices, returns, risk stats | yfinance 10Y |\n| 1 | business mechanics, product economics | annual report, first prospectus, product docs, decks, trade press |\n| 2 | geography, segments, FX | annual report notes, decks (user mix), releases (constant currency), yfinance FX |\n| 4 | statements 5Y | annual reports (as reported), StockAnalysis standardised, yfinance |\n| 5 | statements 8Q, KPIs | decks and releases, yfinance (5Q) |\n| 6 | guidance, transcripts | IR site, Motley Fool, Investing.com, trade press |\n| 7 | consensus, multiples history, peers | yfinance estimates and info, StockAnalysis ratios, Trading Economics for the risk-free rate |\n| 9 | targets, revisions, broker views | coverage compiled with dates (quartiles n/a) |\n| 10 | events | yfinance earnings_dates, filings, web search by date |\n| 11 | buybacks, insiders, holders, short interest | 6-K/10-Q notes, yfinance insider_transactions, institutional_holders, info |"]]},"skills/stock-diligence-report/references/business-understanding.md":{"l":"md","n":85,"s":[[1,"# Section 1: comprehensive business understanding\n\nPurpose: after reading this section the reader can write a Value Investors Club-quality idea without further research. That means product-level and P&L-line-level knowledge, not a description. Test every paragraph against the two example claims that motivated this spec: \"marketplace monetisation is the most underappreciated driver of incremental margin expansion\" and \"the artist-facing tools monetise discovery and promotion inside the ecosystem.\" Neither can be written from a 10-K summary; both fall out of the product-and-monetisation ledger below.\n\n## 1A. Industry primer (2-3 pages)\n\n- How money flows: who pays whom, in what order, at what take rates. Draw it as a flow diagram (matplotlib boxes and arrows) and explain each arrow.\n- Industry size, growth, and share by player; the last five years of the industry's own statistics (IFPI, MIDiA, Nielsen, IDC, Gartner, trade bodies, whatever fits).\n- Structural features: contract cycles, regulated rates, exclusivity, network effects, switching costs, platform dependence (app stores, cloud), regulation and litigation that set prices.\n- Where the pressure is now (technology shifts, new entrants, substitutes) and who benefits.\n- Sources: the company's first prospectus (S-1/F-1 explains mechanics better than later filings), 10-K Item 1, industry bodies, competitor filings.\n\n## 1B. Company history as strategic eras (1-2 pages)\n\nA timeline of eras, each with what the strategy was, what it cost, what it returned, and what management learned. Tie era boundaries to the event chapters in Section 10.\n\n## 1C. Product and monetisation ledger (3-5 pages, the core)\n\nOne row per product, plan, feature or lever. Columns:\n\n| Product / lever | What it is | Who pays | P&L line it hits | Disclosed scale or estimate | Growth | Gross-margin effect | Underappreciated? |\n\nRules:\n- \"P&L line it hits\" must say revenue, cost-of-revenue offset, or opex, because that determines the margin effect. A lever paid by suppliers or partners that offsets cost of revenue lifts gross margin far more than its size suggests; a lever that adds revenue at pass-through cost does not.\n- Include pricing ladders by market and plan, bundles, add-ons, tiers, partner programs, promotional tools, advertising products, transaction or take-rate products, ticketing, merchandise, and marketing assets that behave like products (Wrapped-style campaigns).\n- Estimates are labelled as estimates with the method (for example \"ad revenue ÷ ad users\").\n- Close with the explicit \"underappreciated\" list: the levers whose margin contribution exceeds their revenue contribution, with the arithmetic.\n\n## 1D. Unit economics (1-2 pages)\n\nARPU by segment and region; cost of revenue per user; contribution per user; churn evidence (disclosed or inferred from net adds vs gross adds, price-increase commentary); conversion funnel from free to paid where relevant; mix effects on ARPU and margin; lifetime value where the inputs exist.\n\n## 1E. Cost structure decomposition (1-2 pages)\n\nCost of revenue split (royalties or COGS components, payment processing, hosting, content amortisation); opex split with SBC shown separately; company-specific quirks (payroll taxes tied to the share price, capitalised content, minimum guarantees, revenue-share floors). Feed this directly into Section 4's margin bridges.\n\n## 1F. Competitive position and moat (1-2 pages)\n\nCompetitors and their relative economics; evidence of pricing power (increases taken, churn observed); switching costs; bargaining power versus suppliers and distributors (contract cycles, concentration); where the company is structurally weak. Produce the moat table from `analyst-craft.md`:\n\n| Source of advantage | Evidence (number, source) | Trend (5Y) | Model line it protects | Evidenced or asserted |\n\nAnything without evidence is written as \"asserted, not evidenced\". Quantify bargaining power: suppliers' share of cost of revenue, contract cycle length, take-rate or royalty trend, customer concentration.\n\n## 1G. Management, ownership, governance (1 page)\n\nFounders and executives, tenure, incentives, share classes and voting control (dual-class, beneficiary certificates), board composition, related-party items, insider ownership.\n\n## 1H. KPIs (1 page)\n\nWhat the company discloses, what the street tracks, what it does not disclose and the proxies to use (with formulas), and the jargon glossary entries this section introduces.\n\n## 1I. Strategy and the deciding debates (1-2 pages)\n\nStated long-term targets with dates, the product pipeline, capital allocation stance, and the two or three debates that will decide the stock, each framed as a question with the KPI that answers it.\n\n## 1J. Insight ledger (inside the report)\n\nA closing table of the non-obvious facts a reader needs to write a differentiated thesis. Each row: the insight, the arithmetic or evidence, the source, and the report section that develops it. Aim for 10-20 rows. Examples of the standard from the SPOT instance: marketplace tools paid by labels at near-100% margin; payroll social charges that fall when the share price falls; label contract renewal timing versus the margin roadmap; add-on ARPU arithmetic; audiobook cost accounting; buybacks accelerating while founders sell; a supplier buying the platform's stock.\n\n## Done-when checklist for Section 1\n\n```\nSection 1 QA\n- [ ] Money-flow description names who pays whom and the take rates\n- [ ] Every product or lever is a ledger row with who pays and which P&L line it hits\n- [ ] Every \"underappreciated\" claim has its arithmetic and says why it is underappreciated\n- [ ] Unit economics table derives ARPU, cost per user and gross profit per user for 3 years\n- [ ] Cost structure names the company-specific mechanisms that move margins\n- [ ] Moat table has evidence per row; unevidenced rows are labelled\n- [ ] Control mechanism (share classes, certificates) stated"]]},"skills/stock-diligence-report/references/comps-multiples.md":{"l":"md","n":27,"s":[[1,"# Valuation versus history and versus peers\n\n## Multiples versus the company's own history\n\nFor each of trailing and forward P/E, EV/EBIT, EV/EBITDA, EV/Sales, P/FCF, PEG, FCF yield, dividend yield: build the daily or monthly series over 10 years (5 as fallback) from the price series and the estimate or actual series; report current value, 5-year and 10-year mean, median, standard deviation, z-score, percentile; strip chart with the current value marked. Explain re-ratings with the event chapters, not with adjectives.\n\n## Choosing the multiples for peers\n\n- Asset-light with trivial D&A (SPOT capex 0.4% of revenue): EV/EBIT and P/FCF are the primary reads; EV/EBITDA adds nothing and can hide lease and SBC effects.\n- Capex- or content-intensive (Netflix content amortisation, telecoms, semis, steel): EV/EBIT or EV/(EBITDA − capex); plain EV/EBITDA flatters heavy reinvesters.\n- Structurally different gross margins between company and peers (royalty pass-through vs owned content, marketplace vs retailer): show EV/Gross Profit next to EV/Sales; never EV/Sales alone.\n- Heavy SBC: use IFRS/GAAP EBIT (SBC already expensed); never an adjusted EBITDA that adds SBC back.\n- Negative or early-stage earnings: EV/Sales and EV/GP with a growth adjustment (multiple divided by growth) or rule-of-40 framing.\n- Financials: P/TBV and P/E with ROTE; REITs: P/FFO and implied cap rate; cyclicals: EV/normalised EBIT and P/B at mid-cycle; insurers: P/BV with ROE.\n- Always show the primary multiple on NTM consensus; trailing only as a labelled prototype when consensus is unavailable.\n\n## Building the peer set\n\nGICS sub-industry peers, plus business-model peers from other industries, plus the suppliers and customers whose economics frame the debate. Name the fade anchor (what the company's multiple converges to if the bear case is right) and the aspiration anchor (what the bull case prices in). Four to six names.\n\n## Explaining why each peer sits where it sits\n\nUse drivers, not adjectives: revenue growth, incremental margin, ROIC and reinvestment need, FCF conversion, leverage, geography and currency, accounting differences (capitalised content, lease treatment, SBC), structural discounts (governance, dual-class, regulation, listing venue). One paragraph per peer and a table with the driver columns next to the multiples.\n\n## Peer-implied price\n\nApply the peer median of the primary multiple to the company's FY1 metric and the median forward P/E to FY2 EPS; show both on the football field. The gap between the peer-implied price and the DCF is the premium the growth and margin roadmap must justify; say that in words."]]},"skills/stock-diligence-report/references/dcf-build-process.md":{"l":"md","n":189,"s":[[1,"# DCF companion artifact — build process (skill reference)\n\nWritten from a completed, verified build for Spotify (SPOT), valuation date 4 Sep 2026. Everything below was executed, not theorised: 1,984 live formulas, zero errors, balance sheet ties in every year, four scenarios and every toggle run through LibreOffice. Version 3 adds the structural ideas taken from the STLD reference model (section 14), the multi-page assumptions memo (section 15) and the STLD presentation conventions (section 16).\n\n## 0. What \"picture perfect\" means here (acceptance criteria)\n\nThe workbook ships only when all of the following are true:\n\n1. `recalc.py` reports `total_errors: 0` (LibreOffice evaluates every formula).\n2. Checks tab reads `ALL OK` in the base scenario: balance sheet balances every projected year to < €0.5m, CF closing cash equals BS cash, CF starting line equals IS net income, equity roll-forward ties, debt schedule closing balances tie to the BS, segment revenue sums to total, WACC > g by > 2 pts, TV 40-85% of EV, implied exit multiple 8-40x, diluted share count sane, EPS inside the consensus low-high band ±10%, sensitivity grid centre equals the DCF price.\n3. Every projection row is one formula copied across all forecast columns (no lone edited cells).\n4. The centre cell of sensitivity grid 1 equals the DCF price to the cent; switching the terminal method toggle to \"exit multiple\" reproduces the centre of grid 2. If these do not agree, the grids are not the same math as the model.\n5. Each scenario evaluates without errors; bear-case CHECK flags are allowed, FAIL flags are not.\n6. No hard-coded result anywhere; every input carries a source note (filing, page/note, URL or \"assumption\").\n\n## 1. Architecture\n\nSheet order and dependency: `Cover ← Inputs → Drivers → IS / BS / CF → DCF → Sensitivity`; `Debt → IS (interest) and BS (balances)`; `WACC → DCF`; `Consensus`, `Comps` are context; `Checks` audits; `Hist` holds as-reported history.\n\nColumn alignment rule: on every projection sheet, column A = label, B = unit, C..G = five actual years, H..Q = ten forecast years. The same column letter is the same year on every sheet, so a cross-sheet reference never needs an offset and errors are visible by eye.\n\nColour and format conventions (xlsx skill standard): blue = hard-coded input, black = formula, green = link from another sheet, yellow fill = key lever or headline output. Percentages stored as fractions, negatives in parentheses, zeros as \"-\", multiples as `0.0x`, years as text headers.\n\nScenario mechanism: every operating driver has four scenario rows (Street/Consensus, Mgmt targets, Bull, Bear) plus a \"Selected\" row `=INDEX(rows, Inputs!$C$5)`. Only Selected rows feed the model, so the scenario selector is one cell.\n\nToggles that must exist: terminal method (1 Gordon / 2 exit / 3 average), mid-year convention, SBC add-back, litigation contingency in bridge, roll-forward to valuation date, Blume-adjusted beta.\n\n## 2. Data acquisition order and what to pull\n\nPriority: company filings and decks → yfinance in the sandbox → trade press. The LSEG connector is not used (no data licence on the account).\n\nPull list (with the SPOT source that satisfied each):\n\n- Five years of annual IS/BS/CF, as reported: quarterly shareholder deck appendix (annual summary FY2021-25 with segment revenue, KPIs, FCF) and the 20-F; Yahoo `income_stmt / balance_sheet / cashflow` as a fallback for line detail.\n- Latest interim balance sheet and notes: the most recent 6-K/10-Q. Needed lines: cash, short-term investments, long-term investments at fair value (TME €1,034m + other €84m), lease liabilities split (€466m) and the incremental borrowing rate (5.8%, use as pre-tax cost of debt when the company has no bonds), interest-bearing debt (Exchangeable Notes repaid €1,304m on 15 Mar 2026), shares issued and treasury (210,241,268 − 4,657,063), options outstanding and weighted-average strike (4,496,814 at $256.44), RSUs (1,456,016), contingencies (MLC €473m), minimum guarantees (€2,290m), effective and statutory tax rates (23.6% / 23.87%).\n- Operating KPIs by segment: deck (subs 290m, MAU 751m, ad MAU 476m, Premium ARPU, segment gross margins 35% / 19%).\n- Management long-term targets: Investor Day (mid-teens CAGR, GM 35-40% by 2030, op margin >20% within four years).\n- Consensus: yfinance `revenue_estimate`, `earnings_estimate`, `analyst_price_targets`, `recommendations_summary` (FY26 rev €19,538m, EPS €12.30; FY27 €22,309m, €15.51; targets $424 / $626 median / $727; 39 analysts).\n- Market data: price, EUR/USD spot, raw beta (yfinance `info`), 10Y government yield in the model currency (Bund 3.35% from Trading Economics).\n- Peers for the comps tab: yfinance `info` for EV, EV/Sales, EV/EBITDA, forward P/E, margins, growth. Yahoo's `freeCashflow` field is unreliable (SPOT showed $1.5bn vs €3.3bn company-defined TTM); compute FCF yield from company figures.\n\n## 3. Modelling decisions (rules, then the SPOT instance)\n\nModel currency: the reporting currency. Value in that currency, convert per share at spot. Discount rate must be in the same currency (EUR cash flows → Bund risk-free rate). A USD build is fine if applied consistently; never mix.\n\nRevenue build: driver-based, by business model. Subscription: average subscribers × monthly ARPU × 12 (net adds and ARPU growth are the levers). Ad-funded: average MAU × annual revenue per user. Retail: units × price or stores × sales per store. SaaS: ARR roll-forward (new, expansion, churn). Banks/insurers: do not DCF, use residual income or DDM. Always reconcile the driver build to the last actual year exactly (SPOT FY2025 ARPU €4.63 derived from €15,350m / avg 276.5m subs / 12, so the first forecast year starts from a true base).\n"]]},"skills/stock-diligence-report/references/cowork-orchestration.md":{"l":"md","n":106,"s":[[1,"# Orchestrating the build in Claude Cowork (subagents) and the single-agent fallback\n\n## Contents\n- Principles\n- The plan: waves, roles, inputs, outputs\n- Role prompts (copy, fill the brackets)\n- Handoffs and the shared `work/` layout\n- Merge, audit and render\n- Single-agent fallback order\n\n## Principles\n\nEvery subagent prompt carries a call budget and a reading scope; a subagent that reaches its budget writes what it has and lists the gaps. This skill is built for Claude Cowork and subagents are the default, not an option: Wave 2 (five analysts) and Wave 4 (five auditors) are always run as parallel subagents. Use them for the research-heavy, independent tasks; keep data acquisition, the model build and the final render in one place so that every number comes from one source of truth. Every subagent writes to a fixed path under `work/` and returns a short summary plus its file list; nothing is merged from a subagent's chat text alone. Every subagent receives the non-negotiable rules from SKILL.md verbatim (no invented numbers, basis and source on every figure, summarise never reproduce, no em dashes). Give each subagent the reference file it needs, not the whole skill.\n\n## The plan: waves, roles, inputs, outputs\n\nWave 1 (sequential, orchestrator): Phase 0 scope and Phase 1 data acquisition (filings, `data_pull.py`, transcripts, secondary sources) into `work/data/`, `work/filings/`, `work/transcripts/`; run the three scripts (events, technicals, margin bridge). Record every gap in `work/manifest_notes.md`.\n\nWave 2 (parallel subagents, each reads only its inputs):\n- Business analyst → `work/research/business.md` (Section 1 content: industry primer, eras, product ledger rows, unit economics, cost structure, moat table, governance, KPI table, debates, insight ledger). Inputs: filings, decks, transcripts, product documentation, `business-understanding.md`, `analyst-craft.md` (moat and unit-economics parts).\n- Events analyst → `work/research/events.md` (attribution for every flagged date, runs and drawdowns, chapters draft, catalyst calendar, risk factors with model lines). Inputs: `work/events/`, `work/data/earnings_dates.csv`, filings and releases by date, `event-attribution.md`.\n- Street analyst → `work/research/street.md` (consensus table, target distribution, revision trend with dated sources, named-broker table, theses grouped with must-be-true KPIs). Inputs: `work/data/estimates.json`, IBES if entitled, web coverage, `report-frame.md` sections 9 and 12.\n- Statements analyst → `work/research/statements.md` (reconciliation of standardised vs as-reported, quality-of-earnings readings, the \"what changed and why\" paragraphs per year, guidance ledger, latest-quarter verdicts). Inputs: `work/data/*.csv`, `work/bridge/`, MD&A, decks, `analyst-craft.md` (quality-of-earnings part), `report-frame.md` sections 4-6.\n- Valuation analyst → `work/model/` (adapted DCF builder, validated workbook, `scen.json`, `memo_data.json`, assumptions memo docx) and `work/research/valuation.md` (peer set, multiple choice rationale, why-it-sits-there paragraphs, own-history statistics). Inputs: `work/data/`, filings for the bridge items, `dcf-build-process.md`, `comps-multiples.md`, `assumptions-memo.md`.\n\nWave 3 (sequential, orchestrator): build the content spec from the research files and data (`scripts/example_spot_report/` pattern), render, run the QA gates, then spawn one auditor.\n\nWave 4 (balanced rounds, five subagents in parallel): the QA auditor → `work/research/audit.md` (checklist compliance, numbers, presentation) and the audit panel from `analyst-craft.md`, one subagent per role → `work/research/audit_pm.md`, `audit_ibmd.md`, `audit_er.md`, `audit_redteam.md`, each capped at ten findings ranked by importance, each finding tagged FAIL (wrong number, broken identity, missing section) or IMPROVE. The orchestrator fixes every FAIL and the IMPROVE items it can do in one pass and re-renders. Round 2 runs when round 1 produced any FAIL or more than five applied fixes, scoped to the changed sections, with the QA auditor and the red team plus any lens whose section changed. Round 3 only if round 2 finds a FAIL. Hard stop at three; the remainder goes into MANIFEST.md under \"open items\" and general findings under \"skill changes to make\".\n\n## Role prompts (copy, fill the brackets)\n\n**Business analyst**\n\"Budget: 25 fetches or searches; read only `references/business-understanding.md` and the moat and unit-economics parts of `references/analyst-craft.md`. You are producing Section 1 of a diligence report on [company, ticker]. Read `references/business-understanding.md` and the moat and unit-economics parts of `references/analyst-craft.md`. Inputs are in `work/filings/`, `work/transcripts/`, `work/data/`. Produce `work/research/business.md` with: 1A industry primer with the money-flow description; 1B eras table; 1C the product and monetisation ledger as a table with the eight columns; 1D unit economics table with derivations; 1E cost structure with company-specific mechanisms; 1F moat table with evidence and the model line each protects; 1G management and control; 1H KPIs disclosed and not with proxies; 1I strategy and the three deciding debates; 1J insight ledger with 10-20 rows, each with arithmetic or evidence and a source. Rules: [paste the non-negotiable rules]. Every figure has a source and as-of date; estimates are labelled with method; nothing recalled without a [verify] flag. Return the file path and a five-line summary.\"\n\n**Events analyst**"]]},"skills/stock-diligence-report/references/estimation-methods.md":{"l":"md","n":39,"s":[[1,"# Estimating figures the company does not disclose (Appendix G standard)\n\n## Contents\n- When to estimate\n- The six-step procedure\n- Methods by figure\n- Labelling and placement\n- Worked instance (SPOT)\n\n## When to estimate\n\nEstimate only figures a reader needs for an investment idea and that the filings do not give: churn, gross additions, acquisition cost, lifetime value, marketplace or take-rate revenue, regional revenue when the geographic note is missing, segment margins, unit costs. Do not estimate figures that no identity connects to disclosed data (say why, as in G.5 of the SPOT report). Never present an estimate as a disclosed number.\n\n## The six-step procedure\n\n1. Identity. Write the accounting or flow identity that links the unknown to disclosed figures (net adds = gross adds − churn × base; consolidated gross margin = mix-weighted segment margins; regional revenue = subscriber share × price relativity, scaled to the total).\n2. Anchors. Collect every disclosed data point that bounds the unknown, including old ones (the prospectus often disclosed what later filings do not).\n3. Assumption ranges. For every assumption set low, most likely and high values with the source of each; ranges are the analyst's and are labelled as such.\n4. Propagation. Run a Monte Carlo (20,000 draws, triangular distributions) so that the output is a distribution, not a point; report the median with the 10th and 90th percentiles.\n5. Calibration. Check the result against any disclosed aggregate (a total, a quarter's disclosed segment figure, a prospectus data point) and state the check.\n6. Sensitivity and refresh. State which assumption dominates and by how much; name the disclosure that would replace the estimate.\n\nCompute in a script (`scripts/estimates.py` pattern in the SPOT example builder) so the numbers are reproducible; write the assumptions table, the result table and the calibration note into Appendix G.\n\n## Methods by figure\n\n- Marketplace or take-rate offsets: gross-margin residual (reported segment margin less royalty- or cost-implied margin); reports the combined bucket of undisclosed offsets.\n- Churn, gross adds, acquisition cost, lifetime value: flow identity with a churn anchor range; allocate the share of sales and marketing spent on acquisition; lifetime gross profit = ARPU × gross margin ÷ churn.\n- Regional revenue: subscriber or user share × price or CPM relativity from list prices and ad rates, scaled to the reported total; calibrate against any disclosed country share.\n- Segment margin: decomposition of the consolidated margin using the other segment's disclosed range.\n- Unit costs: total cost ÷ units with the numerator's composition from the notes.\n\n## Labelling and placement\n\nEvery estimated figure carries \"(est.)\" where it appears, with a note pointing to Appendix G; the appendix carries the identity, the assumption table with bases, the result table with the range, the calibration and the sensitivity. Section 1's insight ledger may cite an estimate only with its range.\n\n## Worked instance (SPOT, September 2026)\n\nMarketplace and structural royalty offsets: 7.5 points of Premium gross margin (5.6-9.4), about €1.1bn a year (€0.9-1.4bn). Monthly churn 3.5% (3.0-4.4%), gross adds 147m (127-172m), acquisition cost €6 per gross add, lifetime gross profit €43 per subscriber, ratio 6.9x. Regional Premium revenue shares: North America 36%, Europe 42%, Latin America 13%, rest of world 9%, with implied ARPU of €6.7, €5.4, €2.4 and €2.8 a month. FY2025 Premium gross margin 33.8% (33.7-33.9), bracketed by the disclosed 33% (Q2 2025) and 34.8% (Q4 2025)."]]},"skills/stock-diligence-report/references/presentation-standards.md":{"l":"md","n":68,"s":[[1,"# Presentation standards (enforced by scripts/report_renderer.js; read before Phase 5)\n\n## Contents\n- The rules and where they come from\n- Page and typography\n- Choosing the exhibit type\n- Table rules\n- Diagram rules\n- Writing layout rules\n- The failures these rules exist to prevent\n- QA checks\n\n## The rules and where they come from\n\nThe table rules follow the established canon of table design (Stephen Few's \"Show Me the Numbers\" and the derived guidance): text left-aligned, numbers right-aligned with consistent decimals, headers aligned with their data, horizontal rules only, row height about twice the font size, related rows grouped with space, short titles, no wrapped numbers. The document rules follow how institutional research is laid out: portrait pages, numbered exhibits with a title above and a source below, one idea per exhibit, prose that says what the exhibit means rather than repeating it.\n\n## Page and typography\n\n- Portrait only. The renderer ignores landscape markers. Wide content is split or transposed, never rotated.\n- US Letter, 1-inch margins, 6.5-inch text width (9,360 DXA). Arial 10 body, 9 in tables, 8 in table headers and notes. Headings 15/12/10.5 in dark blue 1F4E79. Header line with ticker and as-of dates; page numbers in the footer.\n- Every H1 starts a new page. Every exhibit has a numbered title above (\"Exhibit 12. Title\") and an italic source note below.\n\n## Choosing the exhibit type\n\n| Content | Block | Rule |\n|---|---|---|\n| Facts about one thing (tear sheet, risk statistics, bridge items, capacity) | `kv` | Two columns, label left, value right, grouped by theme, 6-10 rows per block; never two pairs side by side |\n| Metrics over periods (statements, ratios, KPIs, estimates) | `data` | Metrics in rows, periods in columns, at most 6 period columns (the renderer splits beyond that with (a), (b) suffixes), label column 32-40% of width |\n| Records with many attributes (products, brokers, events) | `cards` or `numbered` | One card per record with attribute rows, or a numbered narrative keyed by date or name; never a 7-column table |\n| Short text in a grid (moat table, glossary, dashboard) | `text` | 2-4 columns, all left-aligned, wrapped prose allowed |\n| Numeric grid with one text column | `data` | The renderer detects text columns (fewer than 60% numeric cells), left-aligns them and gives them 2.4 times the width; still prefer moving long text to bullets under the table |\n\n## Table rules\n\n1. Periods in columns, metrics in rows. Metric names are long and belong in the wide label column; period labels are short (\"FY2025\", \"Q2 2026\"). A table with metric names as column headers (the \"Quarter | Users, m | Subscribers, m | Net subscriber adds, m\" failure) is not allowed.\n2. Headers fit on one line at 8pt in their column; abbreviate (\"GM 2030\", \"CAGR 25-30\") and explain the abbreviation in the note.\n3. Consistent decimals down each column; thousands separators; negatives in parentheses; zero as \"0.0%\" or \"0\"; missing as \"n/d\"; not meaningful as \"n/m\".\n4. No sentences in cells. If a value needs explanation, the explanation goes in the note or in bullets under the table.\n5. Sub-metrics (margins, growth) are rows indented with three spaces; the renderer renders them italic grey.\n6. Totals and key lines in `bold_rows`.\n7. Alternate rows banded (F2F5F9); horizontal rules only; header row dark blue with white text.\n8. Maximum 6 period columns per table (`max_cols`); eight quarters become two tables of four.\n9. Notes state basis (as reported, standardised, derived, estimated), source and as-of date.\n\n## Diagram rules\n\nMoney-flow and structure diagrams are produced by `scripts/flow_diagram.py` from a JSON spec: three columns (payers, company, payees), the centre box spanning all lanes, every arrow horizontal in the gap between columns, each label wrapped to two lines in its own white box on its own lane, return flows in red on a lower lane. Nothing is positioned by hand and node text wraps at a fixed width. Any other diagram must satisfy the same test: no label touches a node or another label at 170 dpi (check the PNG before embedding).\n\n## Writing layout rules\n\n- Prose paragraphs of three to six sentences; a bold lead-in phrase is allowed (\"The deciding question is ...\") but not a \"Label: content\" construction (\"Claim: ...\", \"Must be true: ...\", \"Evidence: ...\").\n- Theses and insights are written as prose subsections or numbered paragraphs, never as compressed bullet strings.\n- Bullets are for lists of parallel items of one sentence each.\n- Callouts (shaded box) for the one arithmetic the reader must not miss in a section.\n\n## The failures these rules exist to prevent\n\n- A four-column tear sheet with two label-value pairs side by side, sentences in value cells, mixed alignment: replaced by grouped key-value blocks.\n- Landscape statement pages with 9-11 columns: replaced by portrait tables with at most 6 period columns and split exhibits.\n- A KPI table with metric names as column headers wrapping to three lines: replaced by metrics-in-rows.\n- A hand-positioned diagram with edge labels over node text: replaced by the lane-layout script.\n- Thesis bullets of the form \"Claim: ... Must be true: ... Model lines: ... Deciding question: ...\": replaced by three-paragraph prose per thesis.\n\n## QA checks (run before presenting)\n\n- Render to PDF; every page portrait; contact sheets of the tear sheet, one card page, one statement page, one appendix page inspected.\n- `pdftotext` audit: zero em dashes; zero \"Claim:\" / \"Must be true:\" / \"Evidence:\" strings; zero \"[NEED\"; every \"(est.)\" has an Appendix G method.\n- No exhibit wider than the text width; no header longer than one line; no numeric cell wrapped."]]},"skills/stock-diligence-report/references/insider-buybacks.md":{"l":"md","n":33,"s":[[1,"# Insider activity and share buybacks\n\n## Data to collect\n\n- Buyback authorisations: date, size, source (press release, 8-K/6-K, AGM resolution and the share-count or time limits it sets), remaining capacity.\n- Execution by quarter from the filings: shares repurchased, amount, implied average price; cumulative since inception; pace relative to free cash flow and to the drawdown (did the pace change when the stock fell?).\n- Share count: basic and diluted by quarter for three years; SBC expense; net dilution after buybacks; buyback yield (trailing four quarters of repurchases over market cap).\n- Dividends where relevant: history, payout ratio, coverage.\n- Insider transactions for 12-24 months (Forms 4 and 144; yfinance `insider_transactions` returns the Form 4 rows for many foreign private issuers too): person, role, date, shares, price, value, transaction type, plan or discretionary, cluster with other insiders, percentage of the person's holdings. Plan signal: identical share counts on a monthly or quarterly schedule paired with same-day option exercises at the same strike are plan-based sales (SPOT: the co-CEOs sold 20,833 and 5,436 shares on the first trading day of three consecutive months, each paired with an exercise at $151.25).\n- Ownership: founders and executives (shares and voting power, including dual-class or beneficiary-certificate structures), top institutional holders and changes from 13F, strategic holders (suppliers, customers, partners) and their recent moves.\n- Short interest: shares short, % of float, days to cover, 12-month history.\n\n## Tables\n\n1. Authorisation history (date, amount, cumulative, remaining, source).\n2. Execution by quarter (shares, €/$ amount, average price, share price range that quarter, % of FCF).\n3. Share count bridge (opening → SBC issuance → option exercises → repurchases → closing) for three years.\n4. Insider transactions (12-24 months) with the plan/discretionary flag and cluster flag.\n5. Holders: founders and management, top ten institutions with quarter-on-quarter change, strategic holders.\n6. Short interest history chart.\n\n## Interpretation rules (write these judgments explicitly)\n\n- Routine 10b5-1 sales by founders diversifying are not a signal; discretionary sales immediately after a guidance cut are.\n- A buyback accelerating into a drawdown while insiders sell is a mixed signal and must be described as mixed.\n- Net buybacks against SBC before calling capital return shareholder-friendly; state the net share-count change.\n- Average repurchase price versus the current price says whether the program has created value so far.\n- A supplier or partner buying the stock is a datapoint about the relationship (SPOT reference: UMG's $160m purchase in August 2026, verify against the filing).\n- Voting control changes the meaning of insider selling: founders can sell economic exposure without losing control.\n\n## SPOT reference figures (verify before reuse)\n\n$1.0bn program August 2021; +$1.0bn 29 July 2025; AGM 15 April 2026 renewed authority for 10m shares over five years; +$1.5bn 20 August 2026 to about $2.2bn total. Cumulative to 30 June 2026: 2,586,713 shares for €1,076m; H1 2026 1,349,216 shares for €547m (about €405 average). SBC FY2025 €247m. Ek cumulative sales above $800m by May 2025; co-CEO Söderström $10.6m sale August 2026; insiders sold $23.8m in the three months to May 2026 with no purchases."]]},"skills/stock-diligence-report/references/assumptions-memo.md":{"l":"md","n":15,"s":[[1,"# DCF assumptions memo (companion to the workbook)\n\nMulti-page, prose first, US Letter portrait, 10.5pt body, tables only for values. Built with `scripts/dcf_memo_builder.js` from a JSON of values read from the recalculated workbook; never type a number into the memo.\n\nStructure:\n1. What the model concludes: selected-method value and upside, the other terminal method, the scenario ladder table (Gordon and exit values, upside, revenue CAGR), where the price sits relative to the scenarios, the market-implied growth and exit multiple.\n2. How the model is built: statements linked, driver build, two-stage horizon, currency, the three policy choices (SBC as a real cost, interest on opening balances, stub and roll-forward).\n3. Operating assumptions, one subsection per lever with a five-column values table (last actual, FY1, FY2, FY5, FY10) then three short paragraphs: Anchor (the last reported figures and consensus), Mechanism (why the path bends the way it does, with the qualitative drivers), What would change it.\n4. Valuation parameters: discount rate, terminal value with the implied cross-checks, time mechanics and share count, equity bridge, then a compact parameter table.\n5. What today's price implies: market-implied growth and multiple, the reverse-DCF reading (which growth and margin pairs justify the price), sensitivity ranges, peer-implied price and the premium being paid.\n6. Calibration and integrity: model versus consensus table; the list of checks and their status in each scenario.\n7. Deliberately excluded and where the assumptions could be wrong: bullets.\n8. Sources.\n\nRules: mechanism over adjectives; every management claim not modelled is listed as excluded; regenerate the memo whenever inputs change; render to PDF and inspect before presenting."]]},"skills/stock-diligence-report/references/event-attribution.md":{"l":"md","n":71,"s":[[1,"# Key events: the attribution process (default window 5 years)\n\nGoal: every large move in the stock is explained, or explicitly marked unattributed, with the sector move and the idiosyncratic component recorded, so the reader knows which moves were about the company and which were about the market.\n\n## Step 1: flag the moves (script)\n\n`python3 scripts/event_scan.py TICKER --years 5 --sector <sector ETF> --peers A,B,C --out work/events`\n\nProduces:\n- `events.csv`: the 15 largest absolute daily moves with close, return, S&P and sector return, trailing 250-day beta to the sector, idiosyncratic return (stock return minus beta times sector), volume versus the 20-day average, forward 1-month and 3-month returns, and empty `cause`, `tag`, `source` columns.\n- `runs.csv`: runs of three or more same-direction days totalling more than 10%, with the sector's move over the same days.\n- `drawdowns.csv`: drawdowns deeper than 15% from the running peak, with trough and recovery dates.\n- `price_annotated.png`: five-year chart with the flagged days labelled.\n\nAdd any day the runs or drawdowns identify that is not already in the top 15 (a trough day, the first day of a run).\n\n## Step 1b: confirm earnings days first\n\nLoad the earnings-date history (yfinance `Ticker.earnings_dates`) and mark every flagged date that falls on or the day after a reported earnings date. Record the EPS estimate, the reported EPS and the surprise in the table. In the SPOT run this confirmed 11 of the 15 largest five-year moves as earnings days in one step, before any web search. The narrative for each still needs the release or deck (what was guided, what missed); tag it \"Earnings/guidance (date confirmed)\" until then.\n\n## Step 2: attribute, in this order, and stop when a primary source confirms\n\n1. Company disclosure within one trading day: earnings release, guidance, 8-K/6-K, press release, management change, capital return announcement.\n2. Web search: \"[company] stock [date]\" and \"[company] shares [month year]\"; read the two or three most specific articles.\n3. Analyst actions that day: initiations, upgrades, downgrades, target changes (trade press, the broker's own note if quoted).\n4. Sector news the same day: index moves, peer prints, thematic selloffs (compare the sector return column).\n5. Macro: rates, CPI, geopolitics (compare the S&P column).\n\nVerify against a primary source before writing the cause: the deck or release for the numbers versus consensus, the transcript for the sentence that moved the stock, the filing for a buyback or insider action. Never let an article's characterisation stand alone if the primary document is available.\n\n## Step 2b: choose the benchmark for the episode\n\nThe scanner's sector ETF is the default benchmark. For a thematic episode (an AI-disruption selloff, a rates shock, a tariff move) also pull the theme index that carried the story (IGV for software, KRE for regional banks, and so on) and report the stock's move against both. When the regression says idiosyncratic and the news says thematic, write the reconciliation: the stock's historical beta to the theme did not hold because the market treated it as a member of the theme for that period. The SPOT February 2026 episode is the reference case: sector ETF −1.6%, software ETF −4.6%, SPOT −6.8% with a trailing beta to software of only 0.4.\n\n## Step 3: tag\n\nEarnings/guidance · Sector/thematic · Macro · Management · Regulatory/legal · Product/partnership · Analyst · Capital return/insider · Unattributed.\n\nA move whose idiosyncratic component is small relative to beta times the sector move is tagged Sector/thematic even if company news existed that day; say both.\n\n## Step 4: the event table (in the report)\n\n| Date | Close | 1D % | Volume vs 20D | Sector 1D % | Idiosyncratic % | Tag | What the market keyed on (with EPS estimate, actual and surprise for earnings days) | Primary source | Fwd 1M | Fwd 3M |\n\nFollow with the multi-day runs table and the drawdowns table.\n\n## Step 5: chapters narrative\n\nGroup the events into chapters (a chapter is a period with one dominant narrative). For each: the narrative the market held, the numbers that supported or broke it, the multiple at the start and end, and what it implies for the current debate. The SPOT chapters in the reference build: H1 2025 re-rating on margin inflection; July 2025 ad and FX scare; September 2025 succession; January to February 2026 AI-disruption contagion and the saturation debate; April 2026 reinvestment guide-down; May 2026 Investor Day reset; August 2026 capital return.\n\n## Step 6: never force a cause\n\nIf the ordered search finds nothing within a day of the move and the sector explains most of it, write \"sector move; no company catalyst found\" and keep the tag Unattributed or Sector/thematic. A forced explanation is worse than an honest blank because the reader will act on it.\n\n## Worked example (the standard)\n\nSPOT, 3-5 Feb 2026: −6.8%, −7.1%, −6.3% on 2.5-3.8x normal volume, closing at $412.75 from $508.58, intraday low $405. No company disclosure. Trade press the same day could only cite market pressure, the pricing gap to Apple and Amazon, a MoffettNathanson Neutral at $487 from 30 Jan arguing subscriber growth was ending, and the Q4 print due 10 Feb. The S&P software and services index fell about 4% on 3 Feb and 4.6% on 5 Feb in a selloff dubbed \"software-mageddon\" triggered by an AI legal tool release, with roughly $1tn of software market value lost since 28 Jan. Tag: Sector/thematic with idiosyncratic overhangs (saturation debate, leadership change, pre-earnings margin fear). Verification: the 10 Feb print (751m MAU, 33.1% gross margin, +47% operating income) disproved the margin fear and the stock rose 14.75%. Forward 1M from the trough: +19%.\n\n## Done-when checklist\n\n```\nEvents QA\n- [ ] Earnings-date history loaded; every flagged date checked against it\n- [ ] Every flagged date, run and drawdown has a tag\n- [ ] Every tag other than Unattributed has a primary source"]]},"skills/stock-diligence-report/scripts/margin_bridge.py":{"l":"py","n":48,"s":[[1,"#!/usr/bin/env python3\n\"\"\"Margin bridge tables in the user's format: each line as % of revenue, % shave from gross profit, % shave from operating income, % shave from prior line.\nUsage: python3 margin_bridge.py statements.csv [--out outdir]\nInput CSV: rows = line items in P&L order with columns per period; required row names (case-insensitive, partial match ok):\n  revenue, cost of revenue, gross profit, [opex lines...], operating income, [below-the-line lines...], net income.\nOutputs: outdir/margin_bridge_<period>.csv per period, outdir/margin_bridge_all.csv (side-by-side), outdir/waterfall_<period>.png\"\"\"\nimport argparse, os, re, sys, pandas as pd, numpy as np\n\ndef find(idx, pat):\n    for i in idx:\n        if re.search(pat, str(i), re.I): return i\n    return None\n\ndef main():\n    ap = argparse.ArgumentParser(); ap.add_argument(\"csv\"); ap.add_argument(\"--out\", default=\".\"); a = ap.parse_args(); os.makedirs(a.out, exist_ok=True)\n    df = pd.read_csv(a.csv, index_col=0); df = df.apply(pd.to_numeric, errors=\"coerce\")\n    rev = find(df.index, r\"^total revenue|^revenue\"); gp = find(df.index, r\"gross profit\"); op = find(df.index, r\"operating income|operating profit|ebit$\")\n    if not all([rev, gp, op]): sys.exit(\"need revenue, gross profit and operating income rows\")\n    allp = []\n    for per in df.columns:\n        s = df[per]; out = []; prev = None\n        for item in df.index:\n            v = s[item]\n            row = {\"period\": per, \"line\": item, \"amount\": v, \"pct_of_revenue\": v / s[rev] if s[rev] else np.nan,\n                   \"shave_from_gross_profit\": (v / s[gp] if item not in (rev, gp) and s[gp] else np.nan),\n                   \"shave_from_operating_income\": (v / s[op] if list(df.index).index(item) > list(df.index).index(op) and s[op] else np.nan),\n                   \"shave_from_prior_profit_line\": (v / prev if prev not in (None, 0) and item not in (rev,) else np.nan)}\n            if re.search(r\"gross profit|operating income|operating profit|income before tax|pre-tax|net income|ebit\", str(item), re.I): prev = v\n            out.append(row)\n        t = pd.DataFrame(out); t.to_csv(os.path.join(a.out, f\"margin_bridge_{per}.csv\"), index=False); allp.append(t)\n        try:\n            import matplotlib; matplotlib.use(\"Agg\"); import matplotlib.pyplot as plt\n            items = [i for i in df.index if i != rev]; vals = []; run = s[rev]; steps = [(rev, s[rev], 0)]\n            for i in items:\n                if re.search(r\"gross profit|operating income|operating profit|net income|income before tax|pre-tax|ebit\", str(i), re.I): steps.append((i, s[i], None)); run = s[i]\n                else: steps.append((i, s[i], run)); run = run + s[i] if s[i] < 0 else run - abs(s[i])\n            fig, ax = plt.subplots(figsize=(11, 5)); x = 0\n            for name, v, base in steps:\n                if base is None or name == rev: ax.bar(x, v, color=\"#1F4E79\")\n                else: ax.bar(x, -abs(v), bottom=base, color=\"#C00000\")\n                ax.text(x, (v if base is None or name == rev else base) , f\"{v:,.0f}\", ha=\"center\", va=\"bottom\", fontsize=7); x += 1\n            ax.set_xticks(range(len(steps))); ax.set_xticklabels([n[:18] for n, _, _ in steps], rotation=35, ha=\"right\", fontsize=8)\n            ax.set_title(f\"Margin waterfall {per}\"); ax.grid(axis=\"y\", alpha=.3); fig.tight_layout(); fig.savefig(os.path.join(a.out, f\"waterfall_{per}.png\"), dpi=150); plt.close(fig)\n        except Exception as e: print(\"chart skipped:\", e, file=sys.stderr)\n    full = pd.concat(allp); full.to_csv(os.path.join(a.out, \"margin_bridge_all.csv\"), index=False)\n    piv = full.pivot(index=\"line\", columns=\"period\", values=\"pct_of_revenue\").reindex(df.index); print((piv * 100).round(1).to_string())\n\nif __name__ == \"__main__\": main()"]]},"skills/stock-diligence-report/scripts/dcf_builder_template.py":{"l":"py","n":1393,"s":[[1,"\"\"\"\nDCF BUILDER TEMPLATE (SPOT reference instance). Adapt for another company:\n  1. Replace the `D` scenario dictionary and the Drivers formulas with the company's revenue model.\n  2. Replace the Hist rows, the bridge items and the market data block with the new filings.\n  3. Keep the scaffold, checks, sensitivity engine, style pass and the validation loop (see references/dcf-build-process.md).\nInputs expected under work/: data/prices_daily.csv (daily prices for the 52-week range); outputs the workbook and a cover map JSON.\nAfter building: python3 /mnt/skills/public/xlsx/scripts/recalc.py SPOT_DCF.xlsx 60, then run the scenario/toggle loop and write the static snapshot to the Cover.\n\"\"\"\n\"\"\"\nSpotify (SPOT) linked three-statement DCF — prototype for the diligence skill.\nModel currency: EUR millions. Share price converted to USD at spot EUR/USD.\nColumn convention (identical on every projection sheet):\n  A = label, B = unit/note, C..G = FY2021A..FY2025A, H..Q = FY2026E..FY2035E\n\"\"\"\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Font, PatternFill, Alignment, Border, Side\nfrom openpyxl.utils import get_column_letter as L\nfrom openpyxl.comments import Comment\nfrom openpyxl.chart import BarChart, LineChart, Reference, Series\n\nwb = Workbook()\nFONT = \"Arial\"\nBLUE = Font(name=FONT, color=\"0000FF\", size=10)\nBLACK = Font(name=FONT, color=\"000000\", size=10)\nGREEN = Font(name=FONT, color=\"008000\", size=10)\nBOLD = Font(name=FONT, bold=True, size=10)\nHDR = Font(name=FONT, bold=True, size=11, color=\"FFFFFF\")\nTITLE = Font(name=FONT, bold=True, size=14)\nYELLOW = PatternFill(\"solid\", fgColor=\"FFFF00\")\nGREY = PatternFill(\"solid\", fgColor=\"D9D9D9\")\nNAVY = PatternFill(\"solid\", fgColor=\"1F3864\")\nLIGHT = PatternFill(\"solid\", fgColor=\"EAF1FB\")\nthin = Side(style=\"thin\", color=\"999999\")\nTOPLINE = Border(top=Side(style=\"thin\", color=\"000000\"))\n\nYEARS = list(range(2021, 2036))          # 2021..2035\nHIST = list(range(2021, 2026))           # actuals\nPROJ = list(range(2026, 2036))           # estimates\ndef col(y): return 3 + (y - 2021)        # 2021 -> C(3)\ndef cl(y): return L(col(y))\nFIRST_P, LAST_P = cl(2026), cl(2035)     # H, Q\nPREV = {y: cl(y - 1) for y in YEARS if y > 2021}\n\nNUM = '#,##0;(#,##0);\"-\"'\nNUM1 = '#,##0.0;(#,##0.0);\"-\"'\nNUM2 = '#,##0.00;(#,##0.00);\"-\"'\nPCT = '0.0%;(0.0%);\"-\"'\nMULT = '0.0\"x\"'\nUSD = '\"$\"#,##0.00'\nEUR = '\"€\"#,##0.00'\n\ndef setw(ws, widths):\n    for k, v in widths.items():\n        ws.column_dimensions[k].width = v\n"]]},"skills/stock-diligence-report/scripts/dcf_validate.py":{"l":"py","n":70,"s":[[1,"#!/usr/bin/env python3\n\"\"\"One-command DCF validation: recalc, checks, scenario and toggle runs, static snapshot on the Cover, summary JSON.\nUsage: python3 dcf_validate.py WORKBOOK.xlsx [--out work/model]\nReplaces the hand-run loop (recalc, read values, loop scenarios, write snapshot) with a single deterministic pass.\nRequires: /mnt/skills/public/xlsx/scripts/recalc.py, a Cover with the scenario selector in B4, Inputs!C6 holding the\nscenario name, and a Checks tab whose overall status is the last non-empty cell in column C.\"\"\"\nimport argparse, json, os, shutil, subprocess, sys\nfrom openpyxl import load_workbook\nfrom openpyxl.styles import Font, PatternFill\nRECALC = \"/mnt/skills/public/xlsx/scripts/recalc.py\"\n\ndef recalc(path):\n    out = subprocess.run([\"python3\", RECALC, path, \"60\"], capture_output=True, text=True).stdout\n    try: return json.loads(out)\n    except Exception: return {\"total_errors\": -1, \"raw\": out[:500]}\n\ndef find_row(ws, prefix, col=1, limit=120):\n    for r in range(1, limit):\n        if str(ws.cell(r, col).value).strip().startswith(prefix): return r\n    return None\n\ndef read_outputs(path):\n    v = load_workbook(path, data_only=True); cv, ck, dc, dv, isw, ip = v[\"Cover\"], v[\"Checks\"], v[\"DCF\"], v[\"Drivers\"], v[\"IS\"], v[\"Inputs\"]\n    status_row = max(r for r in range(1, 60) if ck.cell(r, 3).value not in (None, \"\"))\n    def cover(label):\n        r = find_row(cv, label); return cv.cell(r, 2).value if r else None\n    dual = find_row(dc, \"Implied value per share (US$)\", limit=200)\n    rows = [r for r in range(1, 200) if str(dc.cell(r, 1).value).startswith(\"Implied value per share (US$)\")]\n    gordon = dc.cell(rows[-1], 3).value if rows else None; exit_ = dc.cell(rows[-1], 4).value if rows else None\n    return {\"scenario\": ip[\"C6\"].value, \"price\": cover(\"Implied value per share, selected method (US$)\"), \"upside\": cover(\"Upside / (downside)\"), \"ev\": cover(\"Enterprise value (€m)\"),\n            \"tv_pct\": cover(\"Terminal value % of EV\"), \"cagr\": dv.cell(find_row(dv, \"Revenue CAGR\"), 3).value, \"gm_y5\": dv.cell(find_row(dv, \"Gross margin FY\"), 3).value,\n            \"om_y5\": dv.cell(find_row(dv, \"Operating margin FY\"), 3).value, \"eps_y2\": isw.cell(find_row(isw, \"Diluted EPS\"), 9).value, \"gordon\": gordon, \"exit\": exit_,\n            \"status\": ck.cell(status_row, 3).value, \"flags\": [ck.cell(r, 1).value for r in range(4, status_row) if ck.cell(r, 3).value in (\"CHECK\", \"FAIL\")]}\n\ndef set_input(wb, prefix, value):\n    ws = wb[\"Inputs\"]; r = find_row(ws, prefix); ws.cell(r, 3).value = value\n\ndef main():\n    ap = argparse.ArgumentParser(); ap.add_argument(\"workbook\"); ap.add_argument(\"--out\", default=\"work/model\"); a = ap.parse_args(); os.makedirs(a.out, exist_ok=True)\n    base = recalc(a.workbook); print(\"base recalc:\", base.get(\"total_errors\"), \"errors\")\n    if base.get(\"total_errors\") != 0: sys.exit(\"recalc errors: \" + json.dumps(base)[:800])\n    runs = {}\n    for s in (1, 2, 3, 4):\n        wb = load_workbook(a.workbook); wb[\"Cover\"][\"B4\"] = s; tmp = os.path.join(a.out, f\"_scen{s}.xlsx\"); wb.save(tmp)\n        rc = recalc(tmp); assert rc.get(\"total_errors\") == 0, rc; runs[s] = read_outputs(tmp); os.remove(tmp)\n        print(f\"scenario {s}: {runs[s]['scenario']} price {runs[s]['price']:.2f} status {runs[s]['status']}\")\n    toggles = {}\n    for label, prefix, val in [(\"exit_method\", \"Terminal value method\", 2), (\"mid_year_off\", \"Mid-year\", 0), (\"sbc_addback\", \"Add back SBC\", 1)]:\n        wb = load_workbook(a.workbook); set_input(wb, prefix, val); tmp = os.path.join(a.out, f\"_{label}.xlsx\"); wb.save(tmp)\n        rc = recalc(tmp); assert rc.get(\"total_errors\") == 0, rc; toggles[label] = read_outputs(tmp)[\"price\"]; os.remove(tmp)\n    print(\"toggles:\", {k: round(v, 2) for k, v in toggles.items()})\n    # grid consistency: exit toggle price must equal the DCF's side-by-side exit value in the base run\n    assert abs(toggles[\"exit_method\"] - runs[1][\"exit\"]) < 0.05, \"exit toggle does not reproduce the side-by-side exit value\"\n    wb = load_workbook(a.workbook); cv = wb[\"Cover\"]; top = find_row(cv, \"Scenario snapshot\")"]]},"skills/stock-diligence-report/scripts/build_exhibits.py":{"l":"py","n":216,"s":[[1,"#!/usr/bin/env python3\n\"\"\"Build every numeric exhibit and chart of the diligence report from the data folder, with no model involvement.\n\nUsage: python3 build_exhibits.py --company company.json --data work/data [--technicals work/technicals] [--events work/events]\n       [--bridge work/bridge] [--model work/model/scen.json] [--workbook work/model/TICKER_DCF.xlsx] --out work\nWrites work/exhibits.json: {\"<key>\": [blocks...]} in the renderer's block schema, and work/charts/*.png.\ncompany.json: {\"ticker\": \"SPOT\", \"name\": \"Spotify\", \"price_ccy\": \"USD\", \"report_ccy\": \"EUR\", \"fx\": 1.1627,\n               \"index\": \"^GSPC\", \"sector\": \"XLC\", \"theme\": \"IGV\", \"peers\": {\"NFLX\": \"Netflix\", ...}, \"as_of\": \"2026-09-04\"}\nEvery exhibit carries its source note; missing inputs skip the exhibit and are listed in work/exhibits_gaps.json.\"\"\"\nimport argparse, json, os, re\nimport numpy as np, pandas as pd\n\ndef f0(x): return \"n/d\" if x is None or (isinstance(x, float) and np.isnan(x)) else (f\"{x:,.0f}\" if x >= 0 else f\"({abs(x):,.0f})\")\ndef f1(x): return \"n/d\" if x is None or (isinstance(x, float) and np.isnan(x)) else (f\"{x:,.1f}\" if x >= 0 else f\"({abs(x):,.1f})\")\ndef f2(x): return \"n/d\" if x is None or (isinstance(x, float) and np.isnan(x)) else f\"{x:,.2f}\"\ndef pc(x, n=1): return \"n/d\" if x is None or (isinstance(x, float) and np.isnan(x)) else f\"{x*100:.{n}f}%\"\ndef pcs(x, n=1): return \"n/d\" if x is None or (isinstance(x, float) and np.isnan(x)) else f\"{x*100:+.{n}f}%\"\ndef KV(title, rows, note=None): return {\"kv\": {\"title\": title, \"rows\": [[str(a), str(b)] for a, b in rows], \"note\": note}}\ndef DATA(title, header, rows, note=None, bold_rows=None, max_cols=6): return {\"data\": {\"title\": title, \"header\": [str(h) for h in header], \"rows\": [[str(c) for c in r] for r in rows], \"note\": note, \"bold_rows\": bold_rows or [], \"max_cols\": max_cols}}\ndef IMG(path, title, note=None, width_in=6.3):\n    from PIL import Image\n    im = Image.open(path); w = int(width_in * 96); h = int(w * im.height / im.width)\n    return {\"image\": {\"path\": os.path.abspath(path), \"width\": w, \"height\": h, \"title\": title, \"note\": note}}\n\ndef g(df, names, default=np.nan):\n    for n in names:\n        if n in df.index: return df.loc[n]\n    return pd.Series(default, index=df.columns)\n\ndef main():\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"--company\", required=True); ap.add_argument(\"--data\", default=\"work/data\"); ap.add_argument(\"--technicals\", default=\"work/technicals\")\n    ap.add_argument(\"--events\", default=\"work/events\"); ap.add_argument(\"--bridge\", default=\"work/bridge\"); ap.add_argument(\"--model\", default=\"work/model/scen.json\")\n    ap.add_argument(\"--workbook\", default=\"\"); ap.add_argument(\"--out\", default=\"work\")\n    a = ap.parse_args(); C = json.load(open(a.company)); D = a.data; T = C[\"ticker\"]; ccy = C.get(\"report_ccy\", \"USD\"); fx = C.get(\"fx\", 1.0)\n    os.makedirs(f\"{a.out}/charts\", exist_ok=True); EX = {}; gaps = []\n    def have(p): return os.path.exists(p)\n    import matplotlib; matplotlib.use(\"Agg\"); import matplotlib.pyplot as plt\n    BLUE, GREYC, ORANGE, RED = \"#1F4E79\", \"#7F7F7F\", \"#ED7D31\", \"#C00000\"\n\n    # ------------------------------------------------------------ prices, returns, risk, charts\n    if have(f\"{D}/prices_multi.csv\") and have(f\"{D}/prices_daily.csv\"):\n        close = pd.read_csv(f\"{D}/prices_multi.csv\", index_col=0, parse_dates=True).dropna(subset=[T]); px = pd.read_csv(f\"{D}/prices_daily.csv\", index_col=0, parse_dates=True)\n        spot = close[T]; last = spot.index[-1]\n        def ret_since(s, dt):\n            s2 = s[s.index <= dt]; return s.iloc[-1] / s2.iloc[-1] - 1 if len(s2) else np.nan\n        anchors = {\"1M\": last - pd.DateOffset(months=1), \"3M\": last - pd.DateOffset(months=3), \"6M\": last - pd.DateOffset(months=6), \"YTD\": pd.Timestamp(last.year - 1, 12, 31),\n                   \"1Y\": last - pd.DateOffset(years=1), \"3Y\": last - pd.DateOffset(years=3), \"5Y\": last - pd.DateOffset(years=5), \"10Y\": last - pd.DateOffset(years=10)}\n        names = {T: C.get(\"name\", T), C.get(\"index\", \"^GSPC\"): \"S&P 500\", C.get(\"sector\", \"XLC\"): f\"{C.get('sector', 'XLC')} (sector ETF)\"}; names.update(C.get(\"peers\", {}))"]]},"skills/stock-diligence-report/scripts/event_scan.py":{"l":"py","n":87,"s":[[1,"#!/usr/bin/env python3\n\"\"\"Flag the price moves that need attribution and compute idiosyncratic vs sector components.\n\nUsage: python3 event_scan.py TICKER [--years 5] [--sector XLC] [--peers NFLX,WMG] [--top 15] [--out outdir]\nOutputs in outdir: events.csv (flagged dates with empty cause/tag/source columns to fill), runs.csv (3+ day\nsame-direction runs > 10%), drawdowns.csv (> 15% from running peak), price_annotated.png.\n\nThe analyst then attributes each flagged date in this order: company 8-K/6-K/press release within one trading\nday -> web \"[company] stock [date]\" -> analyst actions -> sector news -> macro, and\nfills cause, tag and source. 'Unattributed' is a valid tag; never force a cause.\n\"\"\"\nimport argparse, os, sys\nimport numpy as np, pandas as pd\n\n\ndef load(ticker, years):\n    import yfinance as yf\n    h = yf.Ticker(ticker).history(period=f\"{years}y\", auto_adjust=True)\n    h.index = h.index.tz_localize(None)\n    return h\n\n\ndef main():\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"ticker\"); ap.add_argument(\"--years\", type=int, default=5)\n    ap.add_argument(\"--sector\", default=\"XLC\", help=\"sector ETF or index proxy\")\n    ap.add_argument(\"--peers\", default=\"\", help=\"comma-separated peer tickers for an equal-weight basket\")\n    ap.add_argument(\"--top\", type=int, default=15); ap.add_argument(\"--out\", default=\".\")\n    a = ap.parse_args(); os.makedirs(a.out, exist_ok=True)\n    px = load(a.ticker, a.years); spx = load(\"^GSPC\", a.years); sec = load(a.sector, a.years)\n    r = px[\"Close\"].pct_change()\n    df = pd.DataFrame({\"close\": px[\"Close\"], \"ret\": r, \"spx\": spx[\"Close\"].pct_change().reindex(r.index),\n                       \"sector\": sec[\"Close\"].pct_change().reindex(r.index), \"volume\": px[\"Volume\"]})\n    df[\"vol20\"] = df[\"volume\"].rolling(20).mean().shift(1); df[\"vol_x\"] = df[\"volume\"] / df[\"vol20\"]\n    cov = df[\"ret\"].rolling(250).cov(df[\"sector\"]); var = df[\"sector\"].rolling(250).var()\n    df[\"beta_sector\"] = (cov / var).shift(1).fillna(1.0)\n    df[\"idio\"] = df[\"ret\"] - df[\"beta_sector\"] * df[\"sector\"]\n    if a.peers:\n        basket = []\n        for p in a.peers.split(\",\"):\n            try: basket.append(load(p.strip(), a.years)[\"Close\"].pct_change().reindex(r.index))\n            except Exception: pass\n        if basket: df[\"peers\"] = pd.concat(basket, axis=1).mean(axis=1)\n    df[\"fwd_1m\"] = df[\"close\"].shift(-21) / df[\"close\"] - 1\n    df[\"fwd_3m\"] = df[\"close\"].shift(-63) / df[\"close\"] - 1\n    top = df.reindex(df[\"ret\"].abs().sort_values(ascending=False).index[: a.top]).copy()\n\n    # multi-day runs: 3+ consecutive same-sign days totalling more than 10%\n    sign = np.sign(df[\"ret\"].fillna(0)); runs = []; start = 1\n    for i in range(2, len(df) + 1):\n        if i == len(df) or sign.iloc[i] != sign.iloc[i - 1]:\n            seg = df.iloc[start:i]\n            if len(seg) >= 3 and abs((1 + seg[\"ret\"]).prod() - 1) > 0.10:\n                runs.append({\"start\": seg.index[0].date(), \"end\": seg.index[-1].date(), \"days\": len(seg),\n                             \"total\": (1 + seg[\"ret\"]).prod() - 1, \"sector_total\": (1 + seg[\"sector\"]).prod() - 1})\n            start = i\n    # drawdowns deeper than 15% from the running peak\n    peak = df[\"close\"].cummax(); dd = df[\"close\"] / peak - 1; ddf = []; in_dd = False\n    for dt, v in dd.items():\n        if not in_dd and v < -0.15: in_dd, s, trough = True, dt, (dt, v)\n        elif in_dd:\n            if v < trough[1]: trough = (dt, v)\n            if v == 0: ddf.append({\"start\": s.date(), \"trough\": trough[0].date(), \"trough_dd\": trough[1], \"recovered\": dt.date()}); in_dd = False\n    if in_dd: ddf.append({\"start\": s.date(), \"trough\": trough[0].date(), \"trough_dd\": trough[1], \"recovered\": None})\n\n    cols = [\"close\", \"ret\", \"spx\", \"sector\", \"beta_sector\", \"idio\", \"vol_x\", \"fwd_1m\", \"fwd_3m\"] + ([\"peers\"] if \"peers\" in df else [])\n    out = top[cols].sort_index()\n    for c in [\"cause\", \"tag\", \"source\"]: out[c] = \"\"\n    out.to_csv(os.path.join(a.out, \"events.csv\"))\n    pd.DataFrame(runs).to_csv(os.path.join(a.out, \"runs.csv\"), index=False)\n    pd.DataFrame(ddf).to_csv(os.path.join(a.out, \"drawdowns.csv\"), index=False)\n    try:"]]},"skills/stock-diligence-report/scripts/dcf_memo_builder.js":{"l":"js","n":217,"s":[[1,"const fs = require(\"fs\");\nconst { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, WidthType, AlignmentType, BorderStyle,\n        ShadingType, HeadingLevel, LevelFormat, PageNumber, Footer, Header, TabStopType } = require(\"docx\");\n\nconst d = JSON.parse(fs.readFileSync(\"work/model/memo_data.json\", \"utf8\"));\nconst D = d.D, o = d.out, S = d.scen;\nconst FONT = \"Arial\";\nconst pct = (x, n = 1) => `${(x * 100).toFixed(n)}%`;\nconst sgn = (x, n = 1) => `${x >= 0 ? \"+\" : \"\"}${(x * 100).toFixed(n)}%`;\nconst n0 = (x) => x.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\nconst usd = (x) => `$${x.toFixed(0)}`;\n\n// ---------- primitives ----------\nconst border = { style: BorderStyle.SINGLE, size: 4, color: \"BFBFBF\" };\nconst borders = { top: border, bottom: border, left: border, right: border };\nfunction P(text, opts = {}) {\n  const runs = Array.isArray(text) ? text : [{ text }];\n  return new Paragraph({\n    alignment: opts.align || AlignmentType.LEFT,\n    spacing: { before: opts.before ?? 0, after: opts.after ?? 120, line: 276 },\n    keepNext: !!opts.keepNext,\n    children: runs.map((r) => new TextRun({ text: r.text, font: FONT, size: opts.size || 21, bold: r.bold || opts.bold, italics: r.italics || opts.italics, color: r.color || opts.color || \"000000\" })),\n  });\n}\nfunction H1(text) { return new Paragraph({ heading: HeadingLevel.HEADING_1, spacing: { before: 280, after: 120 }, keepNext: true, children: [new TextRun({ text, font: FONT, size: 26, bold: true, color: \"1F4E79\" })] }); }\nfunction H2(text) { return new Paragraph({ heading: HeadingLevel.HEADING_2, spacing: { before: 200, after: 80 }, keepNext: true, children: [new TextRun({ text, font: FONT, size: 22, bold: true, color: \"1F4E79\" })] }); }\nfunction bullet(text) {\n  const runs = Array.isArray(text) ? text : [{ text }];\n  return new Paragraph({ numbering: { reference: \"bul\", level: 0 }, spacing: { after: 60, line: 264 },\n    children: runs.map((r) => new TextRun({ text: r.text, font: FONT, size: 21, bold: r.bold, italics: r.italics })) });\n}\nfunction cell(text, width, opts = {}) {\n  return new TableCell({ width: { size: width, type: WidthType.DXA }, borders, margins: { top: 40, bottom: 40, left: 80, right: 80 },\n    shading: opts.fill ? { type: ShadingType.CLEAR, color: \"auto\", fill: opts.fill } : undefined,\n    children: [new Paragraph({ alignment: opts.align || AlignmentType.LEFT, spacing: { after: 0 },\n      children: [new TextRun({ text: String(text), font: FONT, size: opts.size || 19, bold: opts.bold, color: opts.color || \"000000\" })] })] });\n}\nfunction table(widths, rows, header = true) {\n  const trs = rows.map((r, i) => new TableRow({ tableHeader: i === 0 && header, cantSplit: true,\n    children: r.map((t, j) => cell(t, widths[j], i === 0 && header ? { fill: \"1F4E79\", bold: true, color: \"FFFFFF\", align: j === 0 ? AlignmentType.LEFT : AlignmentType.RIGHT }\n      : { align: j === 0 ? AlignmentType.LEFT : AlignmentType.RIGHT, bold: j === 0 })) }));\n  return new Table({ width: { size: widths.reduce((a, b) => a + b, 0), type: WidthType.DXA }, columnWidths: widths, rows: trs });\n}\nconst TW = 9360; // 6.5\" text width in DXA\nfunction valuesTable(label, vals) {\n  const w = [2160, 1440, 1440, 1440, 1440, 1440];\n  return table(w, [[\"Lever\", \"FY2025A\", \"FY2026E\", \"FY2027E\", \"FY2030E\", \"FY2035E\"], [label, ...vals]]);\n}\nconst spacer = () => new Paragraph({ spacing: { after: 100 }, children: [] });\n\n// ---------- content ----------\nconst price = 543.17;\nconst gap26 = D.rev[1] / 19538 - 1, gap27 = D.rev[2] / 22309 - 1, eg26 = D.eps[1] / 12.30 - 1, eg27 = D.eps[2] / 15.51 - 1;\nconst kids = [];\n\nkids.push(new Paragraph({ spacing: { after: 40 }, children: [new TextRun({ text: \"Spotify Technology S.A. (NYSE: SPOT)\", font: FONT, size: 32, bold: true, color: \"1F4E79\" })] }));\nkids.push(new Paragraph({ spacing: { after: 40 }, children: [new TextRun({ text: \"DCF assumptions and rationale\", font: FONT, size: 26, bold: true })] }));"]]},"skills/stock-diligence-report/scripts/data_pull.py":{"l":"py","n":55,"s":[[1,"#!/usr/bin/env python3\n\"\"\"yfinance data pull (the primary market-data source for the skill).\nUsage: python3 data_pull.py TICKER [--peers NFLX,WMG] [--years 10] [--out work/data]\nWrites: prices_daily.csv, prices_multi.csv (ticker, index, sector ETF, theme ETF, peers), earnings_dates.csv, institutional_holders.csv, insider_transactions.csv, short_interest.json, is_annual.csv, bs_annual.csv, cf_annual.csv, is_quarterly.csv, bs_quarterly.csv, cf_quarterly.csv,\n        estimates.json (revenue/EPS FY1-FY2, targets, recommendations), info.json, peers.csv, fx.json\nKnown defects to remember: yfinance 'freeCashflow' is unreliable; ADR enterprise values can be wrong; trailing P/E distorted by one-offs.\"\"\"\nimport argparse, json, os\nimport pandas as pd\n\ndef main():\n    ap = argparse.ArgumentParser(); ap.add_argument(\"ticker\"); ap.add_argument(\"--peers\", default=\"\"); ap.add_argument(\"--years\", type=int, default=10); ap.add_argument(\"--out\", default=\"work/data\"); ap.add_argument(\"--index\", default=\"^GSPC\"); ap.add_argument(\"--sector\", default=\"XLC\"); ap.add_argument(\"--theme\", default=\"\")\n    a = ap.parse_args(); os.makedirs(a.out, exist_ok=True)\n    import yfinance as yf\n    t = yf.Ticker(a.ticker)\n    h = t.history(period=f\"{a.years}y\", auto_adjust=True); h.index = h.index.tz_localize(None); h.to_csv(f\"{a.out}/prices_daily.csv\")\n    multi = {a.ticker: h[\"Close\"]}\n    for sym in [a.index, a.sector] + ([a.theme] if a.theme else []) + [x.strip() for x in a.peers.split(\",\") if x.strip()]:\n        try:\n            hh = yf.Ticker(sym).history(period=f\"{a.years}y\", auto_adjust=True); hh.index = hh.index.tz_localize(None); multi[sym] = hh[\"Close\"]\n        except Exception as e: print(sym, \"prices failed\", e)\n    pd.DataFrame(multi).to_csv(f\"{a.out}/prices_multi.csv\")\n    for name, fn in [(\"earnings_dates\", lambda: t.earnings_dates), (\"institutional_holders\", lambda: t.institutional_holders), (\"insider_transactions\", lambda: t.insider_transactions)]:\n        try:\n            df = fn()\n            if df is not None and len(df): df.to_csv(f\"{a.out}/{name}.csv\")\n        except Exception as e: print(name, \"failed\", e)\n    si = {k: info.get(k) for k in [\"sharesShort\", \"sharesShortPriorMonth\", \"shortPercentOfFloat\", \"shortRatio\", \"dateShortInterest\", \"heldPercentInstitutions\", \"heldPercentInsiders\", \"floatShares\"]} if (info := t.info) else {}\n    json.dump(si, open(f\"{a.out}/short_interest.json\", \"w\"), default=str)\n    for name, df in [(\"is_annual\", t.income_stmt), (\"bs_annual\", t.balance_sheet), (\"cf_annual\", t.cashflow),\n                     (\"is_quarterly\", t.quarterly_income_stmt), (\"bs_quarterly\", t.quarterly_balance_sheet), (\"cf_quarterly\", t.quarterly_cashflow)]:\n        try: (df / 1e6).to_csv(f\"{a.out}/{name}.csv\")\n        except Exception as e: print(name, \"failed\", e)\n    est = {}\n    for name, fn in [(\"revenue_estimate\", lambda: t.revenue_estimate), (\"earnings_estimate\", lambda: t.earnings_estimate), (\"growth_estimates\", lambda: t.growth_estimates), (\"recommendations\", lambda: t.recommendations_summary)]:\n        try: est[name] = fn().to_dict()\n        except Exception as e: est[name] = f\"failed: {e}\"\n    try: est[\"price_targets\"] = t.analyst_price_targets\n    except Exception as e: est[\"price_targets\"] = f\"failed: {e}\"\n    json.dump(est, open(f\"{a.out}/estimates.json\", \"w\"), default=str, indent=1)\n    keys = [\"shortName\", \"currency\", \"financialCurrency\", \"sharesOutstanding\", \"floatShares\", \"beta\", \"marketCap\", \"enterpriseValue\", \"currentPrice\", \"trailingPE\", \"forwardPE\",\n                           \"enterpriseToRevenue\", \"enterpriseToEbitda\", \"grossMargins\", \"operatingMargins\", \"revenueGrowth\", \"sector\", \"industry\", \"fullTimeEmployees\", \"longBusinessSummary\"]\n    json.dump({k: info.get(k) for k in keys}, open(f\"{a.out}/info.json\", \"w\"), indent=1, default=str)\n    rows = []\n    for p in [a.ticker] + [x.strip() for x in a.peers.split(\",\") if x.strip()]:\n        try:\n            i = yf.Ticker(p).info"]]},"skills/stock-diligence-report/scripts/dcf_build.py":{"l":"py","n":1194,"s":[[1,"#!/usr/bin/env python3\n\"\"\"Config-driven linked three-statement DCF builder (subscription-plus-ads archetype; adapt the Drivers block for other revenue models).\nUsage: python3 dcf_build.py --config dcf_config.json --out work/model/TICKER_DCF.xlsx ; then python3 dcf_validate.py <xlsx> --out work/model\nEverything company-specific lives in the config (market data, WACC inputs, switches, bridge, working-capital ratios, the four-scenario\ndriver matrix, historical rows with canonical names, debt instruments, consensus, comps). See references/dcf-build-process.md.\"\"\"\nimport json, sys, datetime\nCFG = json.load(open(sys.argv[sys.argv.index(\"--config\") + 1])); OUTPATH = sys.argv[sys.argv.index(\"--out\") + 1] if \"--out\" in sys.argv else \"DCF.xlsx\"\nCCY = CFG.get(\"currency_symbol\", \"€\"); TICK = CFG[\"ticker\"]\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Font, PatternFill, Alignment, Border, Side\nfrom openpyxl.utils import get_column_letter as L\nfrom openpyxl.comments import Comment\nfrom openpyxl.chart import BarChart, LineChart, Reference, Series\n\nwb = Workbook()\nFONT = \"Arial\"\nBLUE = Font(name=FONT, color=\"0000FF\", size=10)\nBLACK = Font(name=FONT, color=\"000000\", size=10)\nGREEN = Font(name=FONT, color=\"008000\", size=10)\nBOLD = Font(name=FONT, bold=True, size=10)\nHDR = Font(name=FONT, bold=True, size=11, color=\"FFFFFF\")\nTITLE = Font(name=FONT, bold=True, size=14)\nYELLOW = PatternFill(\"solid\", fgColor=\"FFFF00\")\nGREY = PatternFill(\"solid\", fgColor=\"D9D9D9\")\nNAVY = PatternFill(\"solid\", fgColor=\"1F3864\")\nLIGHT = PatternFill(\"solid\", fgColor=\"EAF1FB\")\nthin = Side(style=\"thin\", color=\"999999\")\nTOPLINE = Border(top=Side(style=\"thin\", color=\"000000\"))\n\nLA = CFG[\"last_actual_year\"]; HIST = list(range(LA - 4, LA + 1)); PROJ = list(range(LA + 1, LA + 11)); YEARS = HIST + PROJ\ndef col(y): return 3 + (y - HIST[0])\ndef cl(y): return L(col(y))\nFIRST_P, LAST_P = cl(2026), cl(2035)     # H, Q\nPREV = {y: cl(y - 1) for y in YEARS if y > 2021}\n\nNUM = '#,##0;(#,##0);\"-\"'\nNUM1 = '#,##0.0;(#,##0.0);\"-\"'\nNUM2 = '#,##0.00;(#,##0.00);\"-\"'\nPCT = '0.0%;(0.0%);\"-\"'\nMULT = '0.0\"x\"'"],[648,"# ---------------------------------------------------------------------------\n# DCF\n# ---------------------------------------------------------------------------\ndc = scaffold(\"DCF\", \"Unlevered DCF — stub period from last balance sheet, mid-year convention, dual terminal value, equity bridge\")\nDC = {}\nrr = 5\nDC[\"ebit\"] = prow(dc, rr, \"EBIT\", proj=lambda y: f\"=Drivers!{cl(y)}{DR['ebit']}\"); rr += 1\nDC[\"taxes\"] = prow(dc, rr, \"Less: cash taxes on EBIT\", proj=lambda y: f\"=-MAX(0,{cl(y)}{DC['ebit']})*Drivers!{cl(y)}{DR['tax']}\"); rr += 1\nDC[\"nopat\"] = prow(dc, rr, \"NOPAT\", proj=lambda y: f\"={cl(y)}{DC['ebit']}+{cl(y)}{DC['taxes']}\", bold=True, top=True); rr += 1\nDC[\"da\"] = prow(dc, rr, \"Plus: depreciation & amortisation\", proj=lambda y: f\"=Drivers!{cl(y)}{DR['da']}\"); rr += 1\nDC[\"sbc\"] = prow(dc, rr, \"Plus: SBC add-back (× toggle; default 0 = SBC is a real cost)\", proj=lambda y: f\"=Drivers!{cl(y)}{DR['sbc']}*{I(ROW_SBCT)}\"); rr += 1\nDC[\"capex\"] = prow(dc, rr, \"Less: capital expenditure\", proj=lambda y: f\"=-Drivers!{cl(y)}{DR['capex']}\"); rr += 1\nDC[\"nwc\"] = prow(dc, rr, \"Less: increase in net working capital\", proj=lambda y: f\"=-BS!{cl(y)}{BS['dnwc']}\"); rr += 1\nDC[\"ufcf\"] = prow(dc, rr, \"Unlevered free cash flow (full year)\", proj=lambda y: f\"=SUM({cl(y)}{DC['nopat']}:{cl(y)}{DC['nwc']})\", bold=True, top=True); rr += 1\nDC[\"ufcf_m\"] = prow(dc, rr, \"   UFCF margin\", proj=lambda y: f\"={cl(y)}{DC['ufcf']}/Drivers!{cl(y)}{DR['rev']}\", fmt=PCT); rr += 2\nDC[\"stub\"] = rr; label(dc, rr, \"Fraction of year remaining after last balance sheet date\")\nfor y in PROJ:\n    f = f\"=({I(ROW_FYE)}-{I(ROW_BSDATE)})/365\" if y == PROJ[0] else \"=1\"\n    c = dc.cell(rr, col(y), f); c.number_format = NUM2\nrr += 1\nDC[\"counted\"] = prow(dc, rr, \"UFCF counted (stub-adjusted)\", proj=lambda y: f\"={cl(y)}{DC['ufcf']}*{cl(y)}{DC['stub']}\"); rr += 1\nDC[\"period\"] = rr; label(dc, rr, \"Discount period (years from balance sheet date; mid-year aware)\")\nfor y in PROJ:\n    if y == PROJ[0]: f = f\"={cl(y)}{DC['stub']}*IF({I(ROW_MID)}=1,0.5,1)\"\n    else: f = f\"={cl(PROJ[0])}{DC['stub']}+({y}-{PROJ[0]})-IF({I(ROW_MID)}=1,0.5,0)\"\n    dc.cell(rr, col(y), f).number_format = NUM2\nrr += 1\nDC[\"df\"] = prow(dc, rr, \"Discount factor\", proj=lambda y: f\"=1/(1+{WACC})^{cl(y)}{DC['period']}\", fmt='0.0000'); rr += 1\nDC[\"pv\"] = prow(dc, rr, \"Present value of UFCF\", proj=lambda y: f\"={cl(y)}{DC['counted']}*{cl(y)}{DC['df']}\", bold=True); rr += 2\n\n# valuation block (column C values)\ndef vrow(lab, formula, fmt=NUM, bold=False, fill=None, comment=None):\n    global rr\n    label(dc, rr, lab, bold=bold)\n    c = dc.cell(rr, 3, formula); c.number_format = fmt; c.font = BOLD if bold else BLACK\n    if fill: c.fill = fill\n    if comment: note(dc, rr, 3, comment)\n    DC[lab] = rr; rr += 1; return rr - 1\nN = cl(PROJ[9])\nr_sumpv = vrow(\"Sum of PV of explicit-period UFCF\", f\"=SUM({FIRST_P}{DC['pv']}:{LAST_P}{DC['pv']})\")\nr_wacc = vrow(\"WACC\", f\"={WACC}\", PCT)\nr_g = vrow(\"Terminal growth rate\", f\"={I(ROW_G)}\", PCT)\nr_tvg = vrow(f\"Terminal value — Gordon growth: UFCF({PROJ[9]}) × (1+g) / (WACC − g)\", f\"={N}{DC['ufcf']}*(1+C{r_g})/(C{r_wacc}-C{r_g})\")\nr_tvx = vrow(f\"Terminal value — exit multiple: EBIT({PROJ[9]}) × EV/EBIT\", f\"={N}{DC['ebit']}*{I(ROW_EXIT)}\")\nr_tv = vrow(\"Terminal value selected (1 Gordon / 2 Exit / 3 average)\", f\"=CHOOSE({I(ROW_TVM)},C{r_tvg},C{r_tvx},AVERAGE(C{r_tvg},C{r_tvx}))\", bold=True)\nr_ptv = vrow(\"Discount period for terminal value (Gordon at mid-year N if mid-year; exit at end of N)\",\n             f\"=IF({I(ROW_TVM)}=2,{FIRST_P}{DC['stub']}+9,{N}{DC['period']})\", NUM2,\n             comment=\"Under the mid-year convention a perpetuity-growth TV is discounted from the terminal year's mid-point; an exit-multiple TV (based on a year-end metric) from year-end.\")\nr_pvtv = vrow(\"PV of terminal value\", f\"=C{r_tv}/(1+C{r_wacc})^C{r_ptv}\")\nr_ev = vrow(\"Enterprise value (€m)\", f\"=C{r_sumpv}+C{r_pvtv}\", bold=True, fill=LIGHT)\nr_tvpct = vrow(\"   Terminal value as % of EV\", f\"=C{r_pvtv}/C{r_ev}\", PCT)\nrr += 1\nlabel(dc, rr, \"Equity bridge (latest balance sheet, 30 Jun 2026)\", bold=True); rr += 1"],[728,"             comment=\"Spotify has negative working capital and capex below D&A, so growth releases cash; the classic g = ROIC × reinvestment check is not binding.\")\nrr += 1\nlabel(dc, rr, \"Market-implied expectations (what today's price assumes, holding the explicit forecast)\", bold=True); rr += 1\nr_mev = vrow(\"Market enterprise value (€m) = market cap − net bridge\", f\"={I(ROW_MCAP)}/C{r_roll}-C{r_net}\")\nr_mtv = vrow(\"Terminal value required to justify the price (€m)\", f\"=(C{r_mev}-C{r_sumpv})*(1+C{r_wacc})^C{r_ptv}\")\nr_mg = vrow(\"Market-implied perpetual growth rate\", f\"=(C{r_mtv}*C{r_wacc}-{N}{DC['ufcf']})/(C{r_mtv}+{N}{DC['ufcf']})\", PCT, bold=True, fill=LIGHT)\nr_mx = vrow(f\"Market-implied exit EV/EBIT ({PROJ[9]})\", f\"=C{r_mtv}/{N}{DC['ebit']}\", MULT, bold=True, fill=LIGHT)\nrr += 1"],[794,"# Grid 3: reverse DCF — revenue CAGR × terminal EBIT margin (simplified closed-form engine)\ntop = nxt + 2\nse.cell(top, 1, \"Reverse DCF: implied value (US$) for revenue CAGR FY2025–35 × FY2035 EBIT margin; margin fades linearly from FY2025A\").font = BOLD\nse.cell(top + 1, 1, \"Engine: UFCF_t = Rev_2025×(1+CAGR)^t × [margin_t×(1−tax) + (D&A − capex)% ]; NWC change ignored; same WACC, g, stub and bridge as the DCF tab.\").font = Font(name=FONT, italic=True, size=9)\nhr = top + 2\nse.cell(hr, 1, \"helper: t\"); se.cell(hr + 1, 1, \"helper: stub\"); se.cell(hr + 2, 1, \"helper: period\"); se.cell(hr + 3, 1, \"Rev FY2025A (€m)\"); se.cell(hr + 4, 1, \"EBIT margin FY2025A\"); se.cell(hr + 5, 1, \"Tax rate\"); se.cell(hr + 6, 1, \"(D&A − capex) % rev\")\nfor i, y in enumerate(PROJ):\n    se.cell(hr, 2 + i, i + 1)\n    se.cell(hr + 1, 2 + i, f\"=DCF!{cl(y)}{DC['stub']}\").number_format = NUM2\n    se.cell(hr + 2, 2 + i, f\"=DCF!{cl(y)}{DC['period']}\").number_format = NUM2\nse.cell(hr + 3, 2, f\"=Drivers!{cl(HIST[-1])}{DR['rev']}\").number_format = NUM\nse.cell(hr + 4, 2, f\"=Drivers!{cl(HIST[-1])}{DR['ebit_m']}\").number_format = PCT\nse.cell(hr + 5, 2, f\"=Drivers!{cl(PROJ[0])}{DR['tax']}\").number_format = PCT\nse.cell(hr + 6, 2, f\"=(Drivers!{cl(PROJ[0])}{DR['da']}-Drivers!{cl(PROJ[0])}{DR['capex']})/Drivers!{cl(PROJ[0])}{DR['rev']}\").number_format = '0.00%'\nT_ = f\"$B${hr}:$K${hr}\"; ST_ = f\"$B${hr+1}:$K${hr+1}\"; PD_ = f\"$B${hr+2}:$K${hr+2}\"\nR0 = f\"$B${hr+3}\"; M0 = f\"$B${hr+4}\"; TX = f\"$B${hr+5}\"; NR = f\"$B${hr+6}\"\nWW = f\"DCF!$C${r_wacc}\"; GG = f\"DCF!$C${r_g}\"\ndef f_rev(cg, m):\n    pv = f\"SUMPRODUCT({R0}*(1+{cg})^{T_}*(({M0}+({m}-{M0})*{T_}/10)*(1-{TX})+{NR})*{ST_}*(1+{WW})^(-{PD_}))\"\n    uN = f\"({R0}*(1+{cg})^10*({m}*(1-{TX})+{NR}))\"\n    tv = f\"{uN}*(1+{GG})/({WW}-{GG})/(1+{WW})^{PTV_G}\"\n    return f\"=(({pv}+{tv}+{NETB})*{ROLLF})/{SH}*{FX}\"\ncagrs = [0.08, 0.10, 0.12, 0.14, 0.16]\nmargins = [0.16, 0.20, 0.24, 0.28, 0.32]\ngtop = hr + 8\nnxt = grid(gtop, \"Implied value per share (US$): revenue CAGR × FY2035 EBIT margin\", cagrs, margins, f_rev, PCT, PCT, \"Rev CAGR\", \"EBIT margin 2035\")\nfrom openpyxl.formatting.rule import CellIsRule\nGREENF = PatternFill(\"solid\", fgColor=\"C6EFCE\"); REDF = PatternFill(\"solid\", fgColor=\"FFC7CE\")\nfor t0, n_rows in [(3, 5), (12, 5), (gtop, 5)]:\n    rng = f\"B{t0+2}:F{t0+1+n_rows}\"\n    se.conditional_formatting.add(rng, CellIsRule(operator=\"greaterThanOrEqual\", formula=[I(ROW_PRICE)], fill=GREENF))\n    se.conditional_formatting.add(rng, CellIsRule(operator=\"lessThan\", formula=[I(ROW_PRICE)], fill=REDF))\nse.cell(nxt + 1, 1, \"Green = above current price, red = below. Grids 1-2 use the full three-statement UFCF; grid 3 is a simplified engine for reading market-implied growth/margin combinations.\").font = Font(name=FONT, italic=True, size=9)"],[948,"# ---------------------------------------------------------------------------\n# CHECKS\n# ---------------------------------------------------------------------------\nck = wb.create_sheet(\"Checks\")\nsetw(ck, {\"A\": 70, \"B\": 16, \"C\": 14, \"D\": 50})\nck[\"A1\"] = \"Model integrity checks — all must read OK before the file ships\"; ck[\"A1\"].font = TITLE\nfor j, h in enumerate([\"Check\", \"Value\", \"Status\", \"Rule\"]): c = ck.cell(3, 1 + j, h); c.font = BOLD; c.fill = GREY\nchecks = [\n    (\"Balance sheet balances every projected year (max |assets − L&E|)\", f\"=MAX(ABS(BS!{cl(HIST[-1])}{BS['chk']}),{','.join(f'ABS(BS!{cl(y)}{BS['chk']})' for y in PROJ)})\", NUM2, \"=IF(B{r}<0.5,\\\"OK\\\",\\\"FAIL\\\")\", \"< 0.5\"),\n    (\"Cash & ST investments never negative (min across years)\", f\"=MIN(BS!{FIRST_P}{BS['cash']}:BS!{LAST_P}{BS['cash']})\", NUM, \"=IF(B{r}>=0,\\\"OK\\\",\\\"FAIL\\\")\", \">= 0\"),\n    (\"Segment revenue sums to total (max abs diff)\", f\"=MAX({','.join(f'ABS(Drivers!{cl(y)}{DR['prem_rev']}+Drivers!{cl(y)}{DR['ad_rev']}-Drivers!{cl(y)}{DR['rev']})' for y in PROJ)})\", NUM2, \"=IF(B{r}<0.01,\\\"OK\\\",\\\"FAIL\\\")\", \"= 0\"),\n    (\"WACC exceeds terminal growth\", f\"={WACC}-{I(ROW_G)}\", PCT, \"=IF(B{r}>0.02,\\\"OK\\\",\\\"FAIL\\\")\", \"> 2 pts\"),\n    (\"Terminal value share of EV\", f\"=DCF!C{r_tvpct}\", PCT, \"=IF(AND(B{r}>0.4,B{r}<0.85),\\\"OK\\\",\\\"CHECK\\\")\", \"40-85% typical for a growth company\"),\n    (\"Implied exit EV/EBIT from Gordon TV\", f\"=DCF!C{r_im4}\", MULT, \"=IF(AND(B{r}>8,B{r}<40),\\\"OK\\\",\\\"CHECK\\\")\", \"8-40x plausible\"),\n    (\"Scenario selector within 1-4\", f\"={SCEN}\", None, \"=IF(AND(B{r}>=1,B{r}<=4),\\\"OK\\\",\\\"FAIL\\\")\", \"1-4\"),\n    (\"Diluted share count (m)\", f\"={I(ROW_DIL)}\", NUM2, \"=IF(AND(B{r}>200,B{r}<215),\\\"OK\\\",\\\"CHECK\\\")\", \"205.6m basic + TSM dilution\"),\n    (\"Revenue CAGR FY2025-30 vs Investor Day mid-teens\", f\"=Drivers!C{DR['cagr']}\", PCT, \"=IF(AND(B{r}>0.08,B{r}<0.20),\\\"OK\\\",\\\"CHECK\\\")\", \"info\"),\n    (\"FY2030 operating margin vs >20% target\", f\"=Drivers!C{DR['om2030']}\", PCT, \"=IF(B{r}>0.10,\\\"OK\\\",\\\"CHECK\\\")\", \"info\"),\n    (\"FY2026E EPS within consensus low-high range\", f\"=IS!{cl(PROJ[0])}{IS['eps']}\", NUM2, \"=IF(AND(B{r}>=Consensus!D7*0.9,B{r}<=Consensus!E7*1.1),\\\"OK\\\",\\\"CHECK\\\")\", \"±10% of Yahoo low/high\"),\n    (\"Sum of explicit PV positive\", f\"=DCF!C{r_sumpv}\", NUM, \"=IF(B{r}>0,\\\"OK\\\",\\\"FAIL\\\")\", \"> 0\"),\n    (\"CF closing cash equals BS cash (identity; max abs diff)\", f\"=MAX({','.join(f'ABS(CF!{cl(y)}{CF['end']}-BS!{cl(y)}{BS['cash']})' for y in PROJ)})\", NUM2, \"=IF(B{r}<0.01,\\\"OK\\\",\\\"FAIL\\\")\", \"= 0\"),\n    (\"CF starting line equals IS net income (max abs diff)\", f\"=MAX({','.join(f'ABS(CF!{cl(y)}{CF['ni']}-IS!{cl(y)}{IS['ni']})' for y in PROJ)})\", NUM2, \"=IF(B{r}<0.01,\\\"OK\\\",\\\"FAIL\\\")\", \"= 0\"),\n    (\"Equity roll-forward ties: prior + NI + SBC + options − buybacks − RSU tax (max abs diff)\", f\"=MAX({','.join(f'ABS(BS!{cl(y)}{BS['eq']}-(BS!{PREV[y]}{BS['eq']}+IS!{cl(y)}{IS['ni']}+Drivers!{cl(y)}{DR['sbc']}+{I(WC['Option exercise proceeds per year (€m)'])}-Drivers!{cl(y)}{DR['bb']}-{I(WC['RSU tax withholding payments per year (€m)'])}))' for y in PROJ)})\", NUM2, \"=IF(B{r}<0.01,\\\"OK\\\",\\\"FAIL\\\")\", \"= 0\"),\n    (\"Debt schedule closing balances tie to balance sheet (max abs diff)\", f\"=MAX({','.join(f'ABS(Debt!{cl(y)}{DB['tot']}-BS!{cl(y)}{BS['notes']}-BS!{cl(y)}{BS['lease']})' for y in PROJ)})\", NUM2, \"=IF(B{r}<0.01,\\\"OK\\\",\\\"FAIL\\\")\", \"= 0\"),\n    (\"Sensitivity grid centre equals DCF price (Gordon, active WACC/g)\", f\"=ABS(Sensitivity!D7-DCF!C{r_pxu})\", NUM2, \"=IF(OR(B{r}<0.01,\" + I(ROW_TVM) + \"<>1),\\\"OK\\\",\\\"FAIL\\\")\", \"0 when method = Gordon\"),\n]\nrr = 4\nfor lab, f, fmt, st, rule in checks:\n    ck.cell(rr, 1, lab); c = ck.cell(rr, 2, f)\n    if fmt: c.number_format = fmt\n    s = ck.cell(rr, 3, st.replace(\"{r}\", str(rr))); s.font = BOLD\n    ck.cell(rr, 4, rule).font = Font(name=FONT, size=9, color=\"666666\")\n    rr += 1\nck.cell(rr + 1, 1, \"Overall status\").font = BOLD\nc = ck.cell(rr + 1, 3, f\"=IF(COUNTIF(C4:C{rr-1},\\\"FAIL\\\")=0,IF(COUNTIF(C4:C{rr-1},\\\"CHECK\\\")=0,\\\"ALL OK\\\",\\\"REVIEW CHECK ITEMS\\\"),\\\"FAIL\\\")\"); c.font = BOLD; c.fill = YELLOW\nCHK_ROW = rr + 1\nfrom openpyxl.formatting.rule import CellIsRule as CIR\nck.conditional_formatting.add(f\"C4:C{rr+1}\", CIR(operator=\"equal\", formula=['\"FAIL\"'], fill=REDF))\nck.conditional_formatting.add(f\"C4:C{rr+1}\", CIR(operator=\"equal\", formula=['\"CHECK\"'], fill=PatternFill(\"solid\", fgColor=\"FFEB9C\")))\nck.conditional_formatting.add(f\"C4:C{rr+1}\", CIR(operator=\"equal\", formula=['\"OK\"'], fill=GREENF))\n"]]},"skills/stock-diligence-report/scripts/technicals.py":{"l":"py","n":51,"s":[[1,"#!/usr/bin/env python3\n\"\"\"Technical positioning vs the stock's own history.\nUsage: python3 technicals.py TICKER [--years 10] [--out outdir]\nOutputs: technicals.csv (current values, historical mean/sd/z/percentile), bollinger.png, momentum.png\nMetrics: 1/3/6/12M returns with z-scores vs the rolling distribution; Bollinger (20,2) daily %B and bandwidth plus weekly (20,2);\ndistance to 50/200 DMA (z-scored) and cross dates; RSI(14); 12-1 momentum; 30d/90d realised vol vs history; monthly MACD(12,26,9) regime.\"\"\"\nimport argparse, os, sys, numpy as np, pandas as pd\n\ndef zscore(series, current):\n    s = series.dropna(); return (current - s.mean()) / s.std(), (s < current).mean()\n\ndef rsi(close, n=14):\n    d = close.diff(); up = d.clip(lower=0).ewm(alpha=1/n, adjust=False).mean(); dn = (-d.clip(upper=0)).ewm(alpha=1/n, adjust=False).mean()\n    return 100 - 100 / (1 + up / dn)\n\ndef main():\n    ap = argparse.ArgumentParser(); ap.add_argument(\"ticker\"); ap.add_argument(\"--years\", type=int, default=10); ap.add_argument(\"--out\", default=\".\")\n    a = ap.parse_args(); os.makedirs(a.out, exist_ok=True)\n    import yfinance as yf\n    h = yf.Ticker(a.ticker).history(period=f\"{a.years}y\", auto_adjust=True); h.index = h.index.tz_localize(None); c = h[\"Close\"]\n    rows = []\n    for lab, n in [(\"1M\", 21), (\"3M\", 63), (\"6M\", 126), (\"12M\", 252)]:\n        hist = c.pct_change(n); cur = hist.iloc[-1]; z, p = zscore(hist, cur); rows.append([f\"{lab} return\", cur, hist.mean(), hist.std(), z, p])\n    ma20 = c.rolling(20).mean(); sd20 = c.rolling(20).std(); pb = (c - (ma20 - 2 * sd20)) / (4 * sd20); bw = 4 * sd20 / ma20\n    rows.append([\"Bollinger %B (20,2) daily\", pb.iloc[-1], pb.mean(), pb.std(), *zscore(pb, pb.iloc[-1])])\n    rows.append([\"Bollinger bandwidth daily\", bw.iloc[-1], bw.mean(), bw.std(), *zscore(bw, bw.iloc[-1])])\n    w = c.resample(\"W-FRI\").last(); mw = w.rolling(20).mean(); sw = w.rolling(20).std(); pbw = (w - (mw - 2 * sw)) / (4 * sw)\n    rows.append([\"Bollinger %B (20,2) weekly\", pbw.iloc[-1], pbw.mean(), pbw.std(), *zscore(pbw, pbw.iloc[-1])])\n    for n in (50, 200):\n        dist = c / c.rolling(n).mean() - 1; rows.append([f\"Distance to {n}DMA\", dist.iloc[-1], dist.mean(), dist.std(), *zscore(dist, dist.iloc[-1])])\n    gc = (c.rolling(50).mean() > c.rolling(200).mean()).astype(int).diff(); crosses = gc[gc != 0].dropna().tail(4)\n    r14 = rsi(c); rows.append([\"RSI(14)\", r14.iloc[-1], r14.mean(), r14.std(), *zscore(r14, r14.iloc[-1])])\n    mom = c.shift(21) / c.shift(252) - 1; rows.append([\"12-1 momentum\", mom.iloc[-1], mom.mean(), mom.std(), *zscore(mom, mom.iloc[-1])])\n    lr = np.log(c).diff()\n    for n in (30, 90):\n        rv = lr.rolling(n).std() * np.sqrt(252); rows.append([f\"Realised vol {n}d\", rv.iloc[-1], rv.mean(), rv.std(), *zscore(rv, rv.iloc[-1])])\n    m = c.resample(\"ME\").last(); macd = m.ewm(span=12, adjust=False).mean() - m.ewm(span=26, adjust=False).mean(); sig = macd.ewm(span=9, adjust=False).mean()\n    regime = \"bullish (MACD above signal)\" if macd.iloc[-1] > sig.iloc[-1] else \"bearish (MACD below signal)\"\n    df = pd.DataFrame(rows, columns=[\"metric\", \"current\", \"hist_mean\", \"hist_sd\", \"z\", \"percentile\"]); df.to_csv(os.path.join(a.out, \"technicals.csv\"), index=False)\n    print(df.round(3).to_string()); print(\"\\nMonthly MACD regime:\", regime, f\"(MACD {macd.iloc[-1]:.2f} vs signal {sig.iloc[-1]:.2f})\"); print(\"Recent 50/200 crosses:\", [(d.date(), \"golden\" if v > 0 else \"death\") for d, v in crosses.items()])\n    try:\n        import matplotlib; matplotlib.use(\"Agg\"); import matplotlib.pyplot as plt\n        t = c.tail(252); fig, ax = plt.subplots(figsize=(12, 5)); ax.plot(t.index, t, color=\"#1F4E79\", lw=1.2)\n        ax.plot(t.index, ma20.tail(252), color=\"grey\", lw=.8); ax.fill_between(t.index, (ma20 - 2 * sd20).tail(252), (ma20 + 2 * sd20).tail(252), alpha=.15, color=\"#1F4E79\")\n        ax.plot(t.index, c.rolling(50).mean().tail(252), color=\"orange\", lw=.9, label=\"50DMA\"); ax.plot(t.index, c.rolling(200).mean().tail(252), color=\"red\", lw=.9, label=\"200DMA\")"]]},"skills/stock-diligence-report/scripts/qa_gates.py":{"l":"py","n":46,"s":[[1,"#!/usr/bin/env python3\n\"\"\"Mechanical QA gates on the rendered report. Usage: python3 qa_gates.py REPORT.pdf --mode standard|extensive [--budget budgets.json]\nChecks: page count against the mode's budget (standard: target 50, warn outside 30-70, FAIL above 100; extensive: no cap),\nper-section page counts against the standard budgets, em dashes, \"Label: content\" strings (Claim:, Must be true:, Evidence:),\nunresolved placeholders ([NEED, research pending, verify), estimates without an Appendix G, landscape pages.\nExit code 1 on any FAIL; prints a table the orchestrator pastes into the manifest.\"\"\"\nimport argparse, json, re, subprocess, sys\nBUDGET = {\"0\": 3, \"1\": 9, \"2\": 2, \"3\": 3, \"4\": 8, \"5\": 3, \"6\": 2, \"7\": 6, \"8\": 2, \"9\": 4, \"10\": 5, \"11\": 3, \"12\": 4, \"A\": 6, \"B\": 1, \"C\": 1, \"D\": 1, \"E\": 1, \"F\": 1, \"G\": 3}\n\ndef page_text(pdf, i): return subprocess.run([\"pdftotext\", \"-f\", str(i), \"-l\", str(i), pdf, \"-\"], capture_output=True, text=True).stdout\n\ndef main():\n    ap = argparse.ArgumentParser(); ap.add_argument(\"pdf\"); ap.add_argument(\"--mode\", default=\"standard\"); ap.add_argument(\"--budget\", default=\"\"); a = ap.parse_args()\n    budget = json.load(open(a.budget)) if a.budget else BUDGET\n    n = int(re.search(r\"Pages:\\s+(\\d+)\", subprocess.run([\"pdfinfo\", a.pdf], capture_output=True, text=True).stdout).group(1))\n    full = subprocess.run([\"pdftotext\", a.pdf, \"-\"], capture_output=True, text=True).stdout\n    fails, warns = [], []\n    if a.mode == \"standard\":\n        if n > 100: fails.append(f\"page count {n} above the hard cap of 100 (standard mode)\")\n        elif n < 30 or n > 70: warns.append(f\"page count {n} outside the 30-70 target band\")\n    starts = {}\n    for i in range(1, n + 1):\n        for line in page_text(a.pdf, i).splitlines()[:6]:\n            m = re.match(r\"^(\\d{1,2})\\. [A-Z]|^Appendix ([A-H])\\. \", line.strip())\n            if m:\n                key = m.group(1) or m.group(2)\n                if key not in starts: starts[key] = i\n    keys = list(starts.keys()); sizes = {k: (starts[keys[j + 1]] if j + 1 < len(keys) else n + 1) - starts[k] for j, k in enumerate(keys)}\n    if a.mode == \"standard\":\n        for k, sz in sizes.items():\n            if k in budget and sz > budget[k] * 1.5: warns.append(f\"section {k}: {sz} pages against a budget of {budget[k]}\")\n    if \"\\u2014\" in full: fails.append(f\"em dashes present ({full.count(chr(8212))})\")\n    for pat in [\"Claim:\", \"Must be true:\", \"Evidence:\", \"Deciding question:\"]:\n        if pat in full: fails.append(f\"label-colon construction present: {pat}\")\n    for pat in [\"[NEED\", \"research pending\", \"Research pending\"]:\n        if pat in full: warns.append(f\"placeholder present: {pat} ({full.count(pat)})\")\n    if \"(est.)\" in full and \"Appendix G\" not in full: fails.append(\"estimates marked (est.) but no Appendix G\")\n    landscape = [i for i in range(1, n + 1) if re.search(r\"Page size:\\s+(\\d+) x (\\d+)\", subprocess.run([\"pdfinfo\", \"-f\", str(i), \"-l\", str(i), a.pdf], capture_output=True, text=True).stdout or \"\") and (lambda m: float(m.group(1)) > float(m.group(2)))(re.search(r\"Page size:\\s+([\\d.]+) x ([\\d.]+)\", subprocess.run([\"pdfinfo\", \"-f\", str(i), \"-l\", str(i), a.pdf], capture_output=True, text=True).stdout))] if n <= 250 else []\n    if landscape: fails.append(f\"landscape pages: {landscape[:10]}\")\n    print(f\"pages: {n} (mode {a.mode})\"); print(\"sections:\", {k: sizes[k] for k in keys})\n    for w in warns: print(\"WARN\", w)\n    for f in fails: print(\"FAIL\", f)\n    if not fails and not warns: print(\"OK\")\n    sys.exit(1 if fails else 0)\n\nif __name__ == \"__main__\": main()"]]},"skills/stock-diligence-report/scripts/assemble_report.py":{"l":"py","n":28,"s":[[1,"#!/usr/bin/env python3\n\"\"\"Assemble the report spec from the exhibit file and the research files, in the order fixed by the skeleton.\nUsage: python3 assemble_report.py --skeleton report_skeleton.json --exhibits work/exhibits.json --research work/research\n       --title \"Company (TICKER)\" --subtitle \"Stock diligence report\" --meta \"...\" --header \"...\" --out work/report_spec.json\nSkeleton: [{\"h1\": \"0. Tear sheet\", \"items\": [\"exhibit:tear_market\", \"research:0\", \"exhibit:scenario_ladder\"]}, ...]\nResearch: every work/research/*.json is {\"<section key>\": [blocks...]} in the renderer schema; keys are merged across files\n(the orchestrator's own prose lives in work/research/orchestrator.json). Missing items are skipped and listed.\"\"\"\nimport argparse, glob, json, os\n\ndef main():\n    ap = argparse.ArgumentParser()\n    for k in [\"skeleton\", \"exhibits\", \"research\", \"title\", \"out\"]: ap.add_argument(f\"--{k}\", required=True)\n    ap.add_argument(\"--subtitle\", default=\"Stock diligence report\"); ap.add_argument(\"--meta\", default=\"\"); ap.add_argument(\"--header\", default=\"\")\n    a = ap.parse_args(); sk = json.load(open(a.skeleton)); ex = json.load(open(a.exhibits)); R = {}\n    for f in sorted(glob.glob(os.path.join(a.research, \"*.json\"))):\n        for k, v in json.load(open(f)).items(): R.setdefault(k, []).extend(v)\n    blocks = [{\"toc\": True}]; missing = []\n    for sec in sk:\n        blocks.append({\"h1\": sec[\"h1\"]})\n        for item in sec.get(\"items\", []):\n            kind, key = item.split(\":\", 1)\n            src = ex if kind == \"exhibit\" else R\n            if key in src: blocks.extend(src[key])\n            else: missing.append(item)\n    spec = {\"title\": a.title, \"subtitle\": a.subtitle, \"meta\": a.meta, \"header\": a.header, \"h1PageBreak\": True, \"blocks\": blocks}\n    json.dump(spec, open(a.out, \"w\")); print(\"blocks\", len(blocks), \"missing\", missing)\n\nif __name__ == \"__main__\": main()"]]},"skills/stock-diligence-report/scripts/report_renderer.js":{"l":"js","n":129,"s":[[1,"// Report renderer v2: JSON spec -> Word. Portrait only. Enforces table-design rules:\n// text left / numbers right, headers aligned with data, horizontal rules only, row banding, exhibit numbering,\n// max 6 numeric columns per table (auto-split), key-value blocks for facts, cards for records, numbered narratives.\n// Usage: node report_renderer.js spec.json out.docx\n// Blocks: {h1|h2|h3}, {p: string|[runs]}, {bullets:[...]}, {numbered:[...]}, {kv:{title,rows:[[k,v]],note}},\n//   {data:{title,header:[...],rows:[[label,v1..]],note,bold_rows:[],decimals?}}, {text:{title,header,rows,widths,note}},\n//   {cards:[{title,rows:[[k,v]]}]}, {image:{path,width,height,title,note}}, {callout: string|[runs]}, {toc:true}, {pagebreak:true}\nconst fs = require(\"fs\");\nconst { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, WidthType, AlignmentType, BorderStyle, ShadingType,\n        HeadingLevel, LevelFormat, PageNumber, Footer, Header, ImageRun, TableOfContents, PageBreak, VerticalAlign, TableLayoutType } = require(\"docx\");\nconst [,, specPath, outPath] = process.argv;\nconst spec = JSON.parse(fs.readFileSync(specPath, \"utf8\"));\nconst FONT = \"Arial\", BODY = 20, NAVY = \"1F4E79\", GREY = \"595959\", BAND = \"F2F5F9\", RULE = \"BFBFBF\";\nconst TW = 9360; // 6.5in text width\nlet EX = 0; const exNo = () => ++EX;\n\nconst run = (r, size) => new TextRun({ text: r.text, font: FONT, size: r.size || size || BODY, bold: !!r.bold, italics: !!r.italics, color: r.color || \"000000\" });\nconst runs = (t, size) => (Array.isArray(t) ? t : [{ text: String(t ?? \"\") }]).map((r) => run(r, size));\nconst P = (t, o = {}) => new Paragraph({ alignment: o.align || AlignmentType.LEFT, spacing: { before: o.before || 0, after: o.after ?? 120, line: 264 }, keepNext: !!o.keepNext, keepLines: true, children: runs(t, o.size) });\nfunction H(text, lvl, first) {\n  const sz = { 1: 30, 2: 24, 3: 21 }[lvl]; const hl = { 1: HeadingLevel.HEADING_1, 2: HeadingLevel.HEADING_2, 3: HeadingLevel.HEADING_3 }[lvl];\n  return new Paragraph({ heading: hl, spacing: { before: lvl === 1 ? 360 : (lvl === 2 ? 260 : 200), after: 120 }, keepNext: true, pageBreakBefore: lvl === 1 && spec.h1PageBreak !== false && !first,\n    children: [new TextRun({ text, font: FONT, size: sz, bold: true, color: NAVY })] });\n}\nconst bullet = (t) => new Paragraph({ numbering: { reference: \"bul\", level: 0 }, spacing: { after: 60, line: 259 }, children: runs(t) });\nlet NUMREF = 0; const numbered = (t, ref) => new Paragraph({ numbering: { reference: ref, level: 0 }, spacing: { after: 80, line: 259 }, children: runs(t) });\nconst exTitle = (title) => new Paragraph({ spacing: { before: 160, after: 60 }, keepNext: true, children: [new TextRun({ text: `Exhibit ${exNo()}. ${title}`, font: FONT, size: 19, bold: true, color: NAVY })] });\nconst noteP = (note) => note ? P([{ text: note, italics: true, color: GREY }], { size: 16, before: 40, after: 200 }) : new Paragraph({ spacing: { after: 160 }, children: [] });\nconst isNum = (t) => /^[\\s$€£(+\\-–−]*[\\d.,]+\\s*(%|x|bn|m|k|pts|p)?\\)?$|^n\\/[amd]$|^n\\/m$|^[+\\-–−]$|^[+\\-]?[\\d.,]+%?\\s*\\/\\s*[+\\-]?[\\d.,]+%?$/.test(String(t ?? \"\").trim());\nconst hb = { style: BorderStyle.SINGLE, size: 4, color: RULE }; const none = { style: BorderStyle.NONE, size: 0, color: \"FFFFFF\" };\nconst rowBorders = { top: none, bottom: hb, left: none, right: none };\n\nfunction cell(text, width, o = {}) {\n  const lines = String(text ?? \"\").split(\"\\n\");\n  return new TableCell({ width: { size: width, type: WidthType.DXA }, borders: o.borders || rowBorders, verticalAlign: VerticalAlign.CENTER,\n    margins: { top: 50, bottom: 50, left: 80, right: 80 }, shading: o.fill ? { type: ShadingType.CLEAR, color: \"auto\", fill: o.fill } : undefined,\n    children: lines.map((ln) => new Paragraph({ alignment: o.align || AlignmentType.LEFT, spacing: { after: 0, line: 240 },\n      children: [new TextRun({ text: ln, font: FONT, size: o.size || 18, bold: !!o.bold, color: o.color || \"000000\", italics: !!o.italics })] })) });\n}\nfunction mkTable(widths, rows) {\n  return new Table({ width: { size: widths.reduce((a, b) => a + b, 0), type: WidthType.DXA }, columnWidths: widths, layout: TableLayoutType.FIXED, rows });\n}"]]},"skills/stock-diligence-report/scripts/flow_diagram.py":{"l":"py","n":53,"s":[[1,"#!/usr/bin/env python3\n\"\"\"Money-flow diagram with a deterministic, collision-free layout.\n\nUsage: python3 flow_diagram.py spec.json out.png\nSpec: {\"title\": \"...\", \"footnote\": \"...\",\n       \"center\": {\"label\": \"Spotify (DSP)\", \"sub\": \"revenue €17.2bn FY2025\\\\ngross margin 32%\"},\n       \"left\":  [{\"label\": \"Consumers\", \"sub\": \"300m subscribers\\\\n494m ad-supported users\", \"flow\": \"Subscriptions €15.4bn\"}, ...],\n       \"right\": [{\"label\": \"Record labels\", \"sub\": \"UMG, Sony, WMG, Merlin\", \"flow\": \"Recording royalties\", \"back\": \"Marketplace fees (Marquee, Showcase)\"}, ...]}\nRules the layout enforces: three columns (payers, company, payees); one arrow per flow; every arrow label sits in its own\nwhite box on the arrow's own lane, never over a node; return flows use a separate lane below the forward flow; text wraps\ninside nodes at a fixed width; nothing is placed by hand. Tested on SPOT (see references/presentation-standards.md).\n\"\"\"\nimport json, sys, textwrap\nimport matplotlib; matplotlib.use(\"Agg\")\nimport matplotlib.pyplot as plt\nfrom matplotlib.patches import FancyBboxPatch, FancyArrowPatch\n\nNAVY, LIGHT, GREY = \"#1F4E79\", \"#EAF1FB\", \"#595959\"\n\ndef node(ax, x, y, w, h, label, sub, fill=LIGHT, size=11):\n    ax.add_patch(FancyBboxPatch((x - w / 2, y - h / 2), w, h, boxstyle=\"round,pad=0.02,rounding_size=0.03\", fc=fill, ec=NAVY, lw=1.4))\n    text = textwrap.fill(label, 22) + (\"\\n\" + \"\\n\".join(textwrap.fill(s, 26) for s in sub.split(\"\\n\")) if sub else \"\")\n    ax.text(x, y, text, ha=\"center\", va=\"center\", fontsize=size, color=\"black\", linespacing=1.35)\n\ndef arrow(ax, p, q, label, lane, color=NAVY):\n    \"\"\"Horizontal arrow from p to q; label wrapped to two lines, centred on the arrow, offset to its own lane.\"\"\"\n    ax.add_patch(FancyArrowPatch(p, q, arrowstyle=\"-|>\", mutation_scale=14, lw=1.4, color=color, shrinkA=1, shrinkB=1))\n    mx, my = (p[0] + q[0]) / 2, (p[1] + q[1]) / 2\n    ax.text(mx, my + lane, textwrap.fill(label, 26), ha=\"center\", va=\"bottom\" if lane > 0 else \"top\", fontsize=9, color=color, linespacing=1.2,\n            bbox=dict(boxstyle=\"round,pad=0.2\", fc=\"white\", ec=\"none\", alpha=0.98))\n\ndef main():\n    spec = json.load(open(sys.argv[1])); out = sys.argv[2]\n    left, right = spec[\"left\"], spec[\"right\"]; n = max(len(left), len(right)); H = 2.0 * n + 1.4\n    fig, ax = plt.subplots(figsize=(14, 1.6 + 2.0 * n)); ax.set_xlim(0, 14); ax.set_ylim(0, H); ax.axis(\"off\")\n    cy = H / 2 + 0.1\n    def ys(k): return [cy + (k - 1) * 1.0 - 2.0 * i for i in range(k)] if k > 1 else [cy]\n    span = max(len(left), len(right)); top, bot = ys(span)[0] + 0.75, ys(span)[-1] - 0.75\n    ax.add_patch(FancyBboxPatch((7.0 - 1.3, bot), 2.6, top - bot, boxstyle=\"round,pad=0.02,rounding_size=0.03\", fc=\"#D6E4F0\", ec=NAVY, lw=1.6))\n    ax.text(7.0, cy, textwrap.fill(spec[\"center\"][\"label\"], 20) + (\"\\n\" + spec[\"center\"].get(\"sub\", \"\") if spec[\"center\"].get(\"sub\") else \"\"), ha=\"center\", va=\"center\", fontsize=11.5, linespacing=1.4)\n    for i, item in enumerate(left):\n        y = ys(len(left))[i]; node(ax, 1.6, y, 2.6, 1.4, item[\"label\"], item.get(\"sub\", \"\"))\n        arrow(ax, (2.95, y), (5.65, y), item[\"flow\"], lane=0.16)\n    for i, item in enumerate(right):\n        y = ys(len(right))[i]; node(ax, 12.4, y, 2.6, 1.4, item[\"label\"], item.get(\"sub\", \"\"))\n        yf, yb = (y + 0.22, y - 0.22) if item.get(\"back\") else (y, y)\n        arrow(ax, (8.35, yf), (11.05, yf), item[\"flow\"], lane=0.16)\n        if item.get(\"back\"): arrow(ax, (11.05, yb), (8.35, yb), item[\"back\"], lane=-0.16, color=\"#C00000\")\n    if spec.get(\"title\"): ax.text(0.15, H - 0.05, spec[\"title\"], fontsize=12.5, fontweight=\"bold\", color=NAVY, va=\"top\")\n    if spec.get(\"footnote\"): ax.text(0.15, 0.1, textwrap.fill(spec[\"footnote\"], 170), fontsize=9, color=GREY, va=\"bottom\")\n    fig.savefig(out, dpi=170, bbox_inches=\"tight\"); print(\"written\", out)\n\nif __name__ == \"__main__\": main()"]]},"skills/stock-diligence-report/scripts/two_pager_builder.js":{"l":"js","n":53,"s":[[1,"// Internal two-pager. Usage: node two_pager_builder.js spec.json out.docx\n// Spec: {company, sector, ticker, price, date, price_target, analyst, overview, thesis:[{title, points:[...]}],\n//        metrics:[[label, value], ...], risks:[{title, points:[...]}], logo (optional PNG path), footer (optional text)}\n// Format is fixed (Garamond 11, 1.5 spacing, left-aligned): fill the pages with content, never by changing spacing.\nconst fs = require(\"fs\");\nconst { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, WidthType, AlignmentType, BorderStyle, Footer, LevelFormat, ImageRun } = require(\"docx\");\nconst [,, specPath, outPath] = process.argv; const S = JSON.parse(fs.readFileSync(specPath, \"utf8\"));\nconst FONT = \"Garamond\", SZ = 22, LINE = 336, BLUEC = \"2F5597\";\nconst none = { style: BorderStyle.NONE, size: 0, color: \"FFFFFF\" }; const noB = { top: none, bottom: none, left: none, right: none };\nconst rule = { style: BorderStyle.SINGLE, size: 6, color: \"000000\" };\nconst run = (t, o = {}) => new TextRun({ text: t, font: FONT, size: o.size || SZ, bold: !!o.bold, italics: !!o.italics, color: o.color || \"000000\" });\nconst P = (t, o = {}) => new Paragraph({ alignment: o.align || AlignmentType.LEFT, spacing: { before: o.before || 0, after: o.after ?? 60, line: LINE }, children: Array.isArray(t) ? t : [run(t, o)] });\nconst H = (t) => new Paragraph({ spacing: { before: 100, after: 40, line: LINE }, keepNext: true, children: [run(t, { bold: true })] });\nconst W = 9360; const kids = [];\n// header: company + sector | logo above \"TICKER – price\" | date + PT, rule beneath\nconst mid = [];\nif (S.logo && fs.existsSync(S.logo)) {\n  const { PNG } = (() => { try { return require(\"pngjs\"); } catch { return {}; } })();\n  let w = 130, h = 36; try { const buf = fs.readFileSync(S.logo); const iw = buf.readUInt32BE(16), ih = buf.readUInt32BE(20); h = Math.round(130 * ih / iw); } catch {}\n  mid.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 40 }, children: [new ImageRun({ type: \"png\", data: fs.readFileSync(S.logo), transformation: { width: w, height: h } })] }));\n}\nmid.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 20 }, children: [run(`${S.ticker} – ${S.price}`, { bold: true })] }));\nconst hcell = (children, width, align) => new TableCell({ width: { size: width, type: WidthType.DXA }, borders: noB, verticalAlign: \"bottom\", margins: { top: 20, bottom: 20, left: 60, right: 60 }, children });\nkids.push(new Table({ width: { size: W, type: WidthType.DXA }, columnWidths: [3600, 2160, 3600], rows: [new TableRow({ children: [\n  hcell([P([run(S.company, { bold: true })], { after: 20 }), P([run(S.sector, { bold: true })], { after: 20 })], 3600, AlignmentType.LEFT),\n  hcell(mid, 2160, AlignmentType.CENTER),\n  hcell([P([run(S.date, { bold: true })], { align: AlignmentType.RIGHT, after: 20 }), P([run(`${S.price_target} PT`, { bold: true })], { align: AlignmentType.RIGHT, after: 20 })], 3600, AlignmentType.RIGHT)] })] }));\nkids.push(new Paragraph({ border: { bottom: rule }, spacing: { after: 80 }, children: [] }));\nkids.push(H(\"Company Overview:\")); kids.push(P(S.overview));\nkids.push(H(\"Sector Thesis:\"));\nS.thesis.forEach((th) => {\n  kids.push(new Paragraph({ numbering: { reference: \"th\", level: 0 }, spacing: { after: 20, line: LINE }, children: [run(th.title)] }));\n  (th.points || []).forEach((pt) => kids.push(new Paragraph({ numbering: { reference: \"th\", level: 1 }, spacing: { after: 20, line: LINE }, children: [run(pt)] })));\n});\nkids.push(H(\"Key Metrics:\"));\nconst m = S.metrics || []; const tot = m.reduce((a, [l]) => a + Math.max(l.length, 6), 0); const ws = m.map(([l]) => Math.round(W * Math.max(l.length, 6) / tot));\nconst mcell = (t, width, bold, top, bottom) => new TableCell({ width: { size: width, type: WidthType.DXA }, borders: { top: top ? rule : none, bottom: bottom ? rule : none, left: none, right: none }, margins: { top: 50, bottom: 50, left: 60, right: 60 }, children: [new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 0 }, children: [run(t, { bold })] })] });\nkids.push(new Table({ width: { size: W, type: WidthType.DXA }, columnWidths: ws, rows: ["]]},"skills/stock-diligence-report/scripts/two_pager_check.py":{"l":"py","n":11,"s":[[1,"#!/usr/bin/env python3\n\"\"\"Check a rendered two-pager: exactly two pages and the second page at least 85% filled. Usage: python3 two_pager_check.py file.pdf\nPrints pages and the fill of the last page (share of the text area used); exit 1 if pages != 2 or fill < 0.8.\"\"\"\nimport subprocess, sys, re, tempfile, os\npdf = sys.argv[1]; n = int(re.search(r\"Pages:\\s+(\\d+)\", subprocess.run([\"pdfinfo\", pdf], capture_output=True, text=True).stdout).group(1))\ntmp = tempfile.mkdtemp(); subprocess.run([\"pdftoppm\", \"-png\", \"-r\", \"50\", \"-f\", str(n), \"-l\", str(n), pdf, os.path.join(tmp, \"p\")], check=True)\nfrom PIL import Image; import numpy as np\nim = np.array(Image.open([os.path.join(tmp, f) for f in os.listdir(tmp)][0]).convert(\"L\")); h = im.shape[0]\nrows = np.where(im.min(axis=1) < 200)[0]; top, bot = int(h * 0.07), int(h * 0.93)\nbody = [r for r in rows if top < r < bot]; fill = (max(body) - top) / (bot - top) if body else 0.0\nprint(f\"pages: {n}; last-page fill: {fill:.0%}\"); sys.exit(0 if n == 2 and fill >= 0.85 else 1)"]]},"skills/statements-analysis/SKILL.md":{"l":"md","n":8,"s":[[1,"---\nname: statements-analysis\ndescription: \"Five-year and eight-quarter dissection of the three statements: standardised and as-reported, common-size, growth, margin bridges in the shave format with waterfalls, balance-sheet and cash-conversion bridges, quality-of-earnings checks with readings, guidance-versus-delivered ledger and the latest quarter against guidance and consensus. Use when the user asks for a margin analysis, a margin bridge, quality of earnings, whether earnings are real, or a statement-by-statement review.\"\n---\n\n# Statements and quality of earnings only\n\nComponent of the stock-diligence plugin. Read `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/SKILL.md` rules, sections 4-6 of `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/report-frame.md`, the quality-of-earnings table in `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/analyst-craft.md` and `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/presentation-standards.md`. Run `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/data_pull.py`, `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/margin_bridge.py` on the annual and quarterly files, `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/build_exhibits.py`; spawn the statements-analyst agent (10-fetch budget) for the reconciliation, readings, what-changed paragraphs and the guidance ledger; assemble Sections 4-6, run the QA gates and present."]]},"skills/internal-two-pager/SKILL.md":{"l":"md","n":35,"s":[[1,"---\nname: internal-two-pager\ndescription: \"Write an internal two-page investment write-up: header (company and sector, ticker and price, date and price target), Company Overview, numbered Sector Thesis with lettered sub-points, a Key Metrics row (EV/EBITDA, forward P/E, EPS growth, gross margin, ROE), numbered Risks, and an Analyst line, filling exactly two pages. Pulls every input from an existing diligence workspace when one exists; otherwise asks the user for each component before writing. Use when the user asks for a two-pager, a pitch write-up, a short investment summary, or a short internal note on a stock.\"\n---\n\n# Internal two-pager\n\nComponent of the stock-diligence plugin. Output: a two-page Word document and PDF built by `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/two_pager_builder.js` from a JSON spec, checked by `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/two_pager_check.py`, which requires exactly two pages with the second page at least 85% filled.\n\n## Format\n\nThe format is fixed in the builder and is never adjusted to make text fit: Garamond 11pt, 1.4 line spacing, left-aligned, 0.9-inch margins. Header table: company name and sector bold at left; the company logo centred above \"TICKER – price\" in bold; date and \"$X PT\" bold at right; a rule beneath. Bold section labels \"Company Overview:\", \"Sector Thesis:\", \"Key Metrics:\", \"Risks:\". Theses and risks as decimal-numbered items with lettered sub-points written as full paragraphs. Key Metrics as a two-row table with a rule above the bold labels and a rule below the values, five columns on one line. \"Analyst: <name>\" bold and right-aligned at the end. No footer unless the user gives footer text; no sector-leader line.\n\nLogo: use the company's official logo file (PNG). Take it from the company's press or brand page, or the Wikimedia Commons file for the company logo (an SVG converts with `cairosvg.svg2png`); if neither is available, ask the user for the file. Never draw or approximate a logo; if there is none, leave the space and say so.\n\nLength: fill both pages with content, never by changing spacing, font or margins. About 620-680 words of body text fills two pages (the sample note is 640). Section shares: overview about 190 words, theses about 300 (two or three numbered theses, one to three sub-points each), risks about 150 (three risks, one sub-point each). If the check reports the second page under 85% filled, expand the sub-points with specific numbers (KPIs, margins, valuation, dates, the deciding question) from the package; if it reports three pages, cut the overview first, then the longest sub-point. Nothing else in the builder is tuned.\n\n## Step 1: find the inputs\n\nLook for the company's diligence workspace (`work/` with `exhibits.json`, `research/*.json`, `model/scen.json`, `model/memo_data.json`). If it exists, take the inputs from it and tell the user which ones came from the package:\n\n| Component | Source in the package |\n|---|---|\n| Header: company, sector, ticker, price, date | `work/company.json`, `exhibits.json` tear_market |\n| Price target | the user's choice among the DCF scenario values in `model/scen.json` (Street, management, bull, bear); default the Street value and say so |\n| Company Overview | the first paragraphs of research key `1` (money flow and the product summary), condensed to one paragraph |\n| Sector Thesis | the long arguments of research key `12`, condensed to two or three numbered theses; the deciding question becomes the last sub-point |\n| Key Metrics | EV/EBITDA and forward P/E from tear_valuation; EPS growth from the consensus exhibit; gross margin and ROE from the quality-checks exhibit; substitute the company's own KPI when a metric is not meaningful and label it |\n| Risks | the risk bullets of research key `10`, condensed to three numbered risks with one sub-point each, each naming the line of the model it hits |\n\nIf the workspace does not exist, do not guess. Ask for every component in one message and wait: (1) company, sector, ticker, current price, date, price target and the analyst name; (2) the overview in the user's words or the filing paragraph to condense; (3) two or three thesis statements with their supporting points; (4) the five metrics, or permission to pull them with `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/data_pull.py`; (5) three risks; (6) the logo file if the official one cannot be fetched, and optional footer text. Offer to run `dcf-only` or the full diligence first if the user would rather have the numbers derived.\n\n## Step 2: write the spec, build, check\n\nWrite `work/two_pager.json` with `company`, `sector`, `ticker`, `price`, `date`, `price_target`, `analyst`, `overview`, `thesis`, `metrics`, `risks`, `logo` (path to the PNG), optional `footer`. Writing rules: full sentences, numbers over adjectives, no em dashes, no \"Label: content\" constructions, every figure with its basis. Run the builder, convert to PDF, run the check, adjust as above until it passes, then present the docx and the PDF."]]},"skills/technicals-check/SKILL.md":{"l":"md","n":8,"s":[[1,"---\nname: technicals-check\ndescription: \"Technical positioning of a stock against its own ten-year history: return z-scores, Bollinger, moving-average distances and crosses, RSI, momentum, realised volatility, monthly MACD regime, with a verdict paragraph and charts. Use when the user asks whether a stock is stretched, oversold, overbought, where it sits technically, or for a MACD or Bollinger read.\"\n---\n\n# Technicals only\n\nComponent of the stock-diligence plugin. Read `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/technicals.md` and `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/presentation-standards.md`. Run `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/technicals.py TICKER --years 10 --out work/technicals`, then `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/build_exhibits.py` for the technicals exhibits, write the verdict paragraph per the reference (no trading instruction), assemble a two-to-three page document and present it. No subagents."]]},"skills/dcf-only/SKILL.md":{"l":"md","n":10,"s":[[1,"---\nname: dcf-only\ndescription: \"Build only the linked three-statement DCF workbook (four scenarios, both terminal methods, live sensitivities, market-implied expectations, checks) and the multi-page assumptions memo for a stock, plus the comps tab. Use when the user asks for a DCF, a valuation model, a Street or consensus DCF, an assumptions memo, or 'what does the price imply' for a ticker, without the full report.\"\n---\n\n# DCF only\n\nComponent of the stock-diligence plugin. Read `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/SKILL.md` (rules, budgets) and then only `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/dcf-build-process.md`, `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/assumptions-memo.md`, `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/comps-multiples.md` and `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/data-sources.md`.\n\nSteps: Phase 0 scope; Phase 1 data for the model only (`${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/data_pull.py`, the latest interim filing for the bridge items, share counts and options, the annual report for history, consensus); write `work/model/dcf_config.json` from `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/example_spot_report/dcf_config_spot.json` (adapt the Drivers block of a copy of `dcf_build.py` only if the revenue model is not subscription-plus-advertising); run `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/dcf_build.py --config ... --out work/model/<TICKER>_DCF.xlsx` then `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/dcf_validate.py`; write the memo with `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/dcf_memo_builder.js` from the validated values; run the model-related QA gates; write MANIFEST.md; present the workbook, the memo and the manifest. No report, no research subagents beyond the valuation analyst; run the red team and research partner auditors on the memo if the user asks for an audit."]]},"skills/street-view/SKILL.md":{"l":"md","n":8,"s":[[1,"---\nname: street-view\ndescription: \"The street view on a stock: consensus against actuals and the model, target distribution, revision trend, named brokers with the assumptions they disclosed, next-print setup, and the long and short theses with what must be true and the deciding questions. Use when the user asks what analysts expect, what the bull and bear cases are, what consensus assumes, or for a summary of investment theses.\"\n---\n\n# Street view only\n\nComponent of the stock-diligence plugin. Read `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/SKILL.md` rules, sections 9 and 12 of `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/report-frame.md`, the expectations lens in `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/analyst-craft.md` and `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/presentation-standards.md`. Pull estimates and targets with `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/data_pull.py`, spawn the street-analyst agent (15-search budget), assemble Sections 9 and 12 as a short document, run the QA gates and present. Value Investors Club only through Claude in Chrome with the user signed in; summarise, never reproduce."]]},"skills/business-understanding/SKILL.md":{"l":"md","n":8,"s":[[1,"---\nname: business-understanding\ndescription: \"A comprehensive plain-English understanding of what a company does at the level needed to write an investment idea: industry money flows with a diagram, eras, the product and monetisation ledger (who pays, which P&L line, margin effect, underappreciated levers), unit economics, cost structure quirks, moat table, governance, KPIs disclosed and not, deciding debates, insight ledger. Use when the user asks how a company makes money, what drives its margins, what is underappreciated, or for a business primer.\"\n---\n\n# Business understanding only\n\nComponent of the stock-diligence plugin. Read `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/SKILL.md` rules, `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/business-understanding.md`, the moat and unit-economics parts of `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/analyst-craft.md`, `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/estimation-methods.md` for undisclosed figures and `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/presentation-standards.md`. Gather the filings, decks, transcripts and product documentation into `work/filings`, spawn the business-analyst agent with a 25-fetch budget, render the money-flow diagram with `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/flow_diagram.py`, assemble Section 1 (with the estimation appendix if estimates are used), run the QA gates and present."]]},"skills/insider-buyback-check/SKILL.md":{"l":"md","n":8,"s":[[1,"---\nname: insider-buyback-check\ndescription: \"Insider transactions and share buybacks for a stock: authorisation history, execution against the price, share count net of SBC, the Form 4 ledger with plan signals, top holders, short interest, capital-allocation capacity and the interpretation. Use when the user asks about insider selling or buying, buybacks, who owns the stock, short interest, or capital return.\"\n---\n\n# Insider and buyback check only\n\nComponent of the stock-diligence plugin. Read `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/SKILL.md` rules and `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/insider-buybacks.md`. Pull Form 4 data, holders and short interest with `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/data_pull.py`, take buyback authorisations and execution from the latest filings, build the exhibits, write the interpretation applying the rules (plan-based versus discretionary, buybacks net of SBC, average price versus current), assemble Section 11 as a short document, run the QA gates and present."]]},"skills/comps-valuation/SKILL.md":{"l":"md","n":8,"s":[[1,"---\nname: comps-valuation\ndescription: \"Peer multiples chosen for the business model (EV/EBIT vs EV/EBITDA vs EV/Sales vs P/FCF), why each peer sits where it sits, the stock's multiples against its own history, and the peer-implied value. Use when the user asks for comps, comparable companies, relative valuation, whether a stock is expensive versus peers or history, or which multiple to use.\"\n---\n\n# Comps and valuation history only\n\nComponent of the stock-diligence plugin. Read `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/SKILL.md` rules and then `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/comps-multiples.md` and `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/presentation-standards.md`. Run `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/data_pull.py` with the peer list, build the peer exhibits with `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/build_exhibits.py`, compute the primary multiple from company figures, write the multiple-selection paragraph, one driver-based paragraph per peer, the own-history statistics and the peer-implied value, assemble Section 7.1-7.2 as a short document, run the QA gates and present."]]},"skills/refresh-diligence/SKILL.md":{"l":"md","n":8,"s":[[1,"---\nname: refresh-diligence\ndescription: \"Refresh an existing diligence workspace for a new quarter or a new price: rerun data and exhibits, rebuild only the sections and the workbook that new data affects, keep everything else. Use when the user asks to update, refresh or roll forward a previous diligence report, model or memo.\"\n---\n\n# Refresh\n\nComponent of the stock-diligence plugin. Read `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/SKILL.md` (Frames, Refresh) and the Refresh mode section of `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/cowork-orchestration.md`. Locate the company's `work/` folder; rerun `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/data_pull.py`, the engines and `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/build_exhibits.py`; spawn only the agents whose sections depend on new data (statements for the latest quarter, events since the last run, street for consensus and targets, valuation for the price and the workbook); update their keys in `work/research/*.json`, rebuild and validate the workbook from the updated config, re-assemble, render, run the QA gates, update the manifest and present."]]},"skills/audit-panel/SKILL.md":{"l":"md","n":8,"s":[[1,"---\nname: audit-panel\ndescription: \"Audit an existing diligence report or investment memo with an investment partner, a banking managing director, an equity research partner, a red team and a QA auditor, each returning ranked findings tagged FAIL or IMPROVE. Use when the user asks to audit, review, stress-test, red-team or find what is missing from a report, memo or model.\"\n---\n\n# Audit panel only\n\nComponent of the stock-diligence plugin. Read the audit-panel section of `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/analyst-craft.md` and the Report QA checklist in `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/report-frame.md`. Spawn the five auditor agents in parallel on the document the user provides (each capped at ten ranked findings, FAIL or IMPROVE, with the section to fix), run `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/qa_gates.py` if the document is a PDF, merge the findings into one ranked list with the section and the fix for each, and present it. Do not rewrite the document unless asked; if asked, apply the FAIL items first."]]},"skills/event-attribution/SKILL.md":{"l":"md","n":10,"s":[[1,"---\nname: event-attribution\ndescription: \"Explain why a stock moved: flag the largest one-day moves, runs and drawdowns over five years, confirm earnings days with EPS surprises, attribute every move to a primary source with sector and theme benchmarks, and write the chapters narrative, catalyst calendar and risks. Use when the user asks why a stock fell or rose, what happened on a date, what the biggest moves were, or for a catalyst calendar.\"\n---\n\n# Event attribution only\n\nComponent of the stock-diligence plugin. Read `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/SKILL.md` and then only `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/event-attribution.md` and `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/presentation-standards.md`.\n\nSteps: `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/data_pull.py` (prices, earnings dates), `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/event_scan.py` with the sector and theme ETFs, spawn the events-analyst agent with a 20-search budget, then assemble a short document (Section 10 of the frame: the numeric table, the numbered narrative keyed by date, runs, chapters, catalyst calendar, risks with model lines) through `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/build_exhibits.py`, `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/assemble_report.py` (a skeleton with only Section 10) and `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/report_renderer.js`; run `${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/qa_gates.py`; present with a manifest."]]},"agents/events-analyst.md":{"l":"md","n":8,"s":[[1,"---\nname: events-analyst\ndescription: Use this agent to attribute a stock's largest price moves, runs and drawdowns to primary sources, confirm earnings days, choose sector and theme benchmarks, and draft chapters, catalyst calendar and risks. Spawned by the stock-diligence skills.\nmodel: inherit\ncolor: cyan\n---\n\nYou are the events analyst. Budget: 20 searches. Read only ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/event-attribution.md. Inputs: work/events (events.csv, runs.csv, drawdowns.csv), work/data/earnings_dates.csv, filings and releases by date. First confirm earnings days against the earnings-date history and record the EPS surprise; then work the attribution order; verify against the primary document; tag; never force a cause. For thematic episodes pull the theme index and write the reconciliation when the regression and the news disagree. Output: work/research/events.json with keys 10 and 10.chapters. Non-negotiable rules: never invent a number; every figure carries its basis (as reported, standardised, derived, estimated), source and as-of date; unknown is n/d, recalled is [verify]; summarise and attribute theses, never reproduce; quotes under 15 words, one per source; numbers over adjectives; no em dashes; no 'Label: content' constructions; write prose, not compressed strings. Write your output as JSON blocks under the section keys named below, in the renderer schema described in ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/cowork-orchestration.md, and return the file path plus a five-line summary."]]},"agents/statements-analyst.md":{"l":"md","n":8,"s":[[1,"---\nname: statements-analyst\ndescription: Use this agent for the statement work of a diligence report: reconciliation of standardised versus as-reported lines, quality-of-earnings readings, what-changed-and-why paragraphs, the guidance ledger and the latest-quarter verdicts. Spawned by the stock-diligence skills.\nmodel: inherit\ncolor: yellow\n---\n\nYou are the statements analyst. Budget: 10 fetches. Read only sections 4-6 of ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/report-frame.md and the quality-of-earnings table in ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/analyst-craft.md. Inputs: work/data/*.csv, work/bridge, the as-reported statements and MD&A in work/filings, decks. Output: work/research/statements.json with keys 4, 4.bridges, 5, 6, A and 0.capital (the balance-sheet lines of the tear sheet from the latest filing). Numeric exhibits come from build_exhibits.py; you write readings, reconciliations and narratives. Non-negotiable rules: never invent a number; every figure carries its basis (as reported, standardised, derived, estimated), source and as-of date; unknown is n/d, recalled is [verify]; summarise and attribute theses, never reproduce; quotes under 15 words, one per source; numbers over adjectives; no em dashes; no 'Label: content' constructions; write prose, not compressed strings. Write your output as JSON blocks under the section keys named below, in the renderer schema described in ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/cowork-orchestration.md, and return the file path plus a five-line summary."]]},"agents/audit-red-team.md":{"l":"md","n":8,"s":[[1,"---\nname: audit-red-team\ndescription: Use this agent to attack a diligence report: claims without mechanisms, recalled or unlabelled figures, benchmark choices that contradict the narrative, standardised-versus-reported inconsistencies, discount-rate inconsistencies, approximations presented as history, subjective weights presented as facts, estimates without ranges. Spawned by the stock-diligence audit panel.\nmodel: inherit\ncolor: red\n---\n\nYou are the red team. Read the whole document and the audit-panel section of ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/analyst-craft.md. Return at most ten findings ranked by importance, tagged FAIL or IMPROVE, with the section to fix and whether the fix is general. Write work/research/audit_redteam.md. Do not fix anything yourself."]]},"agents/audit-banking-md.md":{"l":"md","n":8,"s":[[1,"---\nname: audit-banking-md\ndescription: Use this agent to audit a diligence report as an investment banking managing director: capital structure, capital-allocation capacity, debt capacity, change-of-control constraints, strategic options, the one-paragraph equity story. Spawned by the stock-diligence audit panel.\nmodel: inherit\ncolor: cyan\n---\n\nYou are a banking managing director. Read sections 4 and 11 of the document and the audit-panel section of ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/analyst-craft.md. Return at most ten findings ranked by importance, tagged FAIL or IMPROVE, with the section to fix and whether the fix is general. Write work/research/audit_ibmd.md. Do not fix anything yourself."]]},"agents/audit-qa.md":{"l":"md","n":8,"s":[[1,"---\nname: audit-qa\ndescription: Use this agent to check a rendered diligence report against the Report QA checklist: numbers in prose versus tables, TTM versus fiscal-year totals, tear sheet versus the workbook snapshot, placeholders, events without tag or source, theses without must-be-true KPIs, risks without model lines, style and presentation rules. Spawned by the stock-diligence audit panel.\nmodel: inherit\ncolor: yellow\n---\n\nYou are the QA auditor. Read the whole document, work/model/scen.json and the Report QA checklist in ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/report-frame.md; run ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/qa_gates.py on the PDF. Return at most ten findings ranked by importance, tagged FAIL or IMPROVE, with page, section and the fix. Write work/research/audit.md. Do not fix anything yourself."]]},"agents/business-analyst.md":{"l":"md","n":8,"s":[[1,"---\nname: business-analyst\ndescription: Use this agent for Section 1 of a diligence report: industry money flows, company eras, the product and monetisation ledger, unit economics, cost structure, moat table, governance, KPIs, deciding debates, insight ledger. Spawned by the stock-diligence skills for the business-understanding work.\nmodel: inherit\ncolor: blue\n---\n\nYou are the business analyst. Budget: 25 fetches or searches. Read only ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/business-understanding.md and the moat and unit-economics parts of ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/analyst-craft.md; use ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/estimation-methods.md when a figure the idea needs is not disclosed. Inputs: work/filings, work/transcripts, work/data. Output: work/research/business.json with keys 1, 2 and C. Non-negotiable rules: never invent a number; every figure carries its basis (as reported, standardised, derived, estimated), source and as-of date; unknown is n/d, recalled is [verify]; summarise and attribute theses, never reproduce; quotes under 15 words, one per source; numbers over adjectives; no em dashes; no 'Label: content' constructions; write prose, not compressed strings. Write your output as JSON blocks under the section keys named below, in the renderer schema described in ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/cowork-orchestration.md, and return the file path plus a five-line summary."]]},"agents/valuation-analyst.md":{"l":"md","n":8,"s":[[1,"---\nname: valuation-analyst\ndescription: Use this agent to build and validate the DCF workbook from a config, write the assumptions memo, build peer comps by the multiple-selection rules, and compute own-history multiple statistics. Spawned by the stock-diligence skills.\nmodel: inherit\ncolor: magenta\n---\n\nYou are the valuation analyst. Read ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/dcf-build-process.md (section 17 for the config), ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/assumptions-memo.md and ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/comps-multiples.md. Write work/model/dcf_config.json from ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/example_spot_report/dcf_config_spot.json; run ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/dcf_build.py --config and ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/dcf_validate.py (adapt the Drivers block of a copy of dcf_build.py only if the revenue model is not subscription-plus-advertising; at most three build-validate cycles); write the memo with ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/scripts/dcf_memo_builder.js from validated values only. Output: work/model (workbook, scen.json, memo) and work/research/valuation.json with keys 7, 7.history, 7.peers, 11.buybacks and G. Non-negotiable rules: never invent a number; every figure carries its basis (as reported, standardised, derived, estimated), source and as-of date; unknown is n/d, recalled is [verify]; summarise and attribute theses, never reproduce; quotes under 15 words, one per source; numbers over adjectives; no em dashes; no 'Label: content' constructions; write prose, not compressed strings. Write your output as JSON blocks under the section keys named below, in the renderer schema described in ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/cowork-orchestration.md, and return the file path plus a five-line summary."]]},"agents/street-analyst.md":{"l":"md","n":8,"s":[[1,"---\nname: street-analyst\ndescription: Use this agent to compile the street view: consensus against actuals, target distribution, revision trend, named brokers with disclosed assumptions, next-print setup, and long and short theses with must-be-true KPIs and deciding questions. Spawned by the stock-diligence skills.\nmodel: inherit\ncolor: green\n---\n\nYou are the street analyst. Budget: 15 searches. Read only sections 9 and 12 of ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/report-frame.md and the expectations lens in ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/analyst-craft.md. Inputs: work/data/estimates.json, coverage found by search. Value Investors Club only through Claude in Chrome with the user signed in. Output: work/research/street.json with keys 9 and 12; theses as three paragraphs each (the argument with evidence; what must be true and how it shows in the model; the deciding question and what refutes it). Non-negotiable rules: never invent a number; every figure carries its basis (as reported, standardised, derived, estimated), source and as-of date; unknown is n/d, recalled is [verify]; summarise and attribute theses, never reproduce; quotes under 15 words, one per source; numbers over adjectives; no em dashes; no 'Label: content' constructions; write prose, not compressed strings. Write your output as JSON blocks under the section keys named below, in the renderer schema described in ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/cowork-orchestration.md, and return the file path plus a five-line summary."]]},"agents/audit-investment-partner.md":{"l":"md","n":8,"s":[[1,"---\nname: audit-investment-partner\ndescription: Use this agent to audit a diligence report or memo as an investment partner deciding whether to take a position: price-implied expectations, probability-weighted value, return skew, twelve-month values, monitoring dashboard, pre-mortem, liquidity. Spawned by the stock-diligence audit panel.\nmodel: inherit\ncolor: blue\n---\n\nYou are an investment partner. Read sections 0, 7 and 12 of the document and the audit-panel section of ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/analyst-craft.md. Return at most ten findings ranked by importance, each tagged FAIL (wrong number, broken identity, missing section) or IMPROVE, with the section to fix and whether the fix is specific to this company or general (then name the reference file). Write work/research/audit_pm.md. Do not fix anything yourself."]]},"agents/audit-research-partner.md":{"l":"md","n":8,"s":[[1,"---\nname: audit-research-partner\ndescription: Use this agent to audit a diligence report as the equity research partner responsible for the estimates: model summary against consensus line by line, guidance ledger, next-print setup, KPI definitions, currency and mechanical items, the basis of every multiple. Spawned by the stock-diligence audit panel.\nmodel: inherit\ncolor: green\n---\n\nYou are the equity research partner. Read sections 5, 6, 7 and 9 of the document and the audit-panel section of ${CLAUDE_PLUGIN_ROOT}/skills/stock-diligence-report/references/analyst-craft.md. Return at most ten findings ranked by importance, tagged FAIL or IMPROVE, with the section to fix and whether the fix is general. Write work/research/audit_er.md. Do not fix anything yourself."]]},".claude-plugin/plugin.json":{"l":"json","n":15,"s":[[1,"{\n  \"name\": \"stock-diligence\",\n  \"version\": \"1.0.4\",\n  \"description\": \"Institutional stock diligence: the full report pack (standard ~50 pages or extensive), plus individually callable components: DCF and assumptions memo, event attribution, technicals, comps, insider and buyback check, business understanding, statements and quality of earnings, street view, audit panel, refresh, internal two-pager.\",\n  \"author\": {\n    \"name\": \"Jason Zou\"\n  },\n  \"keywords\": [\n    \"equity research\",\n    \"diligence\",\n    \"DCF\",\n    \"valuation\",\n    \"investment memo\"\n  ]\n}"]]}}}