Changelog
All notable changes to Open Trading Surface. Versions follow semantic versioning. This page is generated from CHANGELOG.md.
3.53.3 — understanding, not work
3.53.2 — dependency claims, stated precisely
3.53.1 — a tagline the product can stand behind
3.53.0 — Open Trading Surface
- The product: plugin id
ots, display name Open Trading Surface, package
- Your data migrates itself. On first run, state moves from the legacy
- No install is stranded. The update channel moves to
- After updating: the plugin installs under its new identity — remove the
3.52.1 — the site catches up with the terminal
- A new flagship feature section on the landing page: the full-spectrum
- The charting section now lists pricing-model overlays, per-model weight
- The guide gains a "Pricing engine & Model of Models" chapter — the you-say
- No engine or terminal code changes in this release.
3.52.0 — every judgment model gets hands, and the terms say whose opinions these are
- A generic judgment-inputs store. Every model that publishes an input
- The 🚀 view is now Judgment Models: the venture-book table on top,
- Terms: no affiliation, no endorsement, opinions solely the creator's.
3.51.0 — the venture book gets hands
- A 🚀 Venture Book view in the terminal rail (per security): the book as an
- Edits land everywhere immediately: saving busts the Pricing Models panel
- Served by
/cm/ventures (GET book + live valuation; POST replace/clear).
3.50.0 — the venture book: what the statements cannot see
optionality.venture_book — a PERSISTENT per-symbol book of pre-revenue
- A maintained object, not a keystroke:
set_venture_book defines or edits
- The add-on discipline: the book values the VENTURES; the existing business
- The zero-input read that pairs with it: chart the model-of-models
3.49.0 — every overlay carries its relationship to the price
- value — the series as fetched (unchanged default);
- Δ $ — price − series, in the symbol's dollars at each bar;
- Δ % — (price − series) / |series| × 100, the premium convention.
3.48.0 — the rotation itself, charted
- Per-model weight time series:
pricing:{SYM}:weight.<model_id> charts one
- The picker is unit-aware: dollar-valued entries land on the price scale,
- The
/cm/momw payload and get_model_of_models now include weight_series
3.47.1 — the plot is always a snapshot
pricing:{SYM}:composite.momw now blends the panel under the LATEST
:YYYY-MM-DD holds the vector recorded at (or nearest before) that date —
- Membership disclosure in replots: a recorded member that cannot be
- Panel footer, catalog label, and tool text restated to match; the
3.47.0 — the model of models: what the market is looking at
- Walk-forward correlation weights, per stock, adaptive over time. At each
- Recorded weightings, replottable. Weight vectors are dated artifacts,
- The terminal's 📐 view is now Model of Models: headline weighted fair
- One measured defect fixed before shipping: the models publish on
- Also: the intrinsic models now share one cached statements fetch per symbol
- Descriptive, not predictive: correlation says which frame TRACKED, not which
3.46.1 — the frame holds every pane
- The charts column scrolls inside the frame.
#charts now takes an explicit
- Pricing overlays default to the price scale — and respect a choice. A
- Client-layout change only; no server behavior, routes, or data change.
3.46.0 — the pricing engine reaches the screen
- A PRICING MODELS view in the terminal rail (💠, beside Operating Model):
- The chart picker now lists the models. Root fix:
pricing joined the
- Dividend discount reads the stable field names. The provider renamed
- Verified end-to-end on the live state: 16 models respond on an S&P name in
3.45.2 — the daemon that could not come back
- The poisoned retry (root).
startServer assigned the module-level server
- The zombie nobody retired.
ensureDaemon retired a recorded daemon only
- The chart factory that violated its own containment. With no daemon, every
- Why "all pages" failed: an in-process server dies with the plugin process,
- Test:
serve-bind-retry.test.js (the poisoned-retry seam); the retire/corpse
3.45.1 — the ledger outgrew its reader
- One parse per file change, not per call. Both stores now cache the parsed
- A test that remembers the wedge (
pi-ledger-cache.test.js): repeated reads
- Known secondary defect, deferred with a note: the daemon liveness probe treats
3.45.0 — the pricing engine: every model, one interface, on the chart
- One interface, one registry.
server/pricing/ — every model is a plain module
- The full catalog. Intrinsic: FCFF DCF, FCFE, Earnings Power Value, Dividend
- On the chart, for free. A new series ref
pricing:{SYM}:{model_id} resolves each
- Composite, honestly.
get_pricing_composite blends the panel (equal weight or a
- Input-driven models stay out of the way. Real options, SOTP, the platform model and
- Tools.
list_pricing_models, price_with_model, compare_pricing_models,
3.44.1 — three panels, one computation
- One shared band summary. The
/cm/band cache moved into band-summary.js (getBandSummary, 5-minute TTL, compact summary shape, bustable). /cm/band, /cm/band-backfill and /cm/interpret all consume it; interpret() takes the band as an injected dependency, its default seam IS the shared helper, and it never recomputes the band privately. Same discipline for cohorts: the interpret op injects the exact cohortsFor + fmp deps the /cm/cohorts op serves, so a "profile unavailable" gap cannot appear in Interpretation while Structure is rendering the profile. A gap in Interpretation is only honest if the input is genuinely unavailable to the whole terminal. Band-backfill busts both the band cache and the cached interpretation that spoke the pre-backfill band.
- Two prices, each with its as-of. The stance strip's price is the band summary's LEDGER-dated observation (AMZN: 244.85 @ 2026-07-23); the Structure tiles and the Attribution headline are computed at the live quote (247.59). Both stay — they are different dated observations, not a contradiction — and every price in the three panels now carries its stamp: the strip price shows its ledger date and "(ledger)", the live-quote tiles say "(live)" with their as-of.
- Attribution headline provenance, stated only when it holds. The headline shares are computed from the latest LIVE partition, but a PIT reconstruction can write a ledger row DATED NEWER than the last live one. When that is the case, one sentence appears under the headline: *"Headline uses the latest LIVE partition (date); a newer reconstructed row exists (date) and is shown in the history — live supersedes reconstruction."*
- Glossary pass — one term per concept. The band's foundational valuation is always foundation (patient); the routed level before the patient adjustment is always level (raw); the operating model's blended fair value is always the legacy v1 blend, and its point value renders as "level point (legacy v1 blend)" — never a bare "modeled core" or "point estimate". Block-version strings in the Attribution history now truncate deliberately (ellipsis, full string in the hover title) instead of rendering cut off.
- Truthful persistence count. The unexplained-component "Points" figure is the length of the longest run of partitions under ONE block version — the only stretch the AR(1) is fitted over — not the full ledger count shown by Recorded history. It is now labelled exactly that, so "Points 60" beside "history 64 point(s)" reads as the block-version boundary it is, not as a data loss.
3.44.0 — the interpretation pane: the model explains itself
- A third child under Operating Model — Model, Attribution, Interpretation — rendered as four readable sections with a compact headline on top, in the terminal rail and in grid cells like any other data lens. Server-side assembly at
/cm/interpret (guarded, 5-minute cached like /cm/band, because it fans out to several engines), and a matching tool: interpret_operating_model.
- What the model says. The band's current summary in sentences: the patient foundation and its factor (with the raw routed level beside it), price, deviation, the degree percentile and its positional reading, band edges, the investment phase — plus the recorded premium decomposition where one exists. The degree line says in the same breath that it is a coordinate in the name's own history, not an assessment of it.
- How it was derived. The routed level and its routing reason from the level registry; provenance said plainly — auto-seeded named as seeded, drivers still on catalog defaults named as defaults (per the calibration's own
judgment_needed admission), the interest-rate source verbatim; the patient-adjustment basis and caveat carried word for word; and the live-vs-reconstructed composition of the band history, with the hindsight-lite standing of every reconstructed row restated where the count is given.
- Dependencies. Cohort memberships with their included/excluded-by-choice standing; admitted vs candidate contributors at the admission gate (and when nothing has been admitted, that sentence leads); the risk-free reference chain (FRED DGS10, FMP treasury fallback, an ASSUMED rate only when both fail — and it says so); the data sources the assembly reads; and the depth note that the 8am recorder deepens the band's history by one live row per day.
- Sentiment interpretation — three lenses, juxtaposed, resolved nowhere. The event-sentiment composite (90-day weighted window) and the analyst consensus (target vs price, rating mix in neutral vocabulary) set side by side with the degree reading — e.g. *86th percentile rich by its own history, while event sentiment is net positive and the consensus target sits 10% above price*. These are DIFFERENT lenses on different clocks: agreement or disagreement is STATED and left standing, never averaged into a view. An explicit
what_this_is_not rides the payload: not advice, not a forecast — the degree is positional.
- Absence is content. Every engine is an injected seam; a failing or empty input becomes a stated gap rendered visibly in its section — "not available — why" — never a crash and never an imputed filler. The test suite fails the build if any single dependency (or all of them at once) can take the pane down, or if verdict vocabulary appears in any generated line.
3.43.2 — one headline valuation: the legacy blend says what it is
- Catalog labels tell the truth.
model:SYM:fair_value is now labelled "LEGACY v1 blended fair value (tracked snapshots; includes the price-anchored exit-multiple leg) — a DIFFERENT construction from the valuation band foundation and not comparable to it". forecast_fv and backfill_fv carry the same disclaimer ("v1 construction, not the band foundation"). The band trio labels are unchanged — they are the headline valuation.
- The overlay picker has ONE headline. The model-conclusions section now leads with the valuation band quick-add under a heading naming it the HEADLINE VALUATION; the legacy fair_value / forecast_fv quick-adds (and the model-gap expression) moved below it under a "LEGACY v1 (different construction)" sub-heading. If both constructions end up on the chart at once, a small informational note appears by the active-overlay rows: *different constructions — the v1 blend and the band foundation are not the same fair value*. Non-blocking: you can still chart both, you just cannot mistake them for rivals.
- The Model panel matches. The stance strip's patient foundation remains the FIRST number shown; where the v1 blend is displayed it is labelled "Legacy v1 blend", and the track-record section says its snapshots are the legacy v1 construction.
- Why both stay resolvable: the v1 track is the model's own dated record — the accountability series against price — and erasing it would erase the track record. It is kept, charted, and labelled; it is simply no longer presented as a second opinion on the same question the band answers.
3.43.1 — the sweep that could not advance
- The depth cap: a 15-year request quietly produced ~9 points. Two silent truncations in the reconstruction chain. The PIT backfill clamped
years to 12 without a word — and, far worse, fetched the price history at the provider DEFAULT window (~2 years of daily bars), so every grid date older than that was skipped as "no price on or before this date" and a 15-year request came back with ~9 quarterly points that looked exactly like a short filing history. Every fetch window is now SIZED FROM THE REQUEST: years is honoured up to 25, statement limits cover the window plus the older filings the trailing CAGR needs at its far edge, and bars are fetched from a quarter before the oldest grid date. reconstruct(symbol, {years: 15}) now genuinely produces up to ~60 quarterly PIT points where the filings exist — and where they do not, the result DISCLOSES the achievable span (achievable_span_years, achievable_note) so a short record reads as the history that exists rather than as a truncation defect. The no-lookahead rules are untouched: filed-by-date only, no filing date means no point, growth is trailing CAGR, never consensus.
- The unreachable-threshold stall:
remaining never fell. backfillMany skipped only names already at 40 usable rows — but many names' entire filed record supports FEWER than 40 points, so under the old criterion they could never be "done" and were re-reconstructed on every call while the tail of the cohort was never reached. The skip criterion is now "already reconstructed to its achievable span": reconstruct() persists a marker on the ledger sidecar (requested_years, reconstructed_span_years, points, timestamp), and a name whose marker covers the requested width is skipped REGARDLESS of the depth it reached — reported as skipped_already_reconstructed, separately from skipped_already_deep. An empty reconstruction persists the marker too: a barren answer is still an answer, not an invitation to ask forever. remaining now counts only names that genuinely still need work and strictly decreases across calls.
still_forming keeps the band honest. Names that cannot reach the 12 strictly-prior deviations the first band edge requires even after full reconstruction are counted and NAMED per sweep (still_forming, with rows held vs rows needed). Their bands stay "forming" — honestly, permanently if need be — and the sweep stops pretending another pass will change that.
bandSeries honours the same marker, so viewing a shallow name no longer re-fetches years of statements to relearn that its record is short. reconstruct_price_identity documents the new bound (default 6, max 25) and returns the achievable-span fields.
3.43.0 — realtime tailing for 1-minute charts, opt-in
- A
Realtime checkbox joins Log and Ext on the workbench's bottom bar, enabled ONLY on the 1-minute interval (disabled elsewhere, with a tooltip saying why). It is OPT-IN and never the default: unchecked, today's behavior is exactly unchanged. When checked, once per minute the chart fetches ONLY the newest 1-minute bars and merges them into the loaded series — never a full-history refetch or repaint.
- Merge semantics, by bar time. The still-forming minute (same timestamp as the newest loaded bar) is replaced in place; genuinely new minutes are appended; anything older is ignored — loaded history is final and never rewritten. A tick that changes nothing (market closed, quiet tape) is silent and cheap: zero merged bars means zero repaint.
- The delta stays small server-side. FMP's from/to are date-granular and cannot express "the last five minutes", so the existing
/fmp proxy route learned a lightweight ?tail=N — stripped before forwarding, sliced to the newest N rows on return — instead of a second data route shipping a full day of 1-min bars per tick.
- Polite about resources. The timer pauses while the tab is hidden and resumes (with a catch-up delta) on visibility; changing interval or symbol clears it; leaving the 1-minute interval unchecks the box.
- Agent parity.
control_chart set_realtime {on} — refused with a stated reason on any interval other than 1-minute — and inspect_chart what=state reports realtime: true/false alongside log and ext.
3.42.0 — back-calculated bands for the index, and on demand
backfill_valuation_bands — bulk back-calculation of the deviation history the band needs, across a cohort (default index.sp500) or for a single name. For every member with an operating model whose usable ledger is shallower than 40 rows, it runs the PIT reconstruction (~15 years of filed-only statements) and the ledger deepens; the band forms from history the name actually has. PIT-HONEST, HINDSIGHT-LITE: reconstructed rows are what the model WOULD have said from statements filed by each date, with judgment drivers carried back from today — weaker evidence than live rows, flagged as such, and a live row on the same date always supersedes its reconstruction.
- Resumable, never all-or-nothing — the
record_price_identity sweep discipline, applied to depth. Each reconstruction persists as it completes, the run stops after limit (default 25) new reconstructions, and the report says what remains (reconstructed, skipped_already_deep, refused by name with why, remaining, universe) — repeated calls TILE the cohort. Idempotent: names already at 40 usable rows skip, so a re-run costs nothing and redoes nothing. Names without a model are refused by name, never silently dropped.
- Back-calculate from the Model page. When the VALUATION STANCE strip reports a forming (or absent-for-lack-of-history) band, it now offers a Back-calculate band history button — wired to a new guarded
/cm/band-backfill route that runs the reconstruction, busts the five-minute /cm/band cache for that symbol, and returns the refreshed summary; the strip re-renders in place. The button appears ONLY while the band is forming: a formed band needs no back-calculation, and the caption states the reconstructed rows' weaker-evidence standing before you press anything.
3.41.1 — the band back-fills its own history
- Depth is ensured, not hoped for.
bandSeries invoked the PIT reconstruction only when the usable ledger held fewer than 4 rows, and at default depth (~2 years) — a name with a shallow live ledger produced ~11 points and the band NEVER formed, because 12 strictly-prior deviations are required before the first edge. The reconstruction now runs whenever the usable history (live + already-reconstructed rows) is shallower than 40 rows, and asks for ~15 years of filed statements. Reconstructed rows keep their flag, a live ledger row on the same date still supersedes its reconstruction, current reports priors_live vs priors_reconstructed, and a new limitation line states plainly that reconstructed deviations carry HINDSIGHT-LITE standing — filed-only statements, but judgment drivers carried back from today.
- The quick-add trio cannot land off the price scale.
addOverlay now forces mode price and shift 0 for model:{SYM}:band_value / band_upper / band_lower no matter which caller adds them — the picker quick-add, search, a saved layout, or the agent's control_chart add_overlay. A band edge in a side pane, or lead-lag shifted, is a chart lying about what the band is.
- A forming band says so on the row. When the edge refs resolve to zero points because no date has reached the minimum priors, the resolver now throws
band forming (N/12 prior points) … — how far along, not a bare refusal — and the workbench legend row displays the overlay's error text instead of a lone ⚠ glyph.
3.41.0 — one model on the chart: the superseded overlays retired
- The horizon composite is gone (
model:{SYM}:composite, model:{SYM}:divergence, get_horizon_composite). It was already dead by its own findings: loop 2's held-out check came back NEGATIVE on the class-constant mixture (it over-lifts names that do not carry their group premium and under-lifts those that carry more), loop 3a's walk-forward loading was a reconciling estimator standing in for a mixture that was never earned, and the valuation band ships the same substrate (the routed level, cohort leg stripped, PIT-reconstructed when thin) with an honest reading. horizon-models.js is KEPT as shared math — stripCohortLn is the band's foundation, and loadingSeries/mix/the class registry stay for loop 3b — but compositeSeries and the composite()/divergence() wrappers are deleted, so the retired surface cannot be reached from the chart.
- The T1 fundamental trend is gone as a chart surface (
trend:{SYM}, trend:{SYM}:deviation, get_fundamental_trend). It was retired by its own scramble gate — the admission criterion T1 itself insisted on refused most names, which is the gate working, not failing. trend-model.js and its tests are KEPT: the earned null and the paired scramble scoring are specified in TREND-MODEL-SPEC and may return in the T2 timing-form; the module is simply unreachable from series.js until then.
- The band trio is completed. New ref
model:{SYM}:band_value charts the PATIENT foundational valuation itself, so the full trio — band_value + band_upper + band_lower — draws through the same resolver as everything else, on the price scale by default (their unit IS the price). The workbench overlay picker gains a one-click Valuation band quick-add that toggles all three in price mode, and control_chart add_overlay documents the band trio among the price-scale defaults.
- The model-ref grammar error, the overlay catalog, the KB, and the test areas now describe only surfaces that exist; the retired refs fail to parse with an error naming the surviving fields.
3.40.0 — the patient foundation, the investment phase, and the stance-first panels
- The patient foundation. Where the routed level is
security.valuation — the operating DCF, which EXPENSES R&D — the band's foundational valuation is scaled by the EBIT-margin effect of capitaliseRD: factor = 1 + operating_income_uplift / max(operating income, 5% of revenue), clamped to [0.8, 1.3] and applied multiplicatively to the level. Bands and deviations are computed against the PATIENT valuation; the raw level rides along per point. This is an APPROXIMATION and says so in every payload (patient_adjustment.caveat): a margin-uplift scaling of the level, not a re-run DCF — the re-run DCF per date is the exact form and awaits the level upgrade. No R&D reported → applied:false, nothing scaled. Sector-routed levels (AFFO, residual income) are left exactly as routed, and the clamp works both ways: shrinking R&D scales the level DOWN — patience is not a one-way ratchet.
- The investment phase. Each band point and
current now carry investment_phase: investment_intensity = (capex+R&D)/revenue for the fiscal year IN FORCE at that date — filing-date honest and as-of, a year is never in force before it was filed — ranked as a percentile of the name's OWN strictly-prior intensity history (the in-force year is never a member of its own sample), labelled light (<P25) / normal / heavy (>P75) with n_prior. The phase is what makes the degree readable: an 8th-percentile-cheap name in a HEAVY investment phase (oversold-while-investing) and one in a LIGHT phase (oversold-while-shrinking) are different facts, and the tag keeps them distinguishable without judging either.
- The stance-first panels. The Model panel (Operating Model → Model) and the Attribution panel now both LEAD with a VALUATION STANCE strip — one shared renderer, two hosts, so the panels cannot drift into different stances for the same name: patient foundational valuation (with the adjustment factor and the routed level's name), current price, deviation (ln and %), the DEGREE percentile with its reading, the P10/P90 band edges, and the investment-phase chip with intensity. Served by a new guarded
/cm/band route (compact summary, cached in-route for five minutes). The Attribution panel's contribution table keeps its admitted/candidate standing untouched, under a one-line note that the market's discount of heavy investment phases appears there as DEVIATION, not as a haircut to the foundation.
get_valuation_band payloads now include investment_phase and patient_adjustment throughout (summary and series:true), with the limitations extended to state the approximation and the positional (non-judgmental) reading of the phase labels.
3.39.0 — the valuation band: normal variation bounded, degree not judgment
- The foundation is the model/attribution substrate, not a fit on price. The band's spine is the Model/Attribution panels' per-security valuation: the price-identity LEVEL series — the ledger
known with the admitted cohort leg stripped back out (stripCohortLn, the loop-2 no-double-count discipline), point-in-time reconstructed from filed statements where the ledger is thin, the same substrate and fallback as the horizon composite. Price never enters the foundation; it only ever gets measured against it.
- The bands are the name's own strictly-prior deviation percentiles. Per date,
deviation_ln = ln(price/valuation), and the P10/P90 (outer) and P25/P75 (inner) of the STRICTLY-PRIOR deviations only — the expanding-window no-lookahead pattern of premium.percentileSeries, applied to the deviation itself. Today's deviation is never a member of its own band: an enormous final-date spike leaves its band byte-identical, is reported at full value, and folds into the history one step later, where it widens future bands. Fewer than 12 priors and the band is ABSENT, labelled band forming (n/12 prior points) — never a thin guess.
- The degree, not a judgment. The reading is the continuous percentile of the current deviation within the prior distribution —
92nd percentile rich — beyond the P90 band, within normal band (53rd percentile), 8th percentile cheap — position language only, no verdict vocabulary. And long, persistent excursions are NORMAL: a name can live beyond its P90 for quarters; every date of it is reported, nothing is gated or clipped, and the band simply learns the new behaviour. The percentile degree also cancels a constant multiplicative bias in the level, so the reading survives an uncalibrated foundation that the absolute dollar edges would inherit.
get_valuation_band {symbol, series?}: live summary (valuation, price, deviation, degree percentile, band edges, reading) or the full per-date series, limitations declared in the payload (level grade inherited; own-history bands, not a market standard; descriptive, not predictive). Overlay refs model:{SYM}:band_upper and model:{SYM}:band_lower (P90/P10 in price space: valuation × exp(prior percentile)) chart through the same engine as everything else, with catalog labels that state the standing.
3.38.0 — the fundamental trend: trend correlation without autocorrelation
- The grounding constraint, enforced three ways (TREND-MODEL-SPEC §2). The RHS is fundamentals ONLY — price never appears on the right-hand side, no lagged price, no technicals: the return-panel identity rule applies to the trend verbatim. Coefficients are PRIOR-ONLY: at every date the fit uses strictly-prior observations (exponentially weighted, min 8), so a structural break shows one step late by design and a same-day fit that would reconcile by construction is refused. And the scramble test is the admission criterion: refit with fundamentals shifted to wrong dates (fixed seed, 20 draws) — the real out-of-sample R² must beat 95% of the scrambled distribution, or the name's trend is ABSENT with the auditable verdict. If wrongly-dated filings fit nearly as well, the "trend" was autocorrelation in costume.
- The null had to be earned. A naive permutation hands the real fit a strawman twice over: TTM prints are rolling sums (MA-smooth), so shuffled regressors jump print-to-print in a way the real path never does — and (measured) the real fit beat 20/20 such draws on a PURE RANDOM WALK with unrelated fundamentals. A cyclic rotation still lost through its wrap seam, and a truncated shift lost through its shorter window's smaller SST. The shipped null is an un-wrapped time-shift of the print sequence, scored PAIRED against the real fit on identical evaluation rows: identical y, identical SST, the only difference left is whether the true dating fits better than the wrong one. Across 30 seeded null fixtures the gate false-passes 1 — the designed tail of a 20-draw/95% criterion.
- Filing-date honesty throughout: a TTM is usable only after the LAST of its four filings landed; undated statements are skipped AND counted rather than dated by period end; negative/zero EPS has no logarithm, so those rows are skipped as observations, counted, and disclosed — never zeroed, never clamped into a coefficient.
get_fundamental_trend {symbol, series?}: latest trend, deviation, z, in line / far afield reading, coefficients with n_prior, and the scramble verdict; series:true for the full history. Chartable as trend:{SYM} and trend:{SYM}:deviation through the same overlay engine as everything else, with labels that state the standing: fitted on filings only, never price on the RHS; scramble-tested; descriptive, not predictive. Whether deviation reverts is T4 — a registered thesis with invalidation triggers, not a feature.
3.37.1 — the accuracy test projects, it does not price
3.37.0 — the fundamental-accuracy test: the level adoption criterion, run rather than cited
3.36.0 — the level upgrade surfaces: v2 becomes visible, and its risk-free rate becomes real
- The risk-free rate now falls back honestly:
fred:DGS10 → fmp:treasury:year10 → the assumed 4% as a LAST resort, each source named in the provenance string. Every comparison run before this fix understated every WACC by ~0.6pp (the 10Y is ~4.6%) while presenting itself as a per-name derivation.
- v2 appears in the per-name level registry (
get_level_registry): a fifth row alongside the operating-company model and the three sector blocks, valued/refused with its WACC, risk-free source, competitive-advantage period, and whether R&D was capitalised — plus the note that it is registered but NOT the default (adoption is select_price_level, a recorded block-version event). The registry existed to show "every model that could value this name" and omitted the only general-purpose alternative — its own founding defect, recurring.
- Baseline comparison (22 names, pre-fix risk-free): v1 residual dispersion 0.62 vs v2 0.75 — dispersion RISES, the expected consequence of removing the price-anchored leg's artificial compression. WACC differentiates 5–15% (NVDA 15.0, GILD 5.4) vs the flat 9. Not a validation; the adoption decision needs the re-run with the real risk-free and the fundamental-accuracy criterion.
3.35.0 — the widened leave-one-out set needs a cache, not a timeout
3.34.0 — the sector cohort preference never fired: a kind the producer doesn't emit
- Preference order fixed:
taxonomic (the sector cohort) first; peer/size/index remain the labelled fallbacks.
- The fixture now asserts the real world: the suite constructs memberships with BOTH a taxonomic sector cohort and a peer cohort and asserts the sector wins — and that the peer fallback names why it fired.
- The admitted coefficient must be re-earned. The frozen β was fitted against the old (peer-fallback) exposures; applying it to sector exposures would be a coefficient on the wrong basis. After this release,
evaluate_cohort_block re-runs the held-out gate on the corrected exposure and re-freezes — recorded as a new admission/block-version event so residual history stays readable across the boundary. Until re-run, contribute() keeps applying the previous frozen coefficient to the new exposure, which is exactly why the re-gate should happen promptly.
3.33.0 — horizon models, spiral loop 3a: the per-name loading, fitted walk-forward
- The per-name LOADING g — how much of its group's re-rating this name's own price has historically carried:
ln price − ln level ≈ g · cohort_ln. Fitted walk-forward with exponential weighting from strictly-prior observations, so the divergence at t is price vs the name's OWN historical composite relationship — never a same-day fit that reconciles by construction. A regime break shows up one step late by design, never same-day.
- Honest fallbacks throughout: fewer than 3 prior observations → the class prior, labelled; near-zero cohort_ln cannot identify g and is skipped as an observation; implied loadings are clamped so a data defect cannot become a coefficient; no cohort leg → the declared prior mixture.
- Composite becomes a log-linear blend:
ln composite = ln level + g·cohort_ln (+ momentum tilt). The historical series reports the per-date loading, its source (walk_forward | prior), and prior count; the live payload carries loading with the same disclosure.
- Loop 3b declared, not faked: regressing loading CHANGES on the certainty/risk-reward observables (IV term structure, estimate dispersion, guidance events), lead-lag disciplined, awaits a deeper ledger — the limitation says so in the payload.
3.32.0 — horizon models, spiral loop 2: the short leg earns its coefficient, and the double-count is gone
- Level-only base for every leg.
stripCohortLn divides the recorded cohort contribution back out of known, so SHORT/MEDIUM/LONG are all built on the level (fundamentals) alone. An absent cohort contribution strips nothing — absence is not a value. Rebuilding level × exp(cohort_ln) recovers known exactly: one application, never two.
- The short leg's cohort term now comes through the admission gate.
cohort-block.contribute applies the FROZEN coefficient (α + β·ln leave-one-out cohort premium) that was earned against held-out residuals — the short model's first *fitted* internal weight, replacing the raw premium at full weight. Not admitted, or no eligible cohort → the term is absent and the short leg is momentum-only.
- The historical series is more point-in-time honest: rows recorded with a cohort contribution use their own dated ln; level-only backfill rows borrow the live-edge admitted ln, and the limitation says so.
3.31.0 — horizon models, spiral loop 1: three duration models, a class registry, and the composite tracking line
server/horizon-models.js — three per-horizon values from existing engine outputs: SHORT = relative price (substantiated × cohort premium) tilted by momentum; MEDIUM = the sector-routed substantiated value; LONG = the terminal-heavy DCF leg. A class registry carries per-class mixtures and long-leg forms (duration is one vector, class another); loop 1 implements the software class plus a generic default rather than forcing every name into a class.
- The composite and its divergence.
composite = Σ w_h · value_h under the class mixture PRIOR (declared, not fitted — loop 3 estimates it); divergence = ln(price / composite). The historical series carries a trailing z-band against the name's strictly-prior history — the band is the signal, because it cancels a constant multiplicative bias in an uncalibrated level; the absolute level inherits the level model's grade and says so.
- Absence is not zero. A missing horizon leg yields an absent composite, never an imputed 0; an ineligible cohort premium falls back to a declared momentum-only short leg, never a fabricated 1.0×.
get_horizon_composite tool — live values, or series:true for the historical tracking line with per-date readings (in line / far afield rich / far afield cheap / band forming).
- Chartable as native series —
model:<SYM>:composite and model:<SYM>:divergence resolve through the same overlay engine as every other series; labels state the epistemic standing (candidate, parallel to the price identity).
- Loop-1 simplifications are declared in the payload: historical medium and long legs coincide (level-only PIT backfill), the cohort premium is held at the live reading across history, and the momentum tilt is a trailing 63-bar ROC. Each is scheduled to be replaced by a recorded series in later loops.
3.30.0 — the value line is a native model series, not a separate chart
- New native series
model:<SYM>:value — the price-identity KNOWN block over time: the routed level (fundamentals) plus the admitted cohort re-rating. It is the single line that folds in every contributor the model stands behind, so price − value on the chart is the residual. Distinct from :substantiated (level only) and :backfill_fv (the level-only point-in-time reconstruction), and it reads the recorded residual ledger, backfilling history when thin.
- The model's
series() now declares the full band — value, substantiated, backfilled history, and the driver-derived (drivable) forecast — so all of it is *discoverable from the model* and one click to overlay. Shocking a driver reshapes the forecast line through the same machinery; the value line is drivable, not a picture.
- The separate Valuation view, its bespoke tool, route, and module are removed. Read the bands on the Stock Chart: overlay
model:<SYM>:substantiated and model:<SYM>:value against price, and the gap is the residual.
3.29.0 — the Valuation overlay: the earnings-justified chart, from the price identity
- Three series, not one. The fundamentals line is the routed level (DCF / AFFO / residual income); the upper line adds the admitted cohort re-rating; the band between them is how much of the premium is the peer/sector group being re-rated versus this name specifically; and the gap from there to price is the residual — unexplained, never a claim the price is wrong. Every band is an attribution contribution.
- Point-in-time history. The historical value line is reconstructed from statements filed by each date (via the price-identity backfill), so it is what the model *would* have said with only-then-available data — a stronger claim than "normalized." Reconstructed points render dashed and are labelled weaker evidence than the live segment.
- A dense price line (weekly) weaves around the value line the way the idiom is meant to be read, with a live readout: price, model value, the discount/premium, attributed share, and whether the cohort leg is admitted.
3.28.1 — fix overlapping transcripts in a multi-panel view
3.28.0 — name your views
3.27.0 — a tab is a view, not a security
- A per-view security multi-select. Under the tab strip, a view bar shows the view's securities as chips. Click one to make it the security in context — the selected chip feeds the tab label, the left rail, the quote strip and the agent, so everything downstream keeps pointing at one unambiguous name. Add or remove securities from the view without opening a new tab.
- The 1/2/4 layout is per view, and works for every lens. The grid metaphor that used to live only in the chart view is now a property of the view itself, and it generalizes: a 2- or 4-up layout renders the chart as linked panes *or* any data lens (Operating Model, Financials, Comps…) as one panel per security. The selected cell is outlined; clicking a cell moves the context to it. A single-security view — the default — is unchanged.
- The tab is labelled by its selected security, with a
+N badge when the view holds more, so a dozen names live behind a handful of legible tabs instead of a dozen tabs.
- Migration is automatic. Every existing single-security tab becomes a one-security view. The agent channel gains the new model too: terminal state reports each view's
securities and layout, set_grid now drives the active view's set and layout for any lens, and select_security moves the in-context selection.
3.26.0 — a premium only compares like with like
- The cohort-block exposure is level-restricted. The identity leg shipped in 3.25.0 builds its leave-one-out cohort premium only from members valued on the subject's own level. A REIT sitting in a size-band or peer cohort is dropped before the median is taken, so the exposure is one cohort re-rating rather than a blend of denominators. The leg now reports the level it enforced and how many members it dropped for being on a different basis. (The sector cohort — the default — mostly satisfies this by construction, since sector members route to the same level; the guard catches the peer- and size-cohort fallbacks where they don't.)
decompose_price discloses the mix. Every cohort premium now carries its level_composition, and a premium pooled across more than one valuation level says so plainly, so a reader can prefer a single-level cohort when the comparison has to be exact.
3.25.0 — the cohort re-rating enters the attribution
- Non-circular by construction. The exposure is
ln of the leave-one-out cohort premium — the subject is dropped from its own cohort before the premium is taken — so a name is never explained by its own price (PRICE-IDENTITY-SPEC 2.2). The sector cohort is used because its members route to the same valuation level as the subject, making the premium ratio like-for-like rather than a mix of AFFO- and DCF-based premiums.
- It enters
known only through the gate. The level legs predate the admission gate and are grandfathered; a leg added now gets no such grace. Until evaluate_cohort_block admits it against the recorded residual ledger (name-holdout, incremental, Šidák deflation), it contributes nothing — it is *shown* in the panel as a candidate with its would-be exposure named, and the cohort re-rating stays in the residual. Nothing enters the known block without surviving a held-out test (PRICE-IDENTITY-SPEC 2.1).
- Frozen coefficient. Once admitted, the operative coefficient is the one fitted at admission and frozen — never re-fit on the fly, so residual movement is never partly our own re-estimation.
3.24.0 — one comparable set, used everywhere
3.23.0 — peer groups enter the decomposition, and shed their cap
3.22.0 — a peer group is a cohort with a subject
3.21.0 — the Screener tab is retired; there is one thing now
3.20.0 — the full screening machinery, inside Cohorts
3.19.0 — the screen builder lives in Cohorts (step 2)
3.18.0 — a cohort is a screen (step 1: every cohort is expressible)
3.17.0 — nothing is auto-excluded; exclusion is your choice
3.16.0 — a cohort declares whether it prices its members
3.15.0 — the watchlist tool surface is retired
3.14.0 — Watchlists becomes Alerts; lists live in Cohorts
3.13.0 — a custom cohort is an editable list
3.12.0 — a saved screen is a cohort
3.11.0 — a cohort no longer needs a shadow watchlist
3.10.0 — you can open a cohort
3.9.0 — a watchlist can hold an index
3.8.3 — the reseed can finish
3.8.2 — the reseed can reach the names it was protecting
3.8.1 — the self-check reaches every model
3.8.0 — the model now checks itself against what was reported
3.7.1 — 3.7.0 broke calibration, and the test said it was fine
3.7.0 — the rate on debt is reported, not invented
3.6.3 — a missing valuation no longer disables feature selection
3.6.2 — four bugs the first live run exposed
3.6.1 — the Attribution picker points at returns
3.6.0 — Attribution now selects features against price CHANGES
3.5.0 — Standing removed
3.4.2 — the Standing tab never rendered
3.4.1 — the level registry was dead in the shipped build
3.4.0 — two panels, two valuations, no warning
- declined — the applicability rule looked and said no (a merchant power producer is not rate-regulated)
- refused — it applies and could not produce a number (negative AFFO;
ke within 1.5 points of g)
- absent — nobody built the inputs
get_price_identity stamped the envelope with the globally selected level while the state inside correctly named the routed one. Same defect as the blockVersion(levelIdUsed) fix, missed one layer out. It is now derived from the partition so the two cannot disagree.
- The dependency picker hardcoded
axis: 'cross_section' even when scope was reconstructed, so a temporal claim was scored by a name-holdout — rejected for a reason unrelated to whether it is true.
3.3.0 — the sector models face the gate
3.2.1 — equity cannot be cheaper than debt
- A minimum equity premium over the risk-free rate. No equity claim is worth holding at treasuries plus a rounding error, whatever a trailing regression says. This is a judgment and is labelled as one.
- The company's own cost of debt, implied from interest expense and total debt. This is not a judgment but an arbitrage: equity is subordinated to debt and bears strictly more risk, so it cannot require a lower return in the same firm.
3.2.0 — the prediction held, and the third model
3.1.1 — one function was answering two questions
check(facts) — does the standard operating-company DCF apply?
unvaluable(facts) — is there *no* model at all for this name?
3.1.0 — the study was reading a second definition of substantiated value
- Utilities — VST, PCG, EVRG, SRE, LNT, ES. Six names, one sector, all failing the same way. Rate-regulated returns and heavy capex break the operating-FCF frame exactly as REITs and banks did, and the study's own misspecification warning already flags Utilities by name.
- Negative-earnings growth — CRWD, AXON, MRNA, SNDK, LITE.
- Commodity cyclicals — DOW, LYB, PSX, MOS, ALB.
- Two REITs the REIT model itself refused — ARE and EQIX. A refusal from the right model is a different and more interesting signal than a refusal from the wrong one.
3.0.3 — the block version named the wrong model
3.0.2 — a zero that meant "not reported" arrived as a measurement
3.0.1 — the routing table was never being asked
3.0.0 — the routing table finally has somewhere to route
2.99.0 — a dead end that knows the detour should mention it
2.98.0 — the picker was baking the open tab's ticker into every candidate
2.97.0 — the time axis, which most of the catalogue lives on
2.96.0 — "insufficient_overlap" was hiding two category errors
2.95.0 — the picker was calling a route that had never been called
- Its first version used a fixed series ref and passed once, then failed on the second run. The HTTP calls reach the daemon, which has its own persistent ledger — an in-process
ORRERY_ADMISSION override isolates the test process and not the server answering it.
searchCount() floors at 1 because Šidák needs k ≥ 1, so on a fresh ledger the first inspection moves 1 → 1 and proves nothing. The assertion has to start from past the floor or it passes for the wrong reason.
2.94.0 — the picker could not express the analysis it was calling
2.93.0 — the probabilities, as distinct from the weights
- a large share with a low probability means a correlated group is doing the work, and the group is reported with its joint "at least one belongs" probability — 1.00 for that pair, against 0.46 and 0.57 individually
- a small share with a high probability is a small, robust, consistent effect, which is precisely what a share-ranked table buries
2.92.0 — feature selection analysis, and the gravitational family
- Leave-one-out peer exposures, structurally — no flag, no code path computing an inclusive aggregate. That removes only the tautological part where the name appears on both sides. The real channel survives untouched, because when the basket is bought the *other* members are bought too.
- ρ, the coupling strength, from the spatial autoregressive model
y = ρWy + Xβ + ε. W is mass-weighted, row-normalised, zero diagonal. This is the number that was missing: how hard the cohort pulls, estimated rather than assumed away.
2.91.0 — the Attribution view actually attributes
2.90.0 — Attribution reaches the UI, and Operating Model becomes a parent
- the attributed and unattributed shares of the observed price, as figures and as a bar
- which valuation model was routed to this name and why, with the block version
- the contribution table with the three quantities kept apart — what each factor contributed (exact, and possibly exactly meaningless), whether it belongs at all, and the basis for that
- how the unexplained component behaves: persistence, half-life, and the reading in words
- the recorded ledger history, with reconstructed points marked
- a button to record today's partition
2.89.0 — the attribution panel, and the tool's contract
contribution_ln — what it contributed. Exact, deterministic given the block, not a probability at all, and possibly exactly meaningless.
belongs — whether the factor belongs in the model at all, from the admission gate. This is the quantity that decides whether the first one means anything.
size_uncertainty — how precisely the magnitude is known, given that it belongs.
2.88.0 — camps F and G: the investigation loop, and forecasting both sides apart
2.87.1 — cross-sectionality is a property of the top layer, not of the model
2.87.0 — multiple top-level models, routed by applicability
2.86.0 — the cost rate mattered more than the discount rate, and camp C had left it broken
2.85.0 — the thresholds are judgment, a short page is not a complete band, and the ledger fill is resumable
2.84.1 — the census, and the relevance floor
2.84.0 — joint uncertainty, the claim axis, and compositional submodels
2.83.0 — coefficients belong to the version, or history cannot be recomputed honestly
2.82.0 — the residual is the product: persistence, regime, and what a move actually means
2.81.0 — the cohort and macro candidates, and the two variables that would have fooled us
2.80.0 — the gate gets a second axis: revalidatable beats lucky
- *Statistical* — unchanged, and it still earns its place. Continuous discovery aimed at a residual is a multiple-testing engine and deflation is the only defence. But it is a filter against luck, not evidence of truth.
- *Mechanistic* — is there a stated causal account, does it survive re-derivation, can it be re-checked at all?
confirm_contributor_mechanism requires an ACCOUNT (why should this bear on a price, through what channel, why should the channel persist — restating the correlation is not an account) and a record of WHAT WAS CHECKED. A confirmation with nothing checked is an assertion with a timestamp.
2.79.0 — the security block, rebuilt: no price in the level, a real cost of capital, and advantage that has a duration
2.78.0 — the admission gate: nothing enters the known block without earning it
2.77.0 — the model now resolves to the price, and reports what it cannot explain
- Cross-sectional premium had no stable relationship with forward returns once a name-holdout was applied.
- Sector explained ~27% of premium variance on a knowingly narrowed universe. The further collapse to 7.8% came from excluding REITs and banks — dropping the most distinctive sectors from a between-sector calculation, which is not evidence of anything. The earlier reading of that number as "the signature of an artifact" was wrong and is corrected in the spec.
blended_fair_value averages two legs, and leg 1 is an exit multiple calibrated from the name's own trailing P/E. Algebraically the P/E cancels: that leg measures whether projected EPS growth beats the discount rate and says nothing about whether the stock is expensive.
- The identity is checked on the published values, not on unrounded internals. The first cut rounded the output while self-certifying from the internals — the same shape as three defects already paid for, where a correct engine and its surface quietly disagreed.
- An absent level contributor refuses. It does not report the residual as 100% of price: a missing model is not a spectacular unexplained figure.
- An adjustment that cannot be computed contributes nothing and says so — never a silent zero, because "neutral here" and "not computable here" must stay distinguishable.
- Endogenous contributors must declare a fixed lag. Explaining today's price with today's momentum is circular and would inflate explained share while teaching us nothing.
- Nothing is admitted by default. The holdout-and-deflation gate is camp B, and every contributor in the block until then is disclosed as unvalidated on every reading.
- Block-version changes are recorded as events, so residual movement across a refit boundary is visibly partly our own doing rather than the world changing.
- A live observation supersedes a reconstruction for the same date, never the reverse.
2.76.0 — is the within-cohort spread the market, or is it us?
2.75.0 — Camp 4 completed: equilibrium is reachable, and carries what we now know
2.74.0 — Camp 4: Equilibrium as an Operating Model
- An equilibrium is bounded by its WEAKEST contributor, not the average. Averaging lets one strong input mask several weak ones, which is exactly how a composition acquires authority it has not earned.
- An equilibrium is UNEARNED while any contributor is unearned, and it names which ones. Today that means unearned everywhere: peer-group premiums have never been scored against an outcome, and most influence edges are asserted rather than fitted. That is the honest reading of what this system currently knows, and it should stay visible until the contributors earn their way out of it.
- It refuses to forecast, because forecasting equilibrium means forecasting its premium leg, which the peer-group model refuses on recorded evidence. Composing a refusal into a number would be the failure this spec is organised against.
- It refuses point-in-time, inheriting the peer group's limitation — membership is not versioned, so a historical equilibrium would use today's cohorts with yesterday's prices.
2.73.1 — a cache written under an old schema is stale even when it is fresh
2.73.0 — per-name applicability, and the variance study generalised across cohort axes
2.72.0 — the standard valuation declares where it does not apply
- REITs, banks/insurers/asset managers, and pre-revenue companies are refused. Each refusal names its own cure — FFO or AFFO against a P/FFO multiple for REITs; residual income or a justified P/B for financials; an option-value frame for pre-revenue.
- A sector cohort the model cannot value refuses to publish a premium, rather than medianing meaningless ratios into a baseline that other models then read as a market fact.
- The variance study distinguishes the two reasons a sector is dropped. "Not enough of it is modelled yet" is a coverage problem that time fixes; "our valuation has no meaning here" is a modelling error that time entrenches. Conflating them would have hidden the more important one.
- Over-refusal is a failure too, and is tested: a payments processor or an aerospace manufacturer has operating free cash flow and is valued normally.
2.71.1 — the study now warns when the between-sector term is measuring US
2.71.0 — the sector variance decomposition becomes a tool
- Sectors below 12 modelled members are excluded AND listed, with their coverage. A decomposition that silently drops thin sectors measures the well-covered ones and calls the result "the market".
- The interpretation follows the number. If most variance turns out to sit within sectors, the study says the case for sector cohorts as a primary dimension is WEAK — the finding that would undermine the lattice is stated as plainly as the one that supports it, and a test pins both strings so a later edit cannot quietly soften the unwelcome branch.
- The coverage caveat travels with the result: model coverage is concentrated in index constituents, so both terms are measured over the large-cap portion of each sector. The between term is the more robust; the within term is understated to the extent small caps disperse more than large ones.
- Results are recorded in the study ledger with a date, so the number has provenance rather than living in somebody's memory of a conversation.
2.70.0 — Camp 3: the Relationship Operating Model
- A FITTED edge earns confidence; an ASSERTED one never does. A fitted slope with an observation count returns
earned: true with the grade as its basis. A catalog default returns 0.15, earned: false, and says plainly that it is "a causal claim carrying no evidence beyond its plausibility — a hypothesis being used as an input". Most edges in most models are the second kind.
state() carries the distinction in the payload, not in a footnote: provenance reads FITTED — a regression produced this slope or ASSERTED — supplied by a person or a catalog default, not measured.
- An asserted edge reports itself as an
issue, so it surfaces in a review sweep rather than only where somebody thought to render a label.
forecast() refuses. Whether an edge's strength persists has never been measured here, and a projected edge strength would be an unvalidated mechanism composed into every conclusion above it. The refusal states the condition for lifting it: refit on an earlier window, compare with the realised slope later, record in the search ledger with deflation for the number of edges tested.
state(at) and history() refuse. Edges are not versioned, so past strength is not reconstructible — and an edge history invented from today's slope is the purest form of look-ahead. Both name the fix: snapshot each edge on fit and on edit.
- The model's existing sign-disagreement detector is now surfaced per edge, rather than only in an aggregate table where it was easy to miss.
2.69.0 — membership was filtered by liquidity, and by nothing that checked whether a company still exists
volumeMoreThan: 250000 was excluding live mega caps from their own sectors. An Energy screen at >$40B returned nine names, missing EOG, PSX, MPC, VLO, WMB, OKE, BKR, FANG, TRGP and EQT. COP came back with volume 191,520 on one call and 253,972 on the next — it flickered across the threshold, so membership was not even deterministic between reads. The provider's volume field is plainly not daily volume. The deeper error was conceptual: a cohort is defined by what a company IS, not by how much it traded this morning. Liquidity is a screening concern and belongs in a screen the user asks for. Filter removed.
isActivelyTrading: false was ignored. PXD (Pioneer, acquired 2024) and HES (Hess, acquired) sat in the Energy cohort with healthy-looking caps and prices while live companies were filtered out by volume. The provider had been telling us they stopped trading the entire time; the hygiene filter only checked for a zero cap or price and never read the field.
2.68.0 — Camp 2: Security and Peer Group on the contract
- Security confidence is unearned for almost every symbol. It becomes measured only where backfill scoring has actually resolved outcomes. Everywhere else it is a structural prior explicitly marked
earned: false — and an auto-seeded model now carries *lower* structural confidence (0.2) than one somebody reasoned about (0.4), with an issues() entry saying it is unexamined.
- Security refuses historical dates it cannot reconstruct.
state(at) returns a backfilled point or an absence naming backfill_model_track — never today's drivers dressed as history.
- A peer group has NO point-in-time support, and now says so. Membership is cached with a date but not versioned, so "who was in Technology in 2020" is unanswerable. Claiming otherwise would mean today's members with yesterday's prices.
- A peer group REFUSES to forecast its premium. Per OPERATING-MODEL-SPEC camp 5, forecasting a premium implies the convergence this project has twice failed to demonstrate. The refusal cites the failed name-holdout and states the condition for lifting it. This is camp 5 enforced at the model level rather than left to a future Opportunity Engine to remember.
- Coverage, composition and truncation now live in
state() together, because each changes what a premium means and any one of them missing makes it look better supported than it is.
2.67.0 — Camp 1: the Operating Model contract
series() makes the discoverability law mechanical. A computed quantity that should be visible must be declared here. The failure this closes is the one that recurred three times in a single evening: a correct engine invisible at the surface, most recently a premium built from 47 mechanically-seeded models displayed with nothing saying so.
- A bare number is rejected as confidence.
confidence() requires { value, basis, earned, n }. A heuristic 0.8 and an 0.8 measured over 200 resolved forecasts look identical as numbers and mean nothing alike, so earned: false is a first-class value and cannot be claimed without observations.
- Answering a historical date from today's knowledge fails by DEFAULT. Point-in-time support is declared (
pit: true) and the default is that you do not have it. The first cut of this check required opting *in* to enforcement — meaning the dangerous case passed silently unless someone remembered to ask, which is the same shape as every defect the contract exists to prevent. The burden is now inverted.
undefined is never an acceptable answer. Not-knowing is expressed with absent(reason, what_would_help), which makes an absence actionable rather than merely honest.
2.66.0 — Camp 0: the substrate repair
- Rule-based cohorts were the HEAD of their rule, not its population. The screener returns at most ~60 rows per request whatever
limit you pass, results arrive ordered by market cap, and the default was 60 — so "Technology" was the 60 largest technology companies wearing the sector's name, and its premium was biased upward by exactly the size effect a premium is supposed to be measured against. Membership now resolves by a recursive market-cap band sweep: a band that returns a saturated page is still hiding members, so it splits geometrically (caps are log-distributed) and both halves are swept, until no band saturates.
- A cohort that cannot be completed says so. Where a band cannot be subdivided further, the result carries
truncated: true, the offending bands, and a note stating that the premium is biased toward the largest names and that membership is a lower bound. Truncation travels with the premium into decompose_price and is rendered in the decomposition table.
- The sweep discloses its own floor. Companies below $10M market cap are not swept. That is a design choice; an undisclosed one would still misrepresent the cohort as the whole population of its rule, so
floor and floor_note are reported.
- An explicit
limit is treated as a deliberate request for a slice and is not swept.
2.65.1
- Fixed:
decompose_price readings dropped the composition disclosure. cohortPremium computed the seeded-vs-considered mix correctly, but decompose() built each reading from a hand-copied field list that omitted it — so the path a user actually reads showed a premium built from 47 mechanically-seeded models with nothing indicating that. A disclosure that is computed and not surfaced is not a disclosure. A contract test now pins the field.
2.65.0
- Seeding scope is now a parameter, defaulting to the S&P 500 + Dow. ~503 unique names (the Dow is nearly a subset).
indexes overrides it; index.nasdaq100 remains available.
- Only PUBLISHED constituent lists are seedable, and the reason is recorded in the code. An index cohort resolves from the provider's membership endpoint and returns the whole index; a rule-based cohort goes through the screener, which is capped at 60 rows. Seeding from a capped screen would hand you the 60 largest names of something and call it the set.
- The Nasdaq Composite is not offered, with the measurement to justify it. No endpoint publishes its constituents. The public components page carries ~400 names by company NAME rather than ticker, and a Nasdaq exchange screen returned 196 rows of which 134 were mutual funds. Naming a cohort after an index it is not would be the substitution this codebase refuses everywhere else.
- Every run reports
cohorts_used, so a narrowed scope can never be mistaken for full coverage.
2.64.0
seed_default_models — bulk operating-model coverage. Seeds models across the union of S&P 500, Dow and Nasdaq-100 constituents (~550 unique names) so cohort premiums have enough modelled members to produce a baseline at all. It runs the *same* calibration as build_company_model: drivers come from each company's filed statements and consensus estimates, clamped, with the calibrator's warnings preserved on the model.
- Seeded models are marked, never disguised. Each carries
auto_seeded, its own provenance ("a mechanical STARTING POINT, not a judgment about the company"), and the indexes it came from. The Nasdaq Composite is deliberately not offered: the provider publishes no constituent list for it, and approximating it with an exchange screen would substitute a different universe for the one named.
- Your work is never overwritten. A model whose drivers differ from what the seeder set is yours, and is skipped even by an explicit
overwrite run. "Edited" is judged by comparing the assumptions themselves, not save timestamps — the first implementation compared updated against seeded_at, and because saveModel re-stamps updated on every write, every seeded model read as user-edited within milliseconds. That would have reported 500 mechanical models as 500 considered ones, inverting the disclosure this feature exists to provide.
- Every premium reading now discloses its composition — how many contributing models are mechanical versus your own — in the tool output and in the terminal's decomposition table. Coverage and conviction are different quantities and are no longer displayed as one.
- The harness now runs every suite that exists.
run-all.js carries a hand-maintained list, and two suites had already gone missing from it — including the tests behind the 2.63.0 version fix — while the harness reported green. A registry test now fails if any *.test.js is unregistered, or if any registration points at a file that no longer exists.
2.63.1
- A disconnected terminal now says so. The session token is baked into the page when it is served; if the server restarts and mints a new one, that page holds a dead token and every control-channel poll is rejected. The rejection was swallowed by a bare
.catch and retried silently every 3s — producing a terminal that rendered views and updated prices while the agent could not see or drive it, with nothing on screen to say why. Rejected polls now raise a visible "Disconnected from Open Trading Surface" banner with a cache-busting Reload, because a plain refresh can return the same cached page with the same dead token (which is why refreshing appeared to do nothing).
open_terminal distinguishes the two causes. A blank pane means a boot error; a normally-rendering page means an expired session. They are indistinguishable from the server side and are told apart by looking at the screen, so the tool now says that instead of guessing one.
2.63.0
- Cohort membership hygiene. A live Technology cohort resolved 60 "companies" that included
MU.TO beside MU (at a *larger* cap, for the same company), AVGO.TO, 6697.T, SQ and XYZ (both Block), and ATVI and VMW — acquired in 2023 and still carrying stale caps. A median over that set counts several companies twice and a few that no longer exist; a cap-weighted premium is distorted worse. Secondary listings, duplicate issuers and provider-stated dead tickers are now removed, the largest listing wins, and every removal is reported in the resolved cohort and shown in the Cohorts tab. A *missing* cap is unknown, not dead, and never removes a member.
- One answer to "what version am I". Five call sites answered it independently and two consulted the compiled KB stamp — a file written at release time, so it lags the running code.
diagnose therefore reported "daemon 2.62.0 != plugin 2.61.0" while check_for_updates, in the same process, correctly read 2.62.0, and sent two rounds of debugging in the wrong direction. All consumers now defer to version.js; a test asserts no module resolves its version from a build stamp.
open_terminal no longer asserts liveness it never checked. It reported "Terminal is live" on four consecutive attempts while the page was throwing during boot and no client was registered on the control channel. It now polls for registration and, failing that, says plainly that this is not a working terminal and names the likely cause.
2.62.2
- Fixed: rule-based cohorts were permanently empty. An empty membership resolution was being CACHED, so a transient provider failure became a self-confirming fact — every sector and size cohort resolved to zero members and stayed that way. An empty result is now never persisted, and a cohort that resolved empty re-asks rather than serving the cached nothing.
- Fixed:
max_cache_days: 0 did the opposite of what it says. Under age < max it could never be satisfied, so the one control for forcing a refresh silently pinned the stale answer in place.
- Fixed a refusal that named the wrong cause. "only 0 of 0 members have a model — build models for more members" told the user to fix their modelling when the real failure was membership resolution. Zero members now reports the membership failure, quotes the provider error where there is one, and states that membership is UNKNOWN rather than empty.
2.62.1
- Fixed: selecting the Cohorts tab bricked the entire terminal. Cohorts shipped at pinned index -7 wired into
renderContent but not renderRail, so it fell through to state.tabs[-7] (undefined) and threw on tab.view. Because the throw happened during boot, the agent control channel never started — the terminal painted its tab strip and then sat blank and unreachable, identically on every reload, with every automated check green.
- A render fault can no longer silence the terminal.
renderAll degrades to a visible on-screen error naming the failure, and the ctl channel survives it. A blank pane is the one failure mode this product exists to refuse.
- Both dispatchers now guard an absent tab, and a contract test derives the pinned set from
PINNED and asserts every index is handled by renderContent AND renderRail — so the next pinned tab cannot be wired into one and forgotten in the other.
2.62.0
- Index cohorts ship off the shelf. S&P 500, Nasdaq-100 and the Dow join the lattice, resolved from published constituents. The Dow is registered as PRICE weighted, not quietly normalised to cap weighting — share count is an accident of splits, but the index really is built that way.
- A fourth weighting,
price, with its meaning written down beside the other three.
- Cap weighting degrades loudly. When market caps are unavailable, a cap-weighted index falls back to equal weighting and says so, with coverage reported, rather than presenting a weighted reading built from weights it never had.
- The Cohorts tab is grouped by kind — Indexes, Sectors, Size bands, Factor screens, Custom & examples, Discovered — so what came with the product is legible against what you made. Each row shows its weighting and its caveats.
- A provider failure now makes index membership UNKNOWN rather than an empty set.
[2.61.0] — Peer groups become real: a bug fix, a Cohorts tab, cohort-driven comps, and weighted groups
- The bug that made the whole lattice look empty.
cohorts.js called a provider endpoint that does not exist (stock-screener instead of company-screener), and the failure was swallowed by a .catch(() => []). Every rule-based cohort silently resolved to zero members, so the decomposition reported *"0 of 0 members have a model"* — a refusal naming the wrong cause, which is worse than a crash. Fixed, and a provider failure now returns membership is UNKNOWN, not empty rather than pretending the group is empty.
- A Cohorts tab (🧬). The peer groups every valuation is measured against were reachable as tools and rendered as chips, but there was nowhere to *see or maintain* them — so the lattice was invisible as something a person owns rather than something the system asserts. The new pinned tab lists every cohort with its kind, membership source and provenance, creates cohorts from a symbol list, and deletes the ones you own (rule-based cohorts refuse, being derived rather than stored).
- Peer Comps is cohort-driven. It used to build from whatever
stock-peers returned — one fixed provider list, disconnected from the lattice, so groups curated for valuation had no effect on the comps table actually read. The peer set is now a choice among the cohorts the name belongs to, with the provider list as one option; membership date and resolution errors are disclosed on the table.
- Weighted groups. Some groups are flat — you are in Semiconductors or you are not. Others are weighted: an index is not a list, it is a list with proportions. Cohorts now declare
equal, cap or explicit weighting; cap weights come free with the screener rows, explicit weights must cover every member (a missing weight is a hole in the definition, a zero weight is not membership). The premium reports both figures, because they answer different questions — equal-weighted describes the *typical member*, weighted describes the *group as an index experiences it*. On a test group of 12 small names at 1.5× and 2 giants at 4×, equal-weighted reads 1.50× and index-weighted 3.52×; the gap between them *is* the concentration.
- 485 tests across 51 suites. New contract tests pin the Cohorts tab, its stable agent-addressable index, and the comps cohort chooser.
[2.60.1] — Fix: the decomposition was reachable as a tool but invisible in the UI
- The whole point of the arc was missing from the Operating Model view. The engine shipped, the tool shipped, and the view rendered cohort *membership* chips — so you could see that AAPL belongs to Technology, Mega cap and Megacap platforms, and nothing about what the model actually said against them. The bare −47% gap sat there unreframed, which is precisely what the decomposition exists to fix. Reported by a user looking at the view and finding no appreciable improvement — an accurate reading of what was in front of them.
- What the price is made of now renders directly beneath the fair-value range:
price = substantiated × total premium, then a row per cohort giving members modeled, the cohort premium (what the market pays that whole group over fundamentals — the part that is not about this company), this name vs cohort, and the percentile of that reading in its own history. Absorbed and ineligible cohorts state why. The robustness spread across cohorts renders beneath, so a flattering comparison group stays visible rather than available.
- A contract test now enforces the law I broke. "A capability does not exist until it is discoverable in both renderings" — the test asserts the daemon exposes the route, the view fetches it, all three terms of the identity render, and the recorded negative result travels with them. Shipping an engine plus a tool satisfied neither half properly.
- 480 tests across 51 suites.
[2.60.0] — Value decomposition, camp 6: absolute or relative, but say which. VALUE-DECOMPOSITION-SPEC complete.
set_exit_anchor — anchor a model's exit multiple to its cohort's own median multiple instead of the catalog 18x, which measured across 45 large caps is roughly half what the market pays and is precisely why every default-calibrated model concludes the same thing. The multiple is computed from member models' year-1 EPS — self-contained, no external multiple feed — and refuses below 12 modeled members.
- Suggested, never applied. Default behaviour is suggestion-only, following the
articulation_suggested precedent, because this is the one change in the arc that alters what an existing model *concludes*. The payload states the tradeoff in full: an anchored model can no longer disagree with the market about that cohort, only about this company within it — an absolute valuation becomes a relative one.
- Mutual exclusivity, enforced. While anchored, the decomposition reports that cohort's reading as
absorbed with a null premium: it is now inside the substantiated leg, and reporting it again as a separate multiplicative term would count the same market re-rating twice and make the identity false. The rest of the lattice is unaffected. clear:true returns to an absolute frame and restores the separate premium.
- A measured band displaces an invented one. An anchored
exit_multiple takes the actual cross-sectional dispersion of peer multiples as its uncertainty band, replacing the assumed BASE_SD of 4 — and its provenance is upgraded from *catalog default* (grade D) to *consensus* (grade C), since it is an observed market fact even though using it as a forecast remains an assumption.
- The frame is always stated.
calibration_coherence now reports either frame.cohort_anchored ("this is a RELATIVE valuation") or frame.absolute ("this asserts what the multiple SHOULD be, which is a claim about the market as well as the company"). The two are mutually exclusive and one is always present — a model can no longer be silent about which claim it is making.
- 6 further tests (26 in
premium.test.js), 479 across 51 suites, 157 tools. All seven camps of VALUE-DECOMPOSITION-SPEC are now built.
[2.59.0] — Value decomposition, camp 4: the negative results become part of the product
- A study register, shipped with the software rather than kept as user data — because the failure mode of quantitative research is *rediscovery*: running the same analysis next quarter, getting the same artifact, and believing it because nobody wrote down that it had already been tested and had already died.
- Two findings ship. The premium-vs-forward-returns study is recorded as an ARTIFACT, with the numbers that made it look real (slope −17.5, r² 0.046, p 0.0015, grade B, sign-consistent 5/5 slices, cluster t −8.2, survived Šidák deflation at 0.009) sitting beside the two checks that killed it (name-holdout producing *opposite* signs, +5.3 train vs −26.9 holdout; and collapse to p 0.29 on dropping five AI/semiconductor names). The sector-dispersion study is recorded as CONFIRMED, with the caveat that part of the spread is model misspecification rather than market pricing — which is an argument *for* cohort-relative reading, since comparing like with like cancels shared model error too.
get_research_studies returns the running total of hypotheses tried against this data, so a new test knows it is the 8th and deflates accordingly. record_study lets you add your own — with a field for *what killed it*, because a recorded negative is worth more than an unrecorded positive.
- Two new education modules, both lint-bound to real tools: *"A significant result that wasn't"* carries the full autopsy and its lesson — deflation corrects for how many rules you tried, not for how few independent things you observed — and *"Why one comparison group is never enough"* carries the 1.07×–3.58× sector spread and the 33.6%/66.4% variance split that forced the lattice design.
- 156 tools, 8 education modules, 473 tests across 51 suites.
[2.58.0] — Value decomposition, camp 3b: the decomposition becomes queryable
- Premium fields in the screener, resolved through the panel:
substantiated, total_premium, idio_premium_min/max/median, robustness, n_cohorts, has_model. A new premium stage, appended last so it never runs for a screen that does not ask for it — existing saved screens evaluate identically.
- The anti-shopping law stops being an exhortation and becomes a query.
where idio_premium_max < 0.85 selects names cheap against every cohort they belong to, since the max is the least flattering reading. where robustness > 0.5 finds names whose relative valuation depends on which comparison group you picked. where premium_driven_by = 'substantiated' surfaces the TSLA case — a premium that moved because the model's own fair value collapsed on a filing, not because the market re-rated anything.
- Baselines are computed once per query, not per row. Each cohort's premium is resolved a single time and every row's reading derived from it — the panel exists precisely so that screening 500 names never means 500 model builds.
- Coverage is disclosed on every result that touches the premium stage, measured *before* the where-clause removes anything: *"188 of 312 candidates have no operating model and were EXCLUDED from every premium predicate — absence, not rejection."* Grade-D models are included with their grade visible rather than hidden, because most default-calibrated models are scaffolding and the panel must not launder that into a clean-looking ranking.
- A null premium can never satisfy a predicate. In plain JavaScript
null < 0.85 is *true*, so an unmodelled name would otherwise sail through a cheapness filter and appear cheap. Both evaluation paths reject it — the structured-filter operator returns false for null, and the where-expression returns null against a === true test — and a test now pins both, documenting the trap explicitly.
- New
screener-premium.test.js (7 tests). 473 tests across 51 suites.
[2.57.0] — Value decomposition, camp 2: premium bands, without lookahead
get_premium_band — where a name's idiosyncratic premium sits in its own history against a cohort: percentile, p10/p25/p50/p75/p90, the window, and a plain reading ("at the top of its own range — priced further above its cohort than it usually is"). This is the treatment the premium legs deserve: the substantiated leg is a fundamental quantity and gets fundamental analysis, while the premiums are market-determined and get the statistical treatment a market series gets.
- A stated, different floor. 12 observations minimum, and it refuses below. The price-based valuation bands require 40 — but premiums are quarterly, so 40 would mean a decade. The lower bar is stated rather than silently relaxed, and the refusal points at
seed_premium_history as the way to deepen it.
- The percentile series does not look ahead.
model:<SYM>:premium_idio_pct ranks each point only against observations that preceded it — an expanding window. Ranking against the full history would leak the future into every past point and make the series look far more informative than it is. Tested directly: a monotonically rising premium ranks at 100 throughout, and a falling one at 0, because that is what an honest expanding rank says.
- The distribution discloses its own composition: how many observations are reconstructions seeded from backfills versus live decompositions, since a percentile built mostly from reconstructions inherits their limitations.
- Percentiles now ride each
decompose_price reading alongside the idiosyncratic premium, so "1.6× against this cohort" arrives with "which is the 90th percentile of its own history". Every band surface carries the recorded negative result. 7 further tests (20 in premium.test.js), 154 tools.
[2.56.0] — Value decomposition, camp 3a: the premium panel, and history without waiting three years
get_premium_panel — the batch cross-section of substantiated value, price and premium that the screener and band machinery read, so nothing has to build models inline (evaluating a premium live across 500 names would mean 500 model builds per query). Coverage is the headline, not a footnote: names without a model are excluded *and counted*, never treated as failing a filter, because a panel that silently drops two thirds of its universe is worse than one that refuses.
seed_premium_history — solves the sequencing problem that would have made premium bands useless. Bands need a dozen observations of a name's own history, which would otherwise take three years to accumulate. But the point-in-time backfill already reconstructs the substantiated leg historically — so a cohort's premium can be rebuilt at each past date from the backfills of its members, and the entire decomposition seeded retroactively.
- Marked, never mistaken. Every seeded row carries
seeded: true, and a real snapshot always wins over a reconstruction on a shared date — a test asserts the live observation survives and the seeded one yields. A reconstruction can never be presented as a decomposition that was actually run at the time.
- It discloses what it inherits, in full: trailing-CAGR growth rather than consensus, judgment drivers carried back from today (hindsight), restatements indistinguishable from originals — plus a new one this camp introduces on its own, survivorship bias: today's cohort membership applied to past dates.
- Refusals hold at every level: no backfill on the name, and it refuses rather than inventing the substantiated leg; no date with at least 12 backfilled cohort members, and it refuses rather than publishing a median of four; a date short of the floor yields no baseline for that date rather than a thin one.
- New
panel.test.js (11 tests), most of them pinning what the module declines to do. 153 tools.
[2.55.0] — Value decomposition, camp 1: the gap was never an error
decompose_price — price splits into substantiated value × cohort premium × idiosyncratic premium, computed for *every* cohort the name belongs to. What the model used to report as one number called "the gap", and treat as error, is now two measurable quantities: how much of it is the whole market re-rating, and how much is this name specifically. On AAPL the distinction inverts the reading — total premium rose 1.28× → 1.60× while the idiosyncratic premium fell 0.70× → 0.61×: relative to peers it got *cheaper*, and all of the apparent re-rating was market-wide.
attribute_return — the payoff of the identity. Because it is multiplicative, logs make it additive, so a move splits exactly: *"AAPL +48.8%: +18.9pp substantiated, +34.9pp cohort premium, −5.0pp idiosyncratic."* The residual is computed and shown rather than hidden, and a test asserts it rounds to zero.
get_cohort_premium — what the market pays a whole group above fundamentals, with dispersion and an auditable member list. This is the term that was polluting every single-name gap: across 45 large caps it ran 1.84× (Jul 2024) → 2.61× (Jul 2026); across sectors, roughly 1.07× (Financials) to 3.58× (Energy).
- Robustness makes cohort shopping visible rather than available. The spread of the idiosyncratic reading across every membership is reported: tight, and the relative reading is not an artifact of which comparison group was picked; wide, and the payload says so in as many words.
- The denominator guard. A premium moves because price moved or because the model's own fair value moved — TSLA's fell 173 → 55 on a single filing, mechanically tripling its premium. Every change now carries
driven_by (price / substantiated / both) and flags a >25% move in the substantiated leg as a model event, not market enthusiasm.
- Honest floors throughout: a cohort premium is built only from members that actually have models, coverage is disclosed on every reading, and fewer than 12 modeled members refuses — absent, not zero, not imputed. Attribution refuses on a single snapshot rather than inventing a baseline.
- Four new chartable series —
model:<SYM>:substantiated, :premium_total, :premium_cohort, :premium_idio[:<cohort>] — whose catalog labels state they are descriptive, not predictive. Every premium surface carries the recorded negative result from the 45-name study, permanently. New premium.test.js (13 tests).
[2.54.0] — Value decomposition, camp 0: the cohort lattice
- A cohort is now a first-class entity, and membership is many-to-many. A security belongs to its sector, its size band, an index, any factor screen it satisfies and any custom list containing it — *simultaneously* — and valuation reads differently against each. So the analytical answer is a set of readings, not a scalar, and
cohorts_for_symbol is the central query.
- Why the shape was forced. A 56-name, 7-sector study measured median price ÷ substantiated value at 1.07× (Financials) to 3.58× (Energy), with 33.6% of premium variance between sectors — so a single universe cohort would mis-assign a third of the premium to the name — and 66.4% within, so sector alone is not enough either. Both numbers are load-bearing; the design is measured, not chosen.
- 20 cohorts ship, and most are generated rather than authored: 11 provider sectors, 3 market-cap bands, 4 factor screens — all rule-based and self-populating, so they cannot rot the way a hand-typed list does.
list_cohorts, get_cohort, create_cohort, delete_cohort round it out; generated cohorts refuse deletion because they are derived rather than stored.
- Curated examples are labelled as the editorial judgments they are. "AI complex" is an opinion about membership, not a taxonomy — shipping it beside "Semiconductors" as though both were facts would smuggle a view into an analytical baseline. They carry
curated_example, say so in their own provenance, are freely deletable, and deletion is tombstoned so the seeder cannot helpfully restore the judgment you just rejected.
- Watchlists are cohorts — bridged by reference, never rewritten.
promote_watchlist_to_cohort makes one curated list serve both roles: still a watchlist for monitoring, now also a baseline others are valued against. Membership is stored once, and resync is deliberate rather than automatic so a comparison baseline never shifts under an analysis unnoticed.
- Honest floors and disclosure: fewer than 12 members and a cohort refuses to supply a premium — a median of four is not a median — while remaining perfectly usable for watching and screening. Rule-based membership is cached with its date always disclosed; without a provider profile, rule-based membership is reported as *absent* rather than guessed.
- Cohort chips render on the Operating Model view, colour-coded by kind. New
cohorts.test.js (13 tests) covers the lattice, the generated/authored split, the tombstones, the cache disclosure, the member floor and the watchlist bridge in both directions.
[2.53.0] — Model honesty, camp 6: a point-in-time track record, or none at all
backfill_model_track — reconstructs what a model WOULD have concluded at past dates, so the accountability machinery (forecast scoring, re-underwriting, the model-gap chart) has evidence now rather than in three years. It had exactly one snapshot before this.
- The rules are absolute, not aspirational. A statement is usable at a date only if it was filed by then — fiscal period end is not knowledge, so FY2024 ending 31 December is invisible until its February filing. Statements without a filing date are never used. And if the provider reports no filing dates at all, the tool refuses outright: guessing a reporting lag would fabricate the very track record it claims to measure. A fabricated history would launder hindsight into apparent skill and every downstream honesty mechanism would inherit the lie.
- What it cannot do is stated on every payload: restatements are indistinguishable from originals in this data; analyst consensus cannot be reconstructed historically, so growth uses trailing CAGR from filed statements and the method therefore *differs* from the live model; judgment drivers (discount rate, exit multiple) are carried back from today, which is hindsight.
- Separate by construction: reconstructions are stored apart from the live track and carry their own series ref —
model:<SYM>:backfill_fv, whose catalog label reads *"BACKFILLED … not calls that were made"* in the very text you pick it by. A reconstruction can never be charted as a call that was actually made.
- Scoring refuses more than it reports. Fewer than three completed forward windows yields no summary at all ("computing one anyway would be the failure this module exists to avoid"). When a regression is possible it is graded like every other estimate and ships its own reasons for doubt: overlapping windows, no search-ledger deflation, no holdout, one name — *"a hint to investigate, not evidence of edge"*.
- 9 further tests (48 in
model-honesty.test.js), most of them asserting what the module refuses to do. MODEL-HONESTY-SPEC is now fully executed.
[2.52.0] — Model honesty, camp 5: scope — the model says what it cannot see
- A large gap is now a question, not a verdict. The TSLA model concluded $17 against a $381 price and reported −95% with a straight face. That is not a valuation opinion; it is a five-year DCF on car margins failing to represent what the market is paying for. Gap triage now runs automatically past a ±35% gap: the system reverse-solves *itself* and classifies. On TSLA it finds that no revenue growth between −40% and +120% and no gross margin up to 95% produces $381 — so the verdict reads *"the gap is not an opinion about these drivers"* rather than a −95% call.
- Two classifications, both earned by an actual solve.
drivers_disputable names the assumption the price requires ("growth would have to be X, you assume Y — get_base_rates puts that against the cohort"); value_outside_model_scope says plainly that the price is unreachable inside the model. A gap the model explains is left alone — triage does not manufacture a scope problem.
- The unmodeled register (
set_unmodeled): declare what a model does not capture — a business line, optionality, an asset the DCF cannot see. Fair value is then reported as modeled core with declared items listed beside it. Value ranges are optional by design: an item carried unvalued stays excluded from every total, because *"I cannot value this"* is a position, not a gap to fill with a guess. Declaring scope changes the prompt — the gap becomes expected rather than unexplained.
- Silence about scope is itself reported: a model with nothing declared is told that an undeclared omission reads as a valuation opinion. 8 further tests (39 in
model-honesty.test.js), including that the modeled core is never absorbed into a total and that nameless items and inverted ranges are refused.
[2.51.0] — Model honesty, camp 4: calibration coherence and driver fade
- The source contradiction is named. Calibration takes revenue growth from analyst consensus and margins from the last reported year "for internal consistency" — but the analysts supplying that growth generally assume margin expansion too, so the model was running their volumes against profitability they never forecast, and saying nothing.
calibration_coherence now reports the conflict with its reasoning and remedy. A self-consistent calibration is not accused; the check discriminates.
- Holding a level flat is treated as the forecast it is. Revenue growth always decayed toward terminal while gross margin, opex, D&A and capex intensity were pinned for the entire horizon — an unstated claim that the company escapes the reversion its cohort typically shows. The coherence block now states which drivers are being held flat and why that is an assumption, quoting the base-rate engine's own finding.
set_model_fade — new tool. Set explicit terminal levels ({gross_margin_pct: 45}) or adopt the cohort fade path with from_base_rates: true, which reads the existing base-rate engine rather than inventing a curve; it refuses when the cohort produces no usable path rather than making one up. Faded drivers move linearly from their year-1 value to the terminal level, and the fade flows through the projection, the valuation, the elasticities and the uncertainty band alike.
- Never applied silently: without a fade map the projection is byte-identical to before, so both choices are now explicit and disclosed — flat is a decision you can see rather than a default you never noticed. 7 further tests (31 in
model-honesty.test.js), including that year 1 is unchanged, that interpolation is linear at the midpoint, and that a faded driver stops being reported as "held flat".
[2.50.0] — Model honesty, camp 3: the influence map stops flattering itself
- Model completeness, measured. A graph with fifteen factors and seventeen edges looks authoritative even when nine factors carry no value and every edge is a catalog prior nobody fitted.
provenanceSummary now scores exactly that — factors populated, edges fitted vs catalog default, drivers still sitting on calibration defaults — into one completeness score out of 100 with a plain verdict: *substantially calibrated* / *partly calibrated* / *largely uncalibrated scaffolding*. The scoring is deliberately unforgiving: an empty factor and an unfitted edge both count zero, because both are scaffolding presented as structure. A real TSLA model scores 22/100.
- Empty factors are hidden by default in the influence map, with the count and their names kept visible and a one-click *show them* — hiding as disclosure, never concealment. The map never blanks entirely.
- The score rides the graph payload itself, so the picture and the number are computed from the same registries and cannot drift apart, and
provenance_summary is returned by get_company_model / build_company_model for agent parity.
- Fitted edges were already drawn differently from catalog priors (weight and dash by grade); the missing half was saying so numerically. 6 further tests (24 in
model-honesty.test.js), including that a fully calibrated model scores ≥70 — the metric has to be able to say yes — and that an empty model scores *nothing* rather than scoring zero as a fact.
[2.49.0] — Model honesty, camp 2: fair value stops pretending to be a number
- The output finally joins the epistemic spine. Every model input was graded, deflated and holdout-tested, and then the model printed
blended_fair_value: 17.21 — a scalar, to the cent, with no interval and no grade. Fair value is now a distribution: each driver carries an uncertainty band derived from its own provenance (as-reported → narrow and grade A; fitted → grade B; consensus/judgment → C; catalog default → widest band and grade D), drivers that share an influence-graph factor parent move together rather than independently, and the projection is resampled to yield P10 / P50 / P90.
- The grade is inherited, and materiality is measured. The reported grade is the weakest input the answer *materially* depends on — materiality determined by each driver's own elasticity, not asserted — and the payload names the drivers that cap it.
- Precision follows the interval. A value known to ±50% no longer carries cents; the rounding step is derived from the width of the band. And when P90 exceeds 3× P10, or the answer rests on a grade-D input, the point estimate is withheld entirely — the range is the honest answer. On a default-laden model,
$17.21 now reads $8–$35 (grade D, no point estimate).
- Terminal-heavy valuations are reframed: above 60% terminal share the payload says plainly that this is an assertion about the exit multiple and discount rate, not a cash-flow valuation.
- Deterministic by design — a seeded sampler, so the same model yields the same interval on every refresh; a band that wandered would be its own dishonesty. Absence is honest too: a model with no bandable drivers reports that no distribution can be built rather than inventing one, and the disclosure states what the interval is *not* — model-input uncertainty, never a confidence interval for the stock price.
- The Operating Model view now leads with the range, its grade and what governs it, demoting the scalar to a reference figure.
fair_value_distribution rides get_company_model and build_company_model for the agent. 10 further tests in model-honesty.test.js (18 total).
[2.48.0] — Model honesty, camp 1: elasticities reconciled with the influence graph
- The contradiction is fixed. The elasticity table bumped one driver at a time while the influence graph asserted that a single external factor drives several drivers at once — so the two disagreed in public. On a net-cash balance sheet the old table showed
interest_rate_pct +1pp → fair value +5.2%, which reads as "rates are good for this company"; the graph simultaneously routed the same rate move into the discount rate the other way. New factor_elasticities bumps the FACTOR and propagates through every edge, reporting direct (the strongest single edge, what the driver table effectively showed) beside total (all edges together), with the indirect offset between them — the model's own account of how much of the naive answer is cancelled by paths it already knew about. Factor weights scale propagation.
- Sign disagreements are named, not silently resolved. When the lead edge and the full graph point opposite ways, the row says so explicitly and tells you to trust the total.
- Unstable elasticities are flagged. An elasticity is a ratio, and against a near-zero base it overstates sensitivity: a company earning 1.2% FCF margin shows "capex +1pp moves fair value 15%" because the denominator is nearly zero. Rows now carry
unstable with the measured reason, and the model view leads with the warning. A healthy-margin business is not flagged — the check discriminates.
- Both tables render in the Operating Model view with edge provenance disclosed, and both ride
get_company_model / build_company_model for the agent. New model-honesty.test.js suite (8 tests) pins the reconciliation, the naming of disagreements, weight scaling, honest absence when a model has no edges, and the discrimination of the stability check.
[2.47.0] — EV roll-forward bands and continuous crosshair linking
band:<SYM>:ev_ebitda:<percentile> — EV/EBITDA valuation bands, previously refused because a daily enterprise value would have required inventing daily debt. The EV roll-forward makes it honest: EV(t) = daily market cap + net debt held constant at its last reported balance-sheet value between filings — a disclosed step function, never an interpolation. The implied price line backs net debt out again ((multiple × TTM EBITDA − net debt) / shares), the label discloses the roll-forward and observation count, thin history (<40 observations) still renders as absence, and a percentile that implies non-positive equity refuses rather than drawing nonsense. One-click 10/35/65/90 set beside P/E and P/S in the Macro/Data panel.
- Continuous crosshair linking for grid layouts: the ⌖ Link crosshair toggle makes hovering any grid cell draw a live vertical crosshair at the same date in every other cell (as-of snapped when calendars differ), so AAPL's price, TSLA's band position and the model gap read at the same instant. Throttled at the source, relayed only between your own same-origin cells, and external crosshairs never re-emit — no ping-pong. Range sync (⇄) remains the explicit one-shot it was.
- Agent parity:
control_terminal link_crosshair {on?}; terminal state reports grid.link_crosshair. Contract-tested end to end: message names, engine verbs, the no-re-emit rule, the toggle gate, and same-origin discipline.
[2.46.0] — Charting arc, camp 4: multi-chart layouts
- Grid layouts in the chart view: 1, 2-across, or 2×2 — cell one is the active tab, extra cells default to your other open security tabs, and each cell's symbol is one click to change. Every cell is a full independent chart: its own overlays, indicators, transforms, expressions and bands, with per-symbol isolation holding per cell by construction — Compare remains the only data crossover.
- Range sync: the ⇄ button pushes the primary cell's date window to every other cell — explicit and observable rather than an invisible continuous link (continuous crosshair sync is the stated next step). Same-origin checks guard the relay on both sides.
- Agent parity from day one:
control_terminal set_grid {mode, symbols?} and sync_grid_ranges; the grid is reported in terminal state. Contract-tested, including the isolation guarantee.
[2.45.0] — Charting arc, camp 5: the performance envelope, measured
- The harness's performance suite now covers the extreme charting envelope: the full indicator battery at 25k and 50k bars, and the new series machinery at scale — TTM/per-share transforms over 120 quarters, expression evaluation over 7,500 daily points, valuation-band construction over 7,500 multiple observations — all against explicit budgets that fail on order-of-magnitude regressions.
- The measured result: the 50k-bar battery completes in tens of milliseconds and every series operation in single-digit milliseconds. Computation is not the binding constraint at any realistic chart size; provider latency is. The envelope is documented in the knowledge base (
guide.performance_envelope) from the measured numbers, and re-measurable any time via run_test_suite — measured, not assumed, and no premature optimization shipped.
[2.44.0] — Charting arc, camp 3: valuation bands
band:<SYM>:<pe|ps>:<percentile> — the price implied by holding the multiple at a chosen percentile of its OWN trailing distribution, drawn through time on the price scale. Fundamentals come through the transform pipeline (true TTM EPS / TTM sales per share), so the bands inherit every honesty rule already in the machinery. The label discloses the multiple and the observation count; fewer than 40 multiple observations is refused — a band from thin history is a guess, and absence renders as absence.
- One-click band sets in the Macro/Data panel: P/E bands and P/S bands (10/35/65/90) for the active symbol, added together in price mode. EV/EBITDA bands are deliberately absent until the EV roll-forward is wired — the help text says so rather than shipping a fiction.
[2.43.0] — Charting arc, camp 2: expression series and the accountability chart
expr: refs — arithmetic over resolved series (+ - * /, parentheses, numeric literals), as-of aligned to the first series leaf: expr:(price:AAPL)/(price:MSFT) is relative value; expr:(fmp:income:AAPL:eps:quarter:ttm)*25 is a P/E-25 line; transforms compose inside. Division by ~zero yields a gap, never an infinity.
price:<SYM> joins the ref grammar as daily closes, usable anywhere a series ref works.
- The accountability chart, one click:
expr:(model:SYM:fair_value)-(price:SYM) — the model gap as its own series — leads the Operating-model quick-picks in the Macro/Data panel. When that line trends toward zero the model is being vindicated; when it widens, one of them is wrong.
- Discoverability law honored: the expression grammar self-describes (
EXPR_HELP), rides the series catalog into the KB, and the panel's search hint teaches the syntax inline.
[2.42.0] — Charting arc, camp 1: series transforms
- Composable transforms on any series ref:
:ttm (rolling 4-quarter sum — refuses non-quarterly cadence and says to add :quarter), :per_share (as-of aligned diluted shares; refuses symbol-less refs), :yoy (±45-day matched, unmatched points drop out), :indexed (rebase 100). They compose left to right: fmp:income:AAPL:netIncome:quarter:ttm:per_share is true TTM EPS. Labels state the applied pipeline.
- Discoverability by design (the new law from CHARTING-SPEC): the transforms live in one self-describing registry that the server applies, the field browser's new transform selector enumerates (via
/series/transforms — never a hand-copied list), and the knowledge base compiles (transform.* entries). A transform cannot exist without its affordance.
[2.41.4] — Chart isolation: overlays and indicators are scoped per symbol
- Fixed overlays bleeding across charts (TSLA series appearing on the AAPL chart): the chart layout — indicators, overlays, style — was saved under one GLOBAL key shared by every chart, while drawings were always correctly per-symbol. The layout store is now keyed by symbol, switching symbols reloads that symbol's own layout, and a one-time migration keeps your indicator setup while dropping overlays bound to a different symbol. Compare remains the only cross-symbol mechanism, as designed. A contract test now enforces the isolation.
[2.41.3] — Support view Guide/What's-new crash fixed
- The Support view failed with "Can't find variable: g": the client source is embedded in a template literal, so the regex escape
\*\* collapsed at parse into //g — an empty comment plus a bare identifier, syntactically valid and invisible to every parse guard, a ReferenceError at runtime. Fixed, and a new release-gated scan now rejects any collapsed-regex landmine (// followed by an identifier) in the generated client scripts — the whole class, not just this instance.
[2.41.2] — Statement series are one click, not a magic search term
- The chart's 🌐 Macro/Data panel gains a Company financials quick-pick section for the active symbol — EPS, Revenue, Net income, Free cash flow, Gross margin — each one click into its own pane, with a note that
:quarter on the ref gives quarterly — plus a full field browser: pick any of the five statements (income, balance sheet, cash flow, ratios, key metrics), annual or quarterly, and every numeric field the provider actually returns is listed with its latest value, one click to overlay. The field list is enumerated from the live data row, so it can never disagree with what the provider serves. The capability existed (fmp:income:<SYM>:eps has resolved for many releases) but was reachable only by typing the ref grammar into search — the same discoverability failure as the model series, fixed the same way.
[2.41.1] — Diagnostics fix its own first false alarm
daemon.reachable probed /health; the daemon answers on /_health — a running daemon was reported down. Caught by the check's first live run. Fixed.
[2.41.0] — Self-support complete: the Support view
- A pinned 🛟 Support tab joins the workflow order with four sub-views: Guide (browsable topics + code-generated reference, each stamped with the KB version), Diagnostics (one-click run of the same engine the agent uses via the same daemon routes; findings shown observed-vs-expected with the remedy one click away; last run persisted), What's new (structured release history), and Learn (the curriculum with build-time-computed worked examples).
- "Try it with the agent": every Learn module's schema-validated invocation can be handed to the agent through the ✉ inbox — the UI presents, the agent executes, both from the same module. One KB id space, two renderings: the entry the agent cites in chat is the page the user opens in the terminal.
- Agent parity holds by construction: the view is reachable via control_terminal, readable via inspect_terminal, and served from the identical support-kb/diagnostics modules behind the /support/* routes.
[2.40.0] — Self-support, camp 3 (agent side): education that executes
explain_concept / get_learning_path: a six-module curriculum (operator → analyst → methodologist) teaching the methodology — evidence grades and ceilings, the search ledger, YoY same-quarter discipline, the articulation tie, segment mix as an output, terminal basics. Every module's worked example is computed by the real functions at build time (the deflation example calls the live Šidák code; the tie example runs a real articulated projection and shows gap 0) — the release fails if an example stops matching the software. Every try_it is a literal, schema-validated tool invocation the agent can run against your data; an invocation citing a tool or argument that no longer exists fails the build.
- The cockpit knows about support:
get_workspace_state gains a support block (KB availability and version, last diagnostic run), and failing diagnostic checks surface in the attention list the way fired alerts do.
- Education joins the search index. Coming next: the terminal Support view (guide browser, one-click diagnostics, what's new, Learn with "try it with the agent" handoff) completing the co-equal UX integration.
[2.39.0] — Self-support, camp 2: troubleshooting as behavior
diagnose: the diagnostic engine executes real checks — node version, key presence and a classifying authenticated probe (invalid / plan-limited / rate-limited, never a generic error), daemon reachability and version match against this build (the stale-daemon failure made visible), state-file integrity naming any corrupt file, disk writability, and the release channel. Findings are observed-vs-expected, each bound to a remedy; runs work with zero keys configured, never print key material, are journaled, and persist for the UI. The engine self-describes its full check inventory including what it cannot see.
get_troubleshooting: symptom → causes → the executable check that confirms each → remedy. The release lint enforces the binding in both directions — no cause may cite a check that does not execute, and no check may emit a remedy that has no entry. The prescribed workflow is built into the payload: match the symptom, confirm with diagnose, then remedy — observe, never guess.
- Troubleshooting entries join the search index, so "missing feature" finds the stale-daemon entry with its check and fix.
[2.38.0] — Self-support, camp 1: the knowledge base is generated from the code it describes
- Three new tools:
get_user_guide (enumerable TOC + guide topics + reference sections), search_support (canonical excerpts with kb ids and version stamps), get_whats_new (structured release history). Every payload is stamped with the version it was generated from; every tool description carries the retrieval doctrine: answer from the payload only, cite ids, and say when the knowledge base does not cover a question — never answer from general knowledge, which cannot know this version of the software.
- The KB is compiled from the registries at build time (
gen-kb): the tool reference IS the tool registry (the same objects served to Claude), drivers from DRIVER_DEFS, factors from FACTOR_DEFS, evidence grades and method ceilings from inference.js, ~116 indicators from the workbench registry, the series catalog, the changelog, and the content-hashed terms. The governing rule: a capability describes itself, colocated with its code; authored prose is reserved for cross-capability knowledge no single capability can own.
- The release gate refuses drift:
gen-kb --lint fails the build if any guide topic references a tool or code symbol that no longer exists — documentation cannot ship disagreeing with the software. The lint itself is tested (a planted dangling reference must be caught).
- Introspection of the introspection: the KB manifest (source hashes, generation time, lint status, generated-vs-authored inventory) is queryable, so the agent can verify the knowledge base is fresh relative to the running version.
[2.37.6] — The chart fits its window again
- Fixed resize overflow: pane heights were computed from the viewport once at build time and frozen; resizing the window (or the terminal pane) only refitted widths, so the chart overflowed and pushed the bottom controls — ranges, interval, dates — out of view. A height change now recomputes the whole layout (debounced), and the controls stay visible at every size.
[2.37.5] — Fair value belongs on the price axis
- New overlay mode
price: draws a series on the symbol's own price scale instead of a stacked side axis or pane. model:<SYM>:fair_value and forecast_fv default to it — a fair value is in dollars for this very symbol, and rendering it on a compressed side axis visually parked a $17 conclusion at mid-chart next to a $380 price. On the price scale, the FV-vs-price gap IS the picture. Mode remains switchable per overlay (axis | pane | price), including via control_chart.
[2.37.4] — A model saved on Saturday is no longer invisible on the chart
- Fixed overlay alignment dropping post-close conclusions: model track points are date-stamped in UTC, so a conclusion saved after Friday's close (or on a weekend) is dated after the last trading bar — and the as-of alignment rendered it NOWHERE, leaving the "model fair value" pane empty with data present. Points recorded after the last bar now land on the most recent bar.
- Sparse series are visible: a young model track with one or two points now draws explicit point markers instead of an invisible zero-segment line — the track becomes a line as history accrues.
[2.37.3] — Model conclusions visible in the Macro/Data panel, not hidden behind a search term
- The chart's 🌐 Macro/Data panel now shows an "Operating model conclusions" section for the active symbol — one-click quick-picks for
fair_value, forecast_fv, eps_yr1 and eps_yr5, beside the macro quick-picks, with a note that points appear as the model is saved and attributed. Previously the Model category existed only in search results, which required already knowing to type "model".
[2.37.2] — Model conclusions in the chart's own overlay picker
- The workbench overlay search now carries a Model category:
model:<SYM>:fair_value, model:<SYM>:forecast_fv (attribution runs), model:<SYM>:eps_yr1 and model:<SYM>:eps_yr5 appear beside the economic/rates/commodity catalog — type "model" in the chart's overlay search and the model's own dated conclusions overlay directly on price. Previously these series resolved but were agent-only knowledge: nothing in the UI told you they existed.
[2.37.1] — Forecast & attribution: no more zeros, and no more garbage either
- Fixed forecast-and-attribute reporting zeros on legacy models: the forecast path never received the 2.29.1 series-ref migration that calibration got, so models seeded before factors carried refs skipped EVERY factor and reported a confident +0.00%.
forecast_and_attribute now runs the same ensure() backfill (persisted on save), and factors that genuinely have no observable series are skipped with instructions instead of a bare "no series ref".
- Fixed a units bug the zeros were hiding:
yoy-mode factors (inflation, USD, commodities) were forecast as raw index levels — a CPI trend of −54 index points was fed to a sensitivity calibrated in YoY percentage points, fabricating a +104% fair-value contribution. These factors are now forecast in YoY% space (matching how their sensitivities are calibrated and how their levels are read), with the unit stated on the forecast. LEVEL_MODE is now a single shared definition, so calibration and forecasting can never disagree about units again.
- The attribution track record actually records:
recordAttribution read a result key that never existed (contributions instead of per_factor), so model:<SYM>:attr:<factor> and model:<SYM>:forecast_fv series were always empty. The run now concludes a real forecast fair value and per-factor contributions land in the track — the chartable series work.
- Absence, not zero: when every factor is skipped, the totals are now null with a note ("an absence of data, not a zero-impact forecast"), never +0.00%.
[2.37.0] — The deflation is live; the mountain is documented
- Fixed the acknowledged Šidák dead-code defect: every rule backtest now runs a two-sided t-test of the mean per-trade return against zero, and that nominal p-value flows into the search ledger where it is actually deflated by the number of distinct rules tried (a nominal p=0.03 after 10 tries deflates to ≈0.26 — and the result says so). Previously the deflation machinery existed but no backtest ever computed a p-value to feed it. Fewer than three closed trades yields no p at all, with the reason stated — never a fabricated significance.
- Docs: the user guide's operating-model chapter gains "Going deep: segments, quarters, and the full three statements" — the three ascents (segment-scoped drivers with chartable mix shift, quarterly fits at the honestly-earned A ceiling plus the quarterly EPS scoreboard, and articulated three-statement mode with the enforced tie) with worked "you say / it does" examples and the honesty rules that govern each. The backtester section and the site front page now describe the live deflation pipeline and the deep-model capabilities.
[2.36.0] — Ascent 3: three-statement articulation. The mountain is climbed.
- The projection now articulates: setting any of the new drivers (
payout_ratio_pct, buyback_pct_fcf, target_leverage) switches a model into three-statement mode — earnings → retained earnings → equity; capex − D&A → net fixed assets; working-capital deltas and FCF less dividends and buybacks → cash; interest recomputed from average net debt (fixed-point), so a leveraged company's operating deltas compound through the balance sheet instead of paying interest on a frozen opening number.
- The tie is a test, not a hope: assets = liabilities + equity is enforced in EVERY projected year — a model that cannot tie throws, naming the year and the gap, because a flow booked to one side only is a model defect, not a rounding issue.
- Buybacks retire real dollars:
buyback_pct_fcf spends a share of FCF at the base price and the share count falls by shares actually repurchased — a buyback shock changes EPS through the count, not through a multiplier. Target leverage works through capital return: an under-levered sheet reaches its target by returning the excess via extra buybacks; an over-levered one deleverages only as FCF accumulates — no fictional instant repayment, and no paper debt moves (issuing debt into cash cannot change net debt).
- Honesty rules: articulated mode refuses to run without an opening equity rather than fabricate a balance sheet; buyback dollars without a base price refuse to guess a share count;
build_company_model now captures the opening sheet and reports derived-from-history starting points for the three drivers (latest payout, buyback intensity, leverage) — shown, never applied.
- Full back-compat, tested: models without the new drivers project byte-identically to the legacy engine.
[2.35.0] — Ascent 2 complete: the scoreboard accrues quarterly
- Quarterly forecast scoring in
get_model_events (quarterly_scoring): each dated model-track snapshot becomes a next-quarter EPS checkpoint — the annual conclusion converted through the company's OWN seasonal EPS mix (median share of annual EPS per fiscal quarter over complete profitable years, a reported fact) and scored against the reported quarter within weeks of the print. MAE, bias and a leaning per model; the accountability loop runs four times faster than annual scoring.
- Honesty rules: the quarterly number is labelled a derived checkpoint, not a quarterly model; the LAST snapshot before each print is the one scored (no hindsight re-picks); loss quarters and partial years are excluded from the mix; fewer than two complete profitable years means no mix, never a guess; an empty scoreboard says it fills in as earnings arrive — it never shows zeros.
[2.34.0] — Ascent 2, first camp: quarterly frequency
- Quarterly driver history (
quarterlyDriverHistory): every driver assembled per fiscal quarter from FMP quarterly statements, labelled by the statement's own period field (calendar-month guessing only as a stated fallback). ~60 points where annual data offers ~15.
- Seasonality is structure, never signal: growth and all fit series are year-over-year same-quarter — a strong-Q4 business shows constant growth, not a seasonal sawtooth, and sequential quarter-on-quarter is never regressed.
calibrate_model_factors gains period:'quarter': factor series are aggregated to quarterly averages and YoY-transformed exactly like the driver side, fits run through the inference spine under the quarterly_regression ceiling — the A grade that ~15 annual points can never honestly reach is now earnable on ~60 quarterly points that measure it.
- Breaks at quarter grain: a structural break carrying a quarter (
2020Q2) disqualifies fits at that resolution; year-grain breaks land at that year's Q1 and still disqualify. accept:true never applies a break-spanning fit.
- Fixed a latent grader crash: an A-branch fit with an undefined r² (zero-variance outcome) crashed instead of reporting "r² undefined — outcome has no variance".
[2.33.0] — Ascent 1 complete: factor edges reach individual segments
- Factor edges may now target scoped drivers:
tpu_supply → revenue_growth_pct@cloud is a legal edge, so a supply-chain cause moves the Cloud assumption while Search stands untouched. Cycle detection and path-product propagation carry through unchanged.
- Shocks land honestly at three levels: on the scoped driver when the model carries it; folded into the consolidated driver at the segment's mix weight when it doesn't (a 2pp Cloud shock on a 25% segment is ~0.5pp of the book, and the fold is reported in
scoped_folds); and DROPPED with instructions — never guessed — when there is neither a scoped assumption nor a mix to weight by.
- Scoped edges fit on segment data through the inference spine: calibration fits
factor → revenue_growth_pct@segment against the segment's own reported YoY growth series, graded A–D like every other fit; re-segmentations arrive as structural breaks that disqualify spanning fits even under accept:true.
- Segment mix is chartable:
model:<SYM>:mix:<segment> resolves the disclosed mix history as a series — overlay Cloud's share of revenue directly on the price chart.
shock_company_model accepts scoped driver keys, with a clear error pointing at get_segments apply_to_model when the scoped assumption is missing.
[2.32.0] — Ascent 1, third camp: disclosure wired into the model
get_segments gains apply_to_model: the latest *tied* disclosure period becomes base.segment_mix, and each segment receives a scoped growth default — the median of its recent reported YoY prints, explicitly labelled a reported-fact starting point and NOT a forecast. The model immediately projects with mix-weighted blended growth and mix shift as an output.
- Honesty rules carried through: an existing scoped assumption is never overwritten (the analyst's number stands), the
unallocated residual holds its mix weight but takes no growth default (it grows at the consolidated rate), and seeding is refused outright when no period ties to consolidated revenue — a mix built from suspect data would poison every projection downstream.
[2.31.0] — Ascent 1, second camp: segment-scoped growth drivers
- Drivers may now be scoped to a segment:
revenue_growth_pct@cloud alongside the consolidated assumption. With base.segment_mix set (from get_segments), each year's consolidated growth is the mix-weighted blend — scoped segments grow at their own faded rate, unscoped segments at the consolidated one — and the projection carries segment_mix per year, so mix shift is an output, not an input: a segment growing 30% against a 5% book climbs from 25% to ~37% of revenue over five years, and you watch it happen.
- Honest edges: a scoped rate without a mix is ignored rather than guessed at, models without scoped drivers project byte-identically to before, and the mix always renormalizes to 100.
[2.30.0] — Ascent 1 begins: segment-level revenue history
[2.29.1] — Fix: a confident 0.00% that actually meant "no data"
- Legacy models could never calibrate. Models seeded before factors carried a
series ref (the GOOG case) had catalog factors with no way to resolve data — Series showed "—", levels stayed null, every forward projection was flat, and attribution reported +0.00% as if it were a conclusion. ensure() now migrates old models by backfilling the catalog FRED ref, so Calibrate from FRED works on them like on new ones.
- The attribution panel now says "nothing to attribute yet" — with the reason and the remedy (calibrate, or attach series-backed factors) — whenever every factor forecast is flat, instead of presenting a data-free zero as a forecast. A zero that means "no data" saying so is the product's own standard.
[2.29.0] — Attribution runs are time series, and chart like them
- Correcting an earlier claim: attribution is a vector at one moment, but every run is timestamped with its inputs and outputs — so each factor's contribution ACROSS runs is a time series, and so is the attributed forecast fair value.
forecast_and_attribute now records each run, and two new series refs chart the history through the standard overlay machinery: model:<SYM>:forecast_fv (the attributed forecast over time) and model:<SYM>:attr:<factor> (one factor's fair-value contribution over time, in %). Overlay them on the price chart or stack several factors in panes to watch the attribution composition evolve.
[2.28.1] — The model view can build chains by hand
- The Add factor control's target list now includes existing nodes alongside the financial drivers, so multi-hop chains (materials → unit cost → margin) can be built directly in the Operating Model view, not only through the agent tools.
[2.28.0] — The operating model becomes a real causal graph, and starts keeping its own score
- Any cause, not just macro. Model nodes now cover whatever actually moves the company — supply chains, vendors, energy, raw materials, labor, new products, sales channels, competition, recalls, regulation — with an OPEN category vocabulary instead of a fixed list. The old closed list was forcing real-world causes into whichever bucket fit least badly.
- Causes chain. Edges may land on another node, not only on a financial driver:
lithium_price → cell_cost → gross_margin_pct, or recall → warranty_cost → opex alongside recall → brand → revenue growth. A shock propagates along every path as the product of sensitivities, and each completed path is traced so the answer reads as a story. Dead ends are reported ("this node reaches no financial driver"), cycles are rejected at link time, and paths cap at depth 8 because a longer causal story is a modelling smell.
- Deep chains can earn grades. When both ends of a node→node edge carry a series ref (lithium price → a vendor's reported input costs), the link is FITTED series-to-series with full inference, not just estimated. The influence map lays nodes out by topological depth, so chains render as chains, with stage nodes tinted per category (open categories get a stable generated hue).
- The model keeps a dated track of its own conclusions. Every save snapshots concluded fair value, EPS (yr1/yr5), the drivers, and the price at the time — deduplicated per day unless the conclusion moved, capped at 400.
modelDrift compares snapshots and names which ASSUMPTIONS moved and by how much: the model shifting is a different fact from the stock moving, and it is the one you did to yourself.
- The model is chartable against the price — the only real test. New series refs
model:<SYM>:fair_value, model:<SYM>:eps_yr1, model:<SYM>:eps_yr5 resolve from the snapshot track through the SAME overlay machinery as FRED or a commodity, so control_chart add_overlay {ref: "model:AAPL:fair_value"} draws what the model believed, when, on top of what the stock actually did. The Operating Model view gains a Model track record table (date, price, fair value, FV-vs-price gap, EPS, node/edge counts).
[2.27.1] — Fix: the CI UI check assumed a portfolio existed
- The headless UI check drives the portfolio views, whose rail items only render once a portfolio exists — true on any machine that has used Open Trading Surface, false on a fresh CI runner, so the check's very first CI run failed on "rail item missing". The test now seeds a throwaway portfolio through the real code path when no store is provided, and explicitly selects it before driving the views. Failure output from the UI harness now surfaces the actual error line instead of the last three lines of a stack trace.
[2.27.0] — The strategy reaches the screen: graded visuals, honest rebalance costs, CI-verified UI
- The influence map now looks as certain as the data, not as certain as the artist. Each link is drawn with the confidence its evidence earns: solid and weighted only for statistically supported fits (grade A/B), dashed for weak fits (C/D), sparse dots for catalog estimates never fitted against data — with the grade and its reason in the hover. A legend states the visual grammar. The graph payload (
get_model_graph, /cm/graph) now carries grade, p, fitted and source per edge.
- Rebalance plans state their full cost and the do-nothing hurdle.
get_rebalance_plan now reports turnover, trading cost (cost_bps per side, default 5), and estimated tax on the gain share of each sell (tax_rate_pct, default 20 — pass 0 for sheltered accounts; selling a loser is recognised as tax-free), then says plainly: *this plan starts X% of equity behind doing nothing, which is free.* Assumptions are disclosed in the response, not buried in a constant.
- The headless UI check now runs in CI. It boots the real server and terminal in headless Chromium, drives the portfolio views, and fails on client-side defects. Provider failures (CI uses a demo key) are expected and filtered — the gate is for script errors and broken rendering, not for the network being absent. Locally it still skips cleanly when no browser is provisioned.
- The guide documents phases 4–6: a new "Honest numbers" section covers grades and method ceilings, base rates and fade, implied expectations, the holdout-and-search-ledger backtester, the re-underwrite queue, grade-capped sizing, the adversarial gate and the decision scorecard.
[2.26.0] — Phase 6: the adversarial pass, and scoring decisions instead of luck
- A thesis cannot graduate to
active until someone has argued the other side. The same reasoning that proposes a thesis compiles its screen, builds its book and reads its attribution — apparent confirmation is partly self-generated, and the invalidation triggers were written by the thesis's own author. New challenge_thesis records the STRONGEST opposing case from the same data (fade curves, implied expectations, weak-grade sensitivities are all ammunition); a token objection is rejected for lack of substance. Activation without a challenge throws; force:true overrides but leaves a permanent mark in the review log.
- New
get_risk_register: active convictions nobody has argued against, plus decisions past their stated horizon and unresolved. That list is where the next loss is most likely hiding.
- Decision records demand a falsifier.
record_decision refuses anything without an expectation, a horizon ("eventually" is unfalsifiable) and the observable that would prove it wrong — a decision without a falsifier is a mood: nothing can ever force you to admit it failed, so nothing is learned. The do-nothing case rides along, because the alternative to every trade is free. Recording a deliberate HOLD is as scoreable as a trade.
- Resolution is one-shot and honest about the worst outcome.
resolve_decision closes a record against its own stated terms; right_for_wrong_reason is a separate verdict and the scorecard singles it out as the dangerous column — it pays you to keep a broken process. get_decision_scorecard reports hit rate over judged calls and names every overdue unresolved decision, scoring decisions rather than portfolio return, because return conflates process with luck.
[2.25.0] — Phase 5: the push half — re-underwriting when it matters, sizing by what you know
- New
get_reunderwrite_queue. The pieces existed — events resolve to the drivers they bear on, models know what they depend on, worldview drift flags theses — but nothing pushed; the user had to think to ask. The queue scans holdings and watchlist names and asks one question per name: has anything happened that bears on a driver this position depends on, since it was last underwritten? Reasons, loudest first: structural breaks (every fitted sensitivity spanning one is suspect), events bearing on a depended-on driver (grouped per driver via model_link), worldview drift under an associated thesis, systematic forecast bias (re-derive the assumption, don't adjust it by feel), and staleness — including "never underwritten: held on memory alone". Events already seen at the last underwrite don't re-queue a name. Speed matters most where coverage is thinnest; an entry is a prompt to re-underwrite, never a signal to trade.
- New
propose_sizing — where calibration becomes geometric return. Rank-proportional weighting ignored both how well a position is understood and what the price demands. Now the evidence grade caps size (A 12% / B 8% / C 4% / D 1.5% — an edge you don't understand is one you can't hold through drawdown) and the expectations read sets the multiplier within the cap (undemanding price 1.0×, down to 0.25× for a price requiring a rare cohort outcome; unknown edge costs half). Weights scale *down* to fit and never up: one C-grade idea leaves the book mostly in cash, because a thin opportunity set should look thin.
- Deliberately not Kelly, and the plan says why: Kelly needs win probabilities and payoff ratios measured far better than this data supports, and overbetting on misestimated inputs is ruinous. The full mapping is disclosed in every response rather than optimised in private, the do-nothing row is always present ("doing nothing is free"), and the module never touches a portfolio.
[2.24.0] — Phase 4: the discovery surface stops flattering you
- A search ledger counts every rule you have tried on a symbol. The best of 40 random rules looks impressive; that is a property of 40, not of the rule — and the 40 used to be invisible. Every
backtest_rule run is recorded, distinct rules are counted (re-running the same rule is one attempt), and each result now carries search.rules_tried_on_this_symbol plus a Šidák-deflated significance: p=0.04 after 20 tries is ~0.56 — a coin flip, not a discovery. The correction is conservative in the right direction, since correlated rules make the effective search smaller than the count.
- Every backtest reserves a holdout, structurally. The most recent ~25% of bars are split off inside
runRule itself, so no caller — UI, tool, or agent — can request the flattering half alone. Both windows are scored and compared, with a verdict: a holdout return collapsing to a fraction of in-sample is named as the signature of an overfit rule, a rule that never triggers out-of-sample is named as saying nothing, and a sample too short to split is stamped ENTIRELY IN-SAMPLE rather than silently passing. (Discipline still matters — iterating against the full chart means you have seen the "holdout" — but the honest workflow is now the default, not an option.)
- The universe starts archiving itself toward survivorship correction. The screener universe lists only companies alive today, so every cohort and factor backtest against it is optimistic by an unknown margin. A frozen membership snapshot is now taken monthly as a side effect of normal refreshes — never rewritten, so a company that later delists stays in the months it was actually alive. This cannot fix the past; it builds a genuinely point-in-time panel forward from first use, and until years accumulate the responses say plainly that the bias remains.
[2.23.1] — Fix: the agreement test hardcoded an absolute path and broke CI
- The legal-gate test resolved its module by an absolute path belonging to the machine it was written on, so all five of its cases passed locally and failed on every other machine. CI had been red since 2.22.0 for this reason alone — no product code was involved. It now resolves relatively, like every other test.
- Added a guard so this cannot recur: a test that scans the suite for absolute
require() paths and fails if one appears. A test that only passes on its author's machine is worse than no test, because it reports green while checking nothing everywhere else.
[2.23.0] — Honest confidence, the reference class, and the valuation run backwards
- Every fitted estimate now carries real statistics and an A–D grade. New
inference.js computes t-distribution p-values (validated against published tables), standard errors and 95% confidence intervals — no runtime dependencies.
- The threshold this replaces was indefensible. A sensitivity counted as "trustworthy" at
n ≥ 5 and |r| ≥ 0.3. At n=8 that is p≈0.47 — indistinguishable from noise, presented to the user as trustworthy. That exact case now grades D, and there is a test asserting it.
- Grades are capped by METHOD, not just by the numbers. Ten annual observations cannot support a confident causal slope, so annual regressions cap at B however good the fit looks; a 35-word sentiment lexicon caps at D; chart patterns cap at C; as-filed accounting can reach A. The ceiling encodes what the method can bear, independent of how the particular sample landed.
- An exact fit is flagged, not celebrated — zero residuals in financial data almost always mean the same quantity expressed twice rather than a discovered effect.
- A conclusion is graded by its weakest input, because a chain of inference is only as strong as its worst step.
- New
get_base_rates: what comparable companies actually did. For each driver assumption you get the cohort distribution, where your number sits in it, and a flag when you are forecasting a top- or bottom-decile outcome.
- Persistence: a five-year forecast is a persistence claim, so the question is not "is 18% plausible" but "what share of companies that reached 18% held it every year for five more". It decays far faster than models assume.
- Fade curves: the empirical median path of companies that started in a given band. A model holding a top-quintile margin flat is claiming the company escapes a fade the cohort typically did not — now visible rather than implicit.
- The cohort is built from live listings, so it is survivorship-biased and says so in every response: failed companies are absent and the distribution is optimistic.
- New
get_implied_expectations: the valuation solved backwards. Instead of "what is this worth if I am right", it answers "what must be true for today's price to be fair" — solving for the driver value at which the model equals the price, holding the others fixed.
- The gap against the base rate is where this becomes a research signal: the price requires 21% growth; 4% of the cohort delivered that. Readings are demanding / unremarkable / undemanding against stated quintile thresholds that can be overridden — the cut-offs are a judgement and are shown rather than hidden.
- It refuses to fabricate: if no value of that driver alone explains the price, it says so and shows the range it searched, instead of returning a clamped bound. Where the valuation is undefined at an extreme (margins low enough that no leg stays positive), the solve narrows to the region where a value exists rather than giving up.
- The solve reuses the existing tested
project()/valuation() rather than duplicating the arithmetic — a second implementation would drift and the two would silently disagree. It is graded as arithmetic over assumed inputs, never as a measurement.
calibrate_model_factors now accepts a fit only when the statistics support it. Some sensitivities that previously auto-applied no longer will; that is the point.
- The model-factors test suite was rewritten to the honest standard: eight clean annual points are explicitly not enough to accept a sensitivity.
[2.22.0] — The operating model gets its real data foundation, and events finally have a home
- Every driver now has a fitted history. Calibration used to see two drivers — gross margin and revenue growth — built from three years of income statements, while opex, capex, working capital, tax, D&A, share count and net debt carried catalog ESTIMATES that nothing ever checked. Driver history is now assembled from income/balance/cash-flow statements, key-metrics, enterprise-values and employee count (revenue per employee is a real operating driver), and extended by as-filed SEC EDGAR XBRL where it reaches further back than FMP — which matters when fitting a sensitivity through a cycle.
- EDGAR extends history; it never overwrites it. Only years FMP lacks are filled, because the two sources disagree on restatements and silently preferring one would make a fit unreproducible. Every point records the source it came from.
- Any factor series ref is now calibratable. Calibration read only a hardcoded FRED id, so a factor you attached as
commodity:CLUSD or edgar:... was chartable and shockable but silently skipped — never given a reading, never fitted. Factors now resolve through the full series layer (FRED, commodity, index, FX, crypto, FMP econ/treasury/statements, EDGAR), and anything that still cannot be calibrated is reported in skipped_factors with the reason instead of vanishing.
- Fitted sensitivities are shown beside the estimate in force, and are never applied automatically. Each fit reports n, r, r², the period and the driver's data sources; accepting it is a deliberate act (
accept: true), because a weak regression on a few annual points should not silently overwrite a considered judgment.
- Events enter the model in the three roles they can honestly play — and sentiment regression is deliberately *not* one of them. Regressing a driver on a sentiment score fits reverse causality (news reacts to fundamentals far more than it drives them) and launders a ±1 lexicon into a decimal sensitivity.
- Evidence: dated, sourced claims about a driver (guidance on a call, an 8-K, a downgrade), weighted by how direct the source is, with a categorical leaning. This never changes a driver value — it is what you would cite for changing it, and what answers "why is growth 8%?" months later.
- Structural breaks: acquisitions, divestitures, restructurings, accounting/segment changes and CEO transitions detected from corporate filings only, each with the phrase that triggered it and marked heuristic. A break invalidates the sample a sensitivity is fitted on, so any fit spanning one is now marked untrustworthy instead of quietly averaging two different businesses together. Discussion of a break in news or notes is not a break.
- Forecast scoring: what the model projected versus what was reported, as mean absolute error and bias per driver — a persistent bias sign means the model is systematically optimistic or pessimistic, which is more actionable than the average miss. A model with no overlapping periods reports no track record rather than a flattering one.
- New
get_model_events tool; calibrate_model_factors gains accept, use_edgar and breaks_from_events.
- The legal terms are now versioned by a hash of their own text, not by a constant someone has to remember to bump. Any edit to the disclaimer — with no code change and no version change — automatically invalidates acceptance and re-prompts on the next start. The previously accepted text is stored, so the re-prompt can show what actually changed rather than just demanding agreement again, and "never accepted" is reported differently from "accepted terms that have since changed". Whitespace reflowing does not spuriously re-prompt.
- The support page now carries release information — current version, publish timestamp, update channel and download link, plus the last eight releases with each note grouped into New / Fixed / Changed / Removed. It is generated from
CHANGELOG.md during the release build, so it cannot drift from what was actually shipped; the grouping is derived from each note's wording and the page says so.
[2.21.1] — The breadcrumb reaches the top: worldview → thesis, with drift detection
- A thesis now records the reading of the world it rests on.
create_thesis takes worldview_domains, and Open Trading Surface snapshots what each cited domain said at the time — its trend and summary — not just its name. This completes the chain: worldview → thesis → screen → portfolio, all four hops in the breadcrumb.
- Worldview drift is detected and surfaced. A thesis formed when inflation was *deteriorating* stands on different ground once that domain turns *improving*, so the chain compares the snapshot against the current worldview and flags which domains have moved. Drift is not an error — it is the trigger to revisit the view. Stale domains (not refreshed recently) are reported separately, because not having looked is a different condition from having seen a change.
- The thesis view shows its upward lineage with each cited domain, its trend as cited, and an arrow to where it has since moved;
get_provenance {thesis} returns the same upward chain plus the books that thesis produced.
- No guessing at the top either: a thesis that cites no domains reports none rather than inferring which parts of the worldview "probably" applied — that is a judgment, not a lookup — and citing an unknown domain is rejected instead of silently dropped.
[2.21.0] — The workflow breadcrumb: every holding traces back to the view that produced it
- Portfolios now record where they came from. Building a model book from a thesis snapshots the full lineage: the thesis, the compiled screen exactly as it ran, the screen run id, and every pick with the rank score that earned it a place and the weight it received. Previously the only link was a single
model_portfolio name string on the thesis — one-directional, and the screen was never stored at all, so once you edited the thesis you could no longer reconstruct why those names were bought. A compiled screen is a function of the thesis's *current* tilts, so re-deriving it answers "what would this screen for today", not "why do I own this".
- Links are by id, not name. Renaming a portfolio or retitling a thesis used to silently break the connection (and a rebuild would quietly create a second book beside the orphaned first). The chain now resolves by id and *surfaces* a rename rather than hiding it.
- A breadcrumb in the UI: model portfolios show *Thesis › Screen › Book* above the holdings, each hop clickable, and the thesis view links forward to the book it produced.
- Screen runs are addressable — each run gets an id and is appended to a capped history alongside the spec that produced it, so a portfolio can point at the exact run instead of a name that drifts.
- Per-lot provenance: model-build transactions carry a structured
origin (thesis id, run id, rank score) instead of only a free-text note.
- Honest about what it knows. Books built before this existed can be reconstructed by name-matching, but are labelled inferred and carry no screen spec — none was ever recorded, and fabricating one would misrepresent an audit trail. A portfolio created by hand reports no lineage rather than guessing.
- New
get_provenance tool (trace a book, or reverse-index the books built from a thesis) and a guarded /pv/ route behind the session token.
- Pinned tabs now follow the workflow: Settings, Agent, Watchlists, Theses, Screener, Portfolios, then your security tabs. The saved-state indices behind them are unchanged, so existing sessions and the agent control channel keep working — with a test pinning both the order and the indices.
[2.20.1] — Documentation and test coverage catch up with the features
- The user guide now documents indicators you define yourself. This shipped in 2.19.0 and was missing from the site entirely — there is now a section covering the formula language, the windowed functions available, parameters as editable chart inputs, multi-plot and lower-pane indicators, and the fact that evaluation happens server-side over bars the chart already holds (no
eval in the browser, no extra API calls).
- The events section covers marker hover, click-through and the model link added in 2.20.0, including what the link means and — importantly — what it does not: it is drawn only through influence edges your model actually carries, and it points at a driver to re-examine rather than re-estimating it.
- The landing page gained a charting section. The workbench is the largest surface in the product and had no feature card at all; overlays, events, auto-detection, no-lookahead replay, custom indicators and rule backtesting are now visible to someone deciding whether to install.
- Tests for the client contract. The chart and the terminal are separate documents that talk by
postMessage and cannot import each other, so nothing checked that the two halves agreed. A new suite asserts the message type matches on both sides, the terminal rejects foreign origins and the chart targets its own, every event source has a destination rail view and every view referenced exists, markers carry their payload, and the agent has parity. Each assertion was verified to fail when the contract is broken.
- Behavioural tests for marker hit-testing:
markerAt resolves the marker under the cursor with its event payload, misses outside the radius, drops stale hit boxes when a source is toggled off, and records nothing for markers scrolled out of view.
[2.20.0] — Event markers you can interrogate: hover, click through, and see the operating-model link
- Hover an event marker to see what it is. Markers on the chart carry their source events now, so pointing at one shows a tooltip with the event label, date, detail and sentiment. Where several events land on the same bar the marker collapses to
N× and the tooltip lists them (first six, plus a count of the rest).
- Click a marker to open that event. The chart runs as an iframe inside the research terminal, so selecting a marker posts up to the terminal, which switches the security to the rail view that owns that source — filings → SEC Filings, ratings → Analyst Ratings, insider → Insider Trades, transcripts, news, ownership, notes, earnings/financials → Financial Statements — and highlights the dated row it landed on. A standalone chart with no terminal above it falls back to the event's source URL.
- Every event now knows which operating-model driver it bears on.
get_events (and the chart tooltip) return model_link = {factors, drivers, via, why}. Two paths produce it: event type — reported results and filed statements reset the growth/margin baseline, guidance moves growth, street ratings feed the exit multiple and discount rate — and keyword matching against the factors actually attached to that company's model, so a tariff headline links to the tariff_rate factor and, through its influence edge, to gross_margin_pct. A model that carries no tariff factor produces no tariff link; the association is only ever drawn through edges the model really has. When a clicked event carries a model link, the terminal offers a one-click jump to the Operating Model view.
- Agent parity:
control_chart select_event {source, date} performs the same navigation a click does, and inspect_chart what=state reports selected_event.
[2.19.1] — Fix: spurious "No FMP API key embedded" warning on every chart
- Fixed a false error toast when loading or refreshing a chart. Charts served by the local server deliberately carry no embedded API key — data flows through the same-origin
/fmp proxy with the key held server-side — but a leftover check from the old standalone-file era warned whenever no key was embedded, so it fired on every proxy-served chart even though nothing was wrong. The warning is now correctly scoped to the standalone file:// copy, which is the only case that actually needs an embedded key (and its wording now points at opening the live chart URL instead of "regenerating from Claude Code").
[2.19.0] — Custom indicators on the chart + backtests painted on price (specs complete)
- Spec C polish: custom indicators now appear in the chart's indicator library under a Custom category alongside the built-ins, with the same add / param-edit / hide / remove affordances, and are addable by the agent via
control_chart add_indicator {id:"custom:<name>"} (a definition created after the chart loaded is picked up automatically). Plots marked pane:"lower" get their own pane. The DSL stays server-side — the chart posts the bars it already holds to /ci/eval and receives plot series, so there is still no eval in the browser and no extra API calls. inspect_chart what=state now lists the custom definitions, so an indicator the user edits in the rail and one the agent authored are the same object.
- Spec B painting: a new 🧪 Backtest menu runs an entry/exit rule on the loaded bars and paints the result onto price — BUY/SELL markers per trade (exit labelled with its return, open legs marked) plus an equity-curve pane — with the summary line (trades, total return, hit rate, max drawdown, buy & hold) in the menu.
control_chart backtest {entry, exit, cost_bps} / clear_backtest, and the full result object is readable through the new inspect_chart what=backtest; what=state carries the rule, costs and stats. Backtests launched from the UI and from the agent produce the identical object and the identical painting.
- With this, all three charting specs meet the world-model gate: every new object (detections, replay cursor, backtest results, custom indicator definitions) is introspectable as structured state, every affordance has a tool twin, and mutations flow through the existing stores and journal.
[2.18.0] — PIT bar replay: replay without lookahead (Spec B: part 2)
- Replay mode walks a chart forward bar by bar from any start date, with transport controls (step back/forward, play/pause at 1×/4×/10×, exit) in a replay bar that always states what you are looking at.
- The no-lookahead guarantee is structural, not a promise: replay windows
state.bars to the cursor, so every downstream consumer — indicators, the 12-signal battery, Spec A's auto-detections, event markers, the analysis panel — recomputes from bars ≤ cursor and *cannot* see the future.
- The honest-replay moat: analyst-target and DCF price lines are as-known-now, so during replay they are suppressed and labelled rather than silently drawn. If a point-in-time snapshot covers the cursor date, the values you actually observed then are drawn instead and marked as-known-then (snapshot YYYY-MM-DD). New
/pit/asof route backs the distinction.
- Agent parity:
control_chart replay {action: start|step|play|pause|stop, date?, steps?, speed?}, and inspect_chart what=state reports the cursor index/date, total bars, play state, speed, and whether fundamentals are as-known-then or suppressed — so the agent can run a name forward and narrate what a disciplined analyst would have concluded at each step.
[2.17.0] — Rule backtesting over the series DSL (Spec B: part 1)
- New
backtest_rule: entry/exit rules written as series-DSL expressions (or translated from plain English) are backtested locally on already-fetched bars — returning the trade list, per-bar equity curve, hit rate, average win/loss, profit factor, exposure, max drawdown and a buy-and-hold comparison, with costs charged in basis points per side.
- No lookahead by construction: every DSL builtin is backward-windowed and null-padded, and fills occur at the signal bar's close — a price known once that bar completes. A regression test appends wildly different future bars and asserts a already-closed trade is byte-identical.
- Honest by default: an unclosed leg is reported as an open position rather than silently realized, and every result carries the caveat set — *single-symbol rule backtests are a hypothesis check, not strategy validation*; costs/slippage/borrow/tax limits; in-sample selection is an upper bound. The tool also warns that a level condition (
close > 50) is a STATE that re-enters after each exit, versus edge-triggered crossover/crossunder.
- Next in Spec B: the PIT replay governor (bar-by-bar cursor with indicators, signals and detections recomputed to the cursor, and fundamentals marked as-known-then vs as-known-now) plus painting trades/equity onto the chart.
[2.16.0] — Detection on the chart (Spec A complete: world-model contract met)
- The detection engine from 2.15.0 is now on the chart. New 🔎 Detect menu with per-category toggles (Trendlines, S/R zones, Candlestick patterns, Geometric patterns) and a minimum-confidence slider; detection runs on the bars the chart already holds (posted to a local
/pat route) so it still costs zero API calls.
- Detections render through the ordinary drawing layer tagged
source: auto — dashed and tinted by direction (green bullish / red bearish / grey neutral) so they are always distinguishable from your own drawings, and clearable independently: the legend shows "N detected" and "N drawings" as separate chips, and clear_drawings {source: auto|user} clears one without touching the other.
- Full agent parity (the world-model gate):
control_chart gains set_detection {categories, on, min_confidence}, detect {…} (runs and returns the structured detections), and clear_detections; inspect_chart what=state reports enabled categories, confidence floor and every detection (type, confidence, direction, status, coordinates, narrative); what=signals now returns a patterns section so the 12-signal battery and the detections read as one technical picture. Anything the agent detects is editable by the user, and anything the user detects is readable by the agent.
[2.15.0] — Automated pattern & trendline detection (Spec A: engine + tool)
- New local detection engine (
patterns.js, no extra API calls): fractal pivot extraction, trendline fitting over pivot pairs scored by touch count / recency / violations, volume-weighted support & resistance zone clustering, ~40 candlestick recognizers (engulfing, hammer/hanging man, the doji family incl. dragonfly/gravestone/long-legged, shooting star, marubozu, harami + harami cross, tweezers, kickers, morning/evening (doji) stars, three white soldiers / black crows, three inside/outside up-down, abandoned baby, rising/falling three methods), and geometric patterns (double top/bottom, head & shoulders + inverse, ascending/descending/symmetrical triangles, rising/falling channels).
- Detections are agent-legible, not pixels: every one returns
{type, category, points:[{index,time,price}], confidence 0-1, direction, status: forming|confirmed|invalidated, narrative} — real chart coordinates the agent can cite, reason over, or draw. New detect_patterns tool with category and confidence filters. Each recognizer is pure (bars) → detections and unit-tested; a 500-bar pass runs well inside the 1s budget.
- Not yet: on-chart rendering of detections as
source: auto drawings, the Detect toolbar toggles, and the inspect_chart patterns surface — that is the client half of Spec A and lands next; the tool description says so rather than implying it exists.
[2.14.0] — Series-valued DSL + user-defined indicators (charting specs: shared foundation + C-foundation)
- The shared foundation for the three charting specs: the screener's expression DSL now has a SERIES-valued sibling (
expr-series.js) that evaluates over a bar array. Identifiers bind to OHLCV (open/high/low/close/volume/hlc3/ohlc4/time); windowed builtins sma/ema/wma/highest/lowest/stdev/sum/slope(x,n), ref(x,k), crossover/crossunder, barssince(cond) join abs/log/sqrt/min/max; scalars broadcast against series; results are null-padded and null-poisoning. It reuses the existing tokenizer/grammar and inherits the caps (2,000 chars / 1,000 tokens) with no eval, no loops, no recursion — cost stays O(bars × window). Custom indicators, expressible detectors, and rule backtests all consume this one evaluator.
- User-defined indicators (Spec C engine):
define_indicator compiles a {name, params, plots:[{expr,style,pane,color}], description} definition, validating every expression and requiring each non-bar identifier to be a declared parameter; definitions persist in ~/.ots/indicators.json with the same atomic-write/survive-update guarantees as portfolios and theses, and mutations are journaled. Plus list_custom_indicators, delete_custom_indicator, and eval_expression for one-off agent computations (e.g. sma(close / highest(high,200), 10)) against any symbol. Compile failures return structured, self-correctable messages and never partially save.
- Not yet: on-chart rendering of custom indicators (the "Custom" library rail) — that is C-polish, sequenced after Specs A and B, and the tool descriptions say so plainly.
[2.13.0] — Forecast-attribution calculator in the Operating Model view
- The Operating Model view now has the attribution calculator on top of the engine shipped in 2.10.0: Run forecast & attribution projects every series-backed factor forward (trend / CAGR / flat, per factor) and decomposes the multi-year fair-value & EPS forecast into ranked per-factor contributions with computed weight shares — bar-scaled, with the interaction residual reported — plus the regression cross-check table (OLS beta, standardized beta, R², and sign agreement vs each structured sensitivity).
- New Factors & weights table: every factor with its backing series, current level, an editable tunable weight (persisted immediately), and a per-factor forecast method; plus an Add series factor control to wire any ref (
commodity:CLUSD, fred:DGS10, fmp:income:AAPL:revenue, edgar:AAPL:Revenues, …) to a driver with a sensitivity — so the single-name model can depend on the full external-series layer from the UI, not just the agent.
[2.12.0] — Event markers, events pane & Analysis sentiment (event overlays, part 2)
- All 11 event sources are now chart markers. The 📅 Events menu went from 4 toggles (earnings/dividends/splits/insider) to every source: earnings (beat/miss), dividends, splits, insider buys/sells, SEC filings, earnings-call transcripts, institutional 13F ownership flow, analyst rating changes, quarterly financial releases, news, and research notes — each independently toggleable, sentiment-colored (green positive / red negative), with dense days collapsed into a single
N× cluster and a density cap so long ranges stay readable.
- New events pane: an optional lane under the price chart charting net event sentiment per bar, so you can see tone accumulate over time next to price.
- Analysis panel now includes an Events & sentiment section: the recent event stream with per-event sentiment pills plus the weighted composite score/verdict, alongside the existing technical battery.
- Agent-drivable:
control_chart set_events {events:{…11 sources…}, pane:true}, and inspect_chart reports the enabled sources, pane state, and current sentiment composite.
[2.11.0] — Unified event + sentiment engine (event overlays, part 1)
- New event layer: one
get_events call returns a single, dated, sentiment-scored stream across every source — earnings (beat/miss), dividends, splits, insider trades (buy/sell), SEC filings, earnings-call transcripts, institutional-ownership 13F net flow, analyst rating changes (up/downgrade), quarterly financial releases, news (tone), and your research notes — plus a weighted sentiment composite (score −100..100 + verdict) over the recent window. Each event carries a −1/0/+1 sentiment derived per type (insider P/S, grade-rank moves, EPS vs estimate, a news lexicon, net institutional flow, note kind).
- Backed by
server/events.js (pure per-type sentiment scoring + normalizers, injected data clients) and a token-guarded /ev chart proxy route; unit- and HTTP-tested. This is the data/sentiment foundation; the next releases add the chart event markers / events pane and the Analysis-panel events + sentiment section.
[2.10.0] — Operating-model attribution + self-healing daemon on upgrade
- Any external series can now drive a company operating model, with forecast attribution. New tools:
attach_series_factor (wire any fred:/fmp:econ:/fmp:treasury:/index:/commodity:/financial/edgar series into the model as a factor, optionally linked to a driver), set_factor_weight (tunable weight multiplier per factor), and forecast_and_attribute — which projects each factor forward (trend/cagr/flat/scenario), propagates through the weighted influence edges, and attributes the multi-year EPS/fair-value forecast to the factors: a deterministic contribution decomposition with computed weight shares and a reconciliation residual, plus a multivariate regression cross-check (OLS betas / standardized betas / R² vs the structured sensitivities). Backed by model-attribution.js (forecasting, Gaussian-elimination OLS, attribution, regression — all unit-tested).
- The local server now self-heals on upgrade. The persistent daemon records its plugin version + install path (in
server.json and /_health); after an in-place update the MCP client detects a stale daemon from the prior build and auto-retires and replaces it (graceful shutdown, or by signalling its recorded pid for pre-guard daemons) — so you no longer have to delete ~/.ots/server.json and restart to pick up a new version. Covered by a new daemon version-guard test.
[2.9.2] — Fix: Compare overlay drew nothing
- Fixed the 🆚 Compare feature: adding a comparison symbol flipped the chart to percentage scale and showed a legend chip, but never drew the comparison line — the render step was missing. Comparison symbols now render as normalized lines on the shared percentage scale (all series rebased to their first visible value), so relative performance shows correctly.
- Groundwork (not yet user-exposed): operating-model factors can now be backed by ANY external series ref and carry a tunable weight, and a new forecast+attribution engine (contribution decomposition + weight shares + multivariate regression cross-check) is in place and unit-tested — the next operating-model release wires it to tools and the calculator UI.
[2.9.1] — Fix: charts failed to load on a clean install (ENOENT engine.js)
- Fixed stock charts showing "ENOENT: no such file or directory … server/engine.js" on a fresh install. The chart HTML was assembled by reading three browser-side files (engine/indicators/talib-ext) from disk at runtime, but those files are never
require()d — so a plugin loader that installs only the required module graph placed the bundled index.js without them, breaking every chart. Those client files are now embedded into the bundle at build time (via client-embed), so the shipped server assembles charts with zero runtime file reads and no sibling-file dependency. Dev (unbundled source) still reads the raw files.
- Added a regression test that builds the bundle, deletes the sibling client files, and asserts the engine is still inlined — so this class of install failure cannot ship again.
[2.9.0] — Full external-series coverage: FMP economic, treasury & indices
- Extended the cross-series layer to the remaining FMP time-series families so overlays/analysis now span the full set: FMP economic indicators (
fmp:econ:realGDP|CPI|inflationRate|unemploymentRate|federalFunds|retailSales|consumerSentiment), the FMP treasury yield curve per tenor (fmp:treasury:year2|year10|year30|…), and first-class indices / forex / crypto refs (index:^GSPC, index:^VIX, forex:EURUSD, crypto:BTCUSD) — previously only reachable through the generic price ref and not discoverable. All are catalogued (new rates and index categories) so search_series and the 🌐 Macro/Data menu surface them, and all flow through the same overlay + analyze_lead_lag machinery.
- Honest fallbacks: FMP economic/treasury endpoints require a higher FMP plan; when unavailable the resolver returns a clear message pointing at the equivalent FRED series (which remains the more complete source for economic data and rates).
[2.8.0] — Cross-series chart overlays (chart overlays, part 2)
- Overlay any external series directly on the price chart. A new 🌐 Macro/Data menu adds economic (FRED), commodity/energy, per-company financial-statement, and SEC EDGAR XBRL series to the workbench — each rendered either on its own stacked right-hand axis (multiple axes align mixed units honestly) or in a separate pane, configurable per series. Overlays align to the price dates by as-of forward-fill, so monthly/quarterly series step correctly against daily bars.
- Lead/lag shift on the chart: every overlay carries a ± bar shift so you can slide a leading indicator to line its turns up against price; pair it with
analyze_lead_lag to find the best-aligning lag quantitatively.
- Agent-drivable:
control_chart gains add_overlay / set_overlay / remove_overlay / clear_overlays, and inspect_chart now reports overlays — so the agent can build and shift a cross-series comparison the same way it drives indicators.
- Engine: the charting engine now supports multiple stacked right-hand axes (each overlay its own labeled scale), verified by a headless render test; overlay config persists across reloads.
[2.7.0] — Cross-series analysis engine (chart overlays, part 1)
- New unified external time-series layer: a single
resolve_series fetches any series across FRED (economic), FMP (commodity/energy prices and per-company financial-statement line items over time), and SEC EDGAR (XBRL company-facts, e.g. Revenues, NetIncomeLoss) as normalized {date,value} points, addressed by compact refs (fred:DGS10, commodity:CLUSD, fmp:income:AAPL:revenue:quarter, edgar:AAPL:Revenues). search_series surfaces a curated, symbol-aware catalog.
- New lead/lag analysis:
analyze_lead_lag aligns an external series onto a stock's price dates and cross-correlates across ± lags, reporting the lag that best aligns them and its strength — i.e. whether the series leads the stock (predictive) or lags it, on a level or return basis. Both humans and the agent can now test dependent/predictive relationships quantitatively.
- Under the hood:
server/series.js (ref parsing, resolution, as-of alignment, Pearson + cross-correlation — pure and unit-tested with injected clients), EDGAR client extended to data.sec.gov XBRL company-concept with ticker→CIK resolution, and token-guarded /series/* chart proxy routes. This is the data/analysis foundation; the on-chart overlay rendering (stacked right-hand axes + separate panes, with manual shift) lands next.
[2.6.1] — Documentation accuracy pass
- Reconciled the docs with the shipping product: removed the dead Claude Code marketplace install path everywhere (distribution is package-only), corrected the README tool list (was the stale 17-tool v1.3 set) to the real ~107 tools by area, de-duplicated the README key-configuration section, and added the operating-model influence map to the README and landing page.
- The public changelog is now generated from
CHANGELOG.md at build (it had drifted six releases behind at 2.3.0); added scripts/docs-lint.js (run by release.sh) that fails the build on a reintroduced marketplace path, broken internal links, or signature-verification instructions while releases are unsigned.
- Fixed release-integrity docs to match reality (GPG signing is temporarily paused → checksum-only; removed the stale signature and BAD-signature trap), corrected
RELEASING.md topology (source lives in the private ots-source; distribution is a push to Pages with no tags/Releases), and documented the real terminal failure modes (the daemon "Load failed" fix, blank first-run tabs, and the slow-not-frozen screener) in TROUBLESHOOTING.md.
[2.6.0] — Rich operating models: data-driven calibration (phase c)
- The external-factor layer can now calibrate itself from data. Calibrate from FRED sets each factor's *current level* from its FRED series (interest_rate ← DGS10, inflation ← CPI YoY, consumer_confidence ← UMCSENT, …), shown on each factor node in the influence map, so shocks read as "move rates to 5%" rather than an abstract delta. On request it also fits factor→driver sensitivities by OLS — regressing the company's annual gross margin and revenue growth (from filings) on the aligned annual factor history. A fitted coefficient only replaces the structured prior when the fit is *trustworthy* (≥5 aligned years, real factor variation, |correlation| ≥ 0.3) and you pass apply; otherwise it's attached to the edge as auditable evidence and the prior is kept. New
calibrate_model_factors tool + /cm/calibrate route; the OLS/alignment/level/sensitivity core is pure and unit-tested (injected data clients keep the module runtime-dependency-free).
[2.5.0] — Rich operating models: graphical influence view (phase b)
- The Operating Model view now renders a graphical influence map — a left-to-right diagram of external factors → internal drivers → EPS & fair value. Factors are grouped and colored by category (economic / policy / supply-chain / consumer); edges are green when they raise a driver and red when they lower it, with thickness scaled to sensitivity, and every edge carries its rationale on hover. Shock a factor right in the diagram and the impact propagates through the edges into the valuation engine, with the isolated path highlighted and the full factor→driver→outcome trace shown. New
/cm/graph and /cm/propagate terminal routes, covered by an HTTP integration test.
[2.4.0] — Rich operating models: external factors (phase a)
- Company operating models now support an external-factor layer beyond internal drivers — economic, political/policy, supply-chain, and consumer/customer forces — each linked to internal drivers by signed influence edges (with rationale, auditable). Shocking a factor propagates through the edges into the drivers and reuses the valuation engine to produce the EPS / fair-value impact, with a full factor→driver→outcome trace.
- New tools:
get_factor_catalog, seed_model_factors, add_model_factor, link_factor_to_driver, shock_factors, get_model_graph. (Next: a graphical influence diagram, then data-driven calibration from FRED/filings.)
[2.3.5] — Screener responsiveness
- Screens no longer look frozen: the screener and thesis "Screening…" now show a live elapsed-time counter (screens scan ~7,600 names and enrich the top candidates; 30–90s is normal), a 150s timeout with a Retry button, and clear errors instead of an endless spinner.
- Batched quotes: the quote enrichment stage now uses FMP's comma-separated batch endpoint (≈200 calls → ~2) with per-symbol fallback. (Ratios/scores/growth remain per-symbol; FMP bulk endpoints are the next optimization.)
[2.3.4] — Auto-refresh after key setup
- Fixed blank data tabs on first run: when you save an FMP or FRED key in ⚙ Settings, the terminal now reloads so tabs that were opened before the key existed (chart, financials, analyst, etc.) fetch their data. Previously the quote header updated but the chart/panels stayed blank until a manual page reload.
[2.3.3] — Terminal reliability
- Fixed the terminal's "Load failed" bug: the local web server that backs the terminal (Settings save, data tabs, charts) now runs as a persistent detached daemon with a stable port + token (
~/.ots/server.json), so pages keep working when the plugin process is cycled by the host. Previously the server died with the plugin process, orphaning any open tab. Falls back to the legacy in-process server where a daemon can't be spawned (no regression).
- ⚙ tab is now labeled "Settings" (was a bare gear icon); unreachable-server fetches show an actionable "reopen the terminal" message instead of "Load failed".
- Tests for the gaps this exposed: Settings save round-trip (FMP/FRED/EDGAR) over HTTP, and a daemon-persistence suite proving a published page survives its publisher exiting.
[2.3.2] — Feedback loop, quality & hardening
- In-product feedback:
report_issue composes prefilled GitHub bug/feature/idea reports (version + environment stamped); get_feedback_history; new product-idea issue template; GitHub Discussions; the docs Support page is now a feedback/community hub.
- Opt-in, local-only telemetry:
set_usage_reporting / get_usage_report — a usage/error journal that never leaves your machine but can be exported to attach to a report; wired into the tool dispatch.
- Engineering quality: dev-only TypeScript toolchain + strict
tsc type-checking; GitHub Actions CI (typecheck + full harness + coverage gate + build + package-source-leak check); Node built-in coverage (baseline lines 83% / branch 79% / funcs 79%); the shipped minified bundle is now tested, not just source; data-provider network-layer tests (rate limits, non-JSON, empty payloads, missing key); UI check integrated (opt-in).
- Reliability: all stores now use atomic writes (temp + rename) with schema versioning and user-only file mode — no more half-written/corrupt stores.
- Security: GPG-signed packages with a published verification key + manifest signature fields; indirect prompt-injection defense (untrusted-content fencing + detection) with a documented threat model.
- Honesty: thesis probabilities reframed as subjective, auditable estimates (not validated forecasts) with a calibration tool (Brier score, skill vs base rate) that stays silent until enough theses resolve.
- Legal: explicit non-affiliation with and non-endorsement of data providers (FMP/FRED/SEC EDGAR); no certification that provider data is fit for purpose.
- Docs: a true feature User Guide (+ 5-minute quickstart),
TROUBLESHOOTING.md, ARCHITECTURE.md, CONTRIBUTING.md; private source maps for decoding minified stack traces.
[2.3.1] — Rebrand to Open Trading Surface
- Package-only distribution: the released
.plugin now ships the server bundled + minified (via esbuild) and the client chart files minified — runnable, but not the authored source. Release assets and history contain no source; source lives in a separate private repository.
- Licensing: added a proprietary end-user license (
LICENSE); the software is distributed as a package only, not as source — free to download and use (incl. commercial); no redistribution or derivative works; Licensor may change terms for future versions. Governing law: Texas. Added Open Trading Surface logo/iconography assets under docs/assets/.
- Renamed the entire project from Surface-StockChart to Open Trading Surface — an ots is a mechanical model of the heavens, fitting for a terminal that builds a working model of the market. This touches the brand text, the plugin id (
surface-stockchart → ots), the MCP server name (tools are now mcp__ots__*), the on-disk store directory (~/.surface-stockchart → ~/.ots), environment-variable prefix (STOCKCHART_* → ORRERY_*), and the session token header (x-scp-token → x-ots-token). Docs, release pipeline, and issue templates all point at the pauljbernard org.
- Hardened
.gitignore so secrets (config.json, *.env, *.key), all ~/.ots runtime data stores, build artifacts (*.plugin, dist/), test reports, and local Claude Code state (.claude/) can never be pushed to the remote. Fixed an inert inline-comment rule that had left config.json unignored.
[2.3.0] — Distribution & legal
- Update channel:
check_for_updates tool and cockpit surfacing against a public version manifest; one-command release.sh pipeline emitting the .plugin, SHA-256 checksum, and manifest for GitHub Releases + Pages.
- Documentation website (GitHub Pages): screenshot-rich marketing landing, full user guide, changelog, and support/issues page.
- Legal: comprehensive disclaimer (repo
DISCLAIMER.md, docs legal page); in-app first-run acceptance gate with persistent footer; get_disclaimer / record_disclaimer_acceptance tools. Not investment advice; user assumes full responsibility; as-is, no warranty/liability.
- Refreshed plugin metadata and README to the full capability set; FRED key passthrough in
.mcp.json.
[2.2.0] — Hardening
- Full test harness across functional, non-functional (HTTP + security), and performance categories with a
run-all.js orchestrator producing JSON + Markdown reports; run_test_suite tool.
- Secure code review remediations: EDGAR SSRF allowlist (re-checked on redirects), API keys never embedded at rest, workbench XSS escaping, chart-save path containment, DNS-rebinding Host guard, timing-safe token comparison, expression DoS caps, store-corruption self-heal.
SECURITY.md added.
[2.1.0] — Worldview & thesis probability
- Structured worldview across eight domains;
propose_theses macro signal engine over FRED + FMP data (the system forms its own theses from data).
- Thesis probabilities with a full evidence ledger and probability history; continuous reassessment;
get_worldview_refresh_bundle.
[2.0.0] — Company operating models
- Driver-based per-company operating models calibrated from filings + consensus; instant event→driver→EPS/fair-value shocks; elasticity tables; terminal Operating Model view.
[1.9.0] — The fully-leveraged agent
get_workspace_state cockpit, agent inbox, generalized playbooks, audit journal, API budget governor; filing-diff intelligence, thesis review / IC-memo bundles, event triage, macro context.
[1.8.0] — Thesis engine
- Thesis → compiled screen → model portfolio pipeline; screener expression DSL (where/derive); 13F crowding fields; point-in-time snapshot archive; honest technical-factor backtester.
[1.7.0] — Institutional portfolio operations
- Broker CSV import; dividend reconciliation; Carino-linked multi-period Brinson attribution; ETF-proxy ex-ante factor model with scenario shocks; mean-variance optimizer.
[1.6.0] — Research memory
- Persistent research notes; alert playbooks + morning brief; pro-forma what-if; event-annotated charts; screen deltas; pre-earnings prep pack.
[1.5.0] — Surveillance & relative analysis
- Watchlists + live monitor; edge-triggered alerts; events calendar; peer comps; 13F ownership; earnings surprises; Brinson attribution.
[1.4.0] — Professional screener & portfolio analytics
- Cross-sectional factor screener (percentile / z-score, sector-neutral, Piotroski/Altman, 12-1 momentum); GIPS-style TWR/MWR, risk suite, FIFO tax lots, exposures, rebalancing.
[1.3.0] — Foundation
- Clean-room canvas charting engine (~125 indicators, 39 drawing tools), multi-security research terminal, FMP-backed data, localhost server with agent control channel, packaged as an installable plugin.