Changelog
All notable changes to Open Trading Surface. Versions follow semantic versioning. This page is generated from CHANGELOG.md.
5.205.0 — the structural search could not see a cascade, and said nothing
5.204.0 — fifteen negative controls were counting distance and calling it cost
5.203.0 — a spec may not claim nothing is built while naming the release that built it
CLAUDE.md opens with it — six spec files carried SPEC ONLY for between
SYSTEM-BROWSER-SPEC said *"NOTHING BUILT"* while 5.175.0 was shipping the
- The table then rotted twice — through 5.180.0/5.181.0 and again through
5.202.0 — query by example (P6.5), and §2 is five of five
5.201.0 — what a class can TOUCH, before it runs (P6.4, the fourth of the five)
5.200.0 — what this class BACKS, and whether the two agree (P6.3, the third of the five)
- FIELD —
two_statements. Two statements *inside one class* can disagree:
- SCREEN —
panel_may_lag. The run *already* follows the class;
- INDICATOR —
panel_follows. The panel is read LIVE from the class, so a
- PROGRAM —
sync_back. The only kind that can be read back, because
5.199.2 — the yield guard was green over eight arms, because its own shape could not see a ternary
5.199.1 — the seeder's rule was fixed and its trigger was not
5.199.0 — a tag FILTERS, and the two kinds are told apart on sight
5.198.0 — the Packages column holds packages, exclusively (owner ruling)
5.197.0 — the seed declared a rule and implemented its opposite, and a suite was reading a live image
5.196.0 — time-travel browsing, and the manifest 5.195.0 shipped empty
5.195.0 — why, as a browsing axis: the first thing this browser does that no other Smalltalk can
5.194.0 — the cross-reference walk moves into the image, and there is no JavaScript walk left
5.193.0 — the cross-reference could not see a cascade, and the field that would have said so was on every answer
5.192.0 — one surface may not carry two denominators, and a dialog that built 142 rows and attached none
5.191.0 — the percentage is computed, and 118 institutions were invisible
5.190.2 — the loader called the bound instead of the door, and now a browser checks
5.190.1 — the ownership route threw on every request, and the suite was green
5.190.0 — the finder's ranking moves into the image (P3)
5.189.0 — ownership as a percentage of the shares the company actually filed
5.188.0 — the 13F numerator is checked, not trusted
5.187.2 — the vertical sliders: a circular floor, and two units for one number
5.187.1 — THE SPLITTER COLLAPSED THE PANE IT WAS MEANT TO SIZE
5.187.0 — SUnit IN THE IMAGE, TESTING THE BROWSER
5.186.0 — THE HIERARCHY MOVES INTO THE IMAGE
5.185.0 — THE BROWSER IS WRITTEN IN THE LANGUAGE IT BROWSES
5.184.0 — THE BROWSER'S BOUNDARIES BECOME SPLITTERS
5.183.1 — THE STATE MOVED AND THE SCREEN DID NOT
5.183.0 — THE TWO COLUMNS NEST
5.182.1 — A DISPLAY FORMATTER WAS USED AS A NUMERIC ACCESSOR
5.182.0 — THE OWNERSHIP PANE SORTS, AND THE RATIO NAMES ITS DENOMINATOR
5.181.2 — THE REPAINT THAT LANDED IN A HIDDEN NODE
5.181.1 — THE VERB THAT HAD NEVER BEEN CALLED
5.181.0 — THE FINDER (SYSTEM-BROWSER-SPEC P4)
- an exact match beats a prefix beats a substring, because a person who typed
- a definition beats a use —
CiSeries answers with the class, not the
- a name beats text — a selector called
plot is a better answer to
5.180.0 — PROTOCOL MANAGEMENT (SYSTEM-BROWSER-SPEC P3)
5.179.0 — THE SYSTEM BROWSER STOPS BEING OPAQUE TO AN AGENT
5.178.1 — THE SPREAD THAT DESTROYED A SENTENCE
5.178.0 — THE HIERARCHY PANE (SYSTEM-BROWSER-SPEC P2)
- the superclass chain, nearest first, each rung marked kernel or not — a
- direct subclasses with the descendant count beside them, because the tree
- the resolution: which class defines it, whether this class merely
5.177.0 — THE INSPECTOR PANE (SYSTEM-BROWSER-SPEC P1)
5.176.0 — THE SCREEN CAN REACH THE REMEDY THE REFUSAL NAMES, AND THE LEDGER FILTERS
5.175.0 — THE VERSIONS BROWSER (SYSTEM-BROWSER-SPEC P0)
5.174.0 — A RUN THAT FIRES AND PRODUCES NOTHING MUST LEAVE A TRACE
5.173.0 — UNTRUSTED NETWORK INPUT MAY NOT CRASH THE DAEMON
5.172.0 — THE HONEST ZERO WAS THE ARM THAT THREW
5.171.0 — AGREEMENT AND COVERAGE ARE DIFFERENT NUMBERS, AND ONLY ONE WAS PRINTED
5.170.0 — THE MEASUREMENT THE OWNER'S OWN RULES MADE POSSIBLE, AND THE STAGED PLAN
5.169.0 — PHASE 3b: THREE MORE PROCESSES, AND THE RUN OUTCOME BECOMES THE IMAGE'S JUDGMENT
5.168.0 — THE REMEDY IS REACHABLE FROM WHERE THE DIVERGENCE IS STATED
5.167.0 — THE IMAGE IS THE DEFINITION FOR FIELDS AND SCREENS TOO
5.166.0 — D1 RESOLVED: a condition is a class of named parts, and the image is the definition
5.165.0 — PHASE 3a: the Process package, and the strategy walk order is ruled
5.164.0 — PHASE 2b: the switch exists, and it defaults to off
5.163.0 — PHASE 2a: the disclosure moves into the image
5.162.0 — PHASE 1c: the constructs Phase 1 refused, and one that was never a limit
5.161.0 — PHASE 1b: the condition operators go host-backed, and the slowness is explained
5.160.0 — PHASE 1 OF THE UNIFIED-EXECUTION ARC: the condition, in the image
5.159.0 — PHASE 0 OF THE UNIFIED-EXECUTION ARC: the seam, and nothing moves
5.158.0 — PHASE 6 OF THE DEVELOPER-JOURNEY ARC: XMPP symmetry, and a request is a proposal
5.157.0 — PHASE 5 OF THE DEVELOPER-JOURNEY ARC: the package as an entity
- THE BASELINE IS A CLASS, NOT A FILE. BaselineOf<Package> with class-side
- MEMBERSHIP IS DERIVED from each class's own package: - no second list that
- The structural dependency the baselines cannot see is reported beside them: a
- A package with no baseline SAYS SO rather than showing an empty requires list
- LOAD ORDER derived depth-first every time, stored nowhere. A CYCLE IS NAMED,
- UNINSTALL is one attributed act that refuses and names what it would break.
- Retracted, not deleted: chains stay readable with the uninstall cited.
- The activation gate reads the baseline off the payload and refuses by name
- THE PACKAGE DEPENDENCIES BROWSER, in the System Browser: requires, required
5.156.0 — PHASE 4 OF THE DEVELOPER-JOURNEY ARC: rename, with the senders updated
- st_rename_method rewrites the definition AND every sender at AST TOKEN
- The parser now records EVERY selector token's position. Before this a rename
- FOUND BY DRIVING IT: a rename rewrites every SEND of that selector, and no
- FOUND BY THE SUITE: rewrite_symbols reported a count of symbols it had not
- Every refusal fires BEFORE any write and the refusal is a WORK LIST: arity,
- A symbol is reported and not rewritten by default:
perform: #foo should
- The old name is RETRACTED, not deleted - its history survives its name.
- st_rename_class moves the methods, rebases the subclasses and rewrites the
- The browser's Rename control dry-runs first, always.
5.155.0 — PHASE 3 OF THE DEVELOPER-JOURNEY ARC: the stack, and the inspector
- THE STACK IS BUILT DURING UNWINDING, NOT MAINTAINED DURING EXECUTION. Each
- Frames are innermost-first with the receiver, the arguments and the
- The failing send is its own fact beside the frames: the innermost frame names
- A non-local return is control flow and never a frame. The stack is bounded at
- Every reader carries it: the playground, a failing test, and a failing tick -
- st_inspect walks an object graph ONE DEPTH AT A TIME and never serializes it
- cmFetch now carries the refusal PAYLOAD: it threw a bare sentence, so the
- No step, resume or restart-frame: those need a suspendable interpreter (D3).
5.154.0 — PHASE 2 OF THE DEVELOPER-JOURNEY ARC: SUnit in the image
- TestCase is a KERNEL class: every image has it without loading anything, and
- Protocol: setUp, tearDown, assert:, deny:, assert:description:,
- THREE OUTCOMES KEPT APART, classified by a marker the interpreter stamps and
- A FOURTH STATE: a TestCase subclass with no test method is UNTESTED, never
assert: refuses a non-Boolean as an ERROR: assert: nil is the classic
- Scratch capabilities by default - a test suite that can stage orders is not a
- st_commit_packages now carries a test verdict, on the dry run too. It
- Tests pane in the System Browser, with Run all / Run class / Run package; a
- should:raise: could not be built as specified - there are no exception
5.153.0 — PHASE 1 OF THE DEVELOPER-JOURNEY ARC: senders, implementors, references, search
- st_senders / st_implementors / st_references / st_search_source, and the
- DERIVED FROM THE AST, NEVER A REGEX. A selector inside a string literal or a
- A selector NAMED AS A SYMBOL is reported beside the senders and never among
- IMPLEMENTORS derives the override chain: several along one superclass chain
- REFERENCES keeps three kinds apart - variable, symbol, superclass - because
- An unparseable method is NAMED, never skipped: the count is a floor and says
- The walker's node vocabulary is closed and throws on an unknown kind - a node
- Nothing here writes, asserted from the shape of the module.
- A DEFECT FOUND ON THE WAY IN: st-migrate's commitIfChanged read
.selector
5.152.0 — PHASE 0 OF THE DEVELOPER-JOURNEY ARC: two lifecycle defects, both in shipped code
- A SHAPE CHANGE WAS A SILENT DROP. The tick restored persisted ivars with
- The restore is now TOTAL and PURE: restored / started_nil / shed / rejoined.
- A tick does not stop because a shape moved. The author changed the class on
- A FAILING HANDLER WAS INVISIBLE. The tick computed the failure with the
- Three states now, kept apart: never_ticked / ok / failing, each with its own
- DEVELOPER-JOURNEY-SPEC is the new spec: the SDLC arc from a measured audit,
5.151.0 — THE REPOSITORIES BROWSER: git integration, and the way out of the image trap
- Analysis -> Repositories: Projects (Repository / Status / Branch, as Pharo
- THE IMAGE STORES NO VERSIONS - the owner's own correction made structural.
- ONE GESTURE, THREE DECLARED ARMS: a URL clones, an existing checkout is
- LOADING NEVER PRESUMES EXECUTION: a version is read in a detached worktree
- CREDENTIALS ARE NEVER OURS: no token field exists, a remote URL carrying a
- COMMIT IS SCOPED AND PREVIEWED: explicit packages required, dry-run names
- Two defects found by driving real git: unborn is not detached (a fresh init
- Seven verbs: st_add_repository, st_list_repositories, st_fetch_repository,
5.150.0 — PROGRAMS COMPILE INTO THE IMAGE, and an EDIT IS A DEPLOY (SMALLTALK-SPEC §12.6)
- A create or a deploy compiles the program into its Programs-package class:
- THE OWNER CORRECTED THE DESIGN MID-BUILD and the correction is the
- AN EDIT IS A DEPLOY: a hand-edited pin method is an UNRECORDED DEPLOY (a
- The binding is DERIVED, never stored (class name from program name +
- §12.7 records the owner's IMAGE-TRAP directive — robust git integration is
5.149.0 — SCREENS COMPILE INTO THE IMAGE (D7 resolved — SMALLTALK-SPEC §12.5)
- save_screen/update_screen auto-compile every saved screen into
- THE RUN GOES THROUGH THE IMAGE at the engine's one applyOp call site,
- THE CLASS GOVERNS: a hand edit changes the next run's membership
- THE MATRIX FOUND AN INCUMBENT QUIRK, mirrored not fixed: applyOp's JS
- Saved screens backfill at boot like fields and indicators; presets stay
5.148.0 — THE FACADE COMPILES INTO THE IMAGE: cf_ fields and ci_ indicators are Smalltalk-backed by construction (SMALLTALK-SPEC §12.5)
- A DSL SAVE IS A COMPILE: define_field/define_indicator auto-translate
- THE BINDING IS EARNED: an identity probe proves the class answers exactly
- THE D-A DIVERGENCE RULE at the define door: machine-current classes
- THE BOOT BACKFILL (idempotent, daemon boot): every existing expression-
- NOT in this release and said so: SCREENS (the owner's item 3) — D7 is
- THE COMPILED DIGEST: machine-writtenness is a property THE CLASS CARRIES
5.147.0 — THE CHARTS SECTION: each palette themes the chart drawing surface (TERMINAL-WORKSPACE-SPEC §48)
- Six Charts vars (chart background, text, grid, scale border, candle
- The chart document derives at emission exactly as the terminal does: its
- Precedence stated: a candle color from a chart's own settings dialog
- Unthemed deliberately: indicator line colors (a data-distinguishing
- §47.6's "the chart documents are not themed" carries a dated
- artifact.test.js's build is HERMETIC — it wrote server/client-embed.js
- run-all keeps a FAILING suite's raw output (/tmp/ots-suite-fail-*.log):
5.146.0 — VISUAL COLOR PALETTES: Appearance in the Settings Browser (TERMINAL-WORKSPACE-SPEC §47)
- THE REGISTRY IS THE TRUTH AND THE STYLESHEET DERIVES: server/ui-theme.js
- The promoted literals are gone: --chrome, --titlebar, --titlebar-focus,
- Two palettes: OTS Dark (default, incumbent) and DarkBlue (Pharo). up/down/
- The drill-downs: Appearance FIRST, one collapsed subgroup per palette,
- Validation is the injection guard (values are emitted inside a <style>):
- get_ui_theme / set_ui_theme for agent parity; ui-theme.json rides the
5.145.0 — THE SETTINGS BROWSER: the Pharo model, one settings surface, floating (TERMINAL-WORKSPACE-SPEC §46)
- The Pharo shape: one tree of groups and rows with inline widgets, a search
- It floats, and the float is a DECLARATION: the admin surface vertical joins
- What moved, by the owner's own list: the FMP, FRED and Sharadar API keys,
- The Prompts card is REMOVED by owner ruling (vestigial beside the direct
- Both agent doors follow the declaration: open_settings returns the opened
5.144.0 — D-ARC D-C + D-D: XMPP package transit under the trust confirm, and the lean distribution artifact (SMALLTALK-SPEC §14.4 — THE D-ARC IS COMPLETE)
- D-C:
share_package reviewed into the chat-trust closed vocabulary; the
- Per-verb wire bounds: chat-trust verbs may declare wire_max/arg_max; the
- D-D: st_export_artifact / st_import_artifact (.otspkg, ots-stpkg/1) — the
5.143.0 — D-ARC D-B: the package quarantine (the trust-confirm layer)
5.142.0 — D-ARC D-A: the source-tree projection (git role of the distribution thesis)
5.141.0 — PERF ARC P3+P4: the measured verdicts (SMALLTALK-SPEC §12.1.1)
5.140.0 — PERF ARC P2: hop-resolved lexical addressing
5.139.0 — PERF ARC P1: the CiSeries accelerator (owner: "we should do all of those")
5.138.0 — THE INTERPRETER PERFORMANCE REVIEW (task #77 taken; SMALLTALK-SPEC §12.1.1)
5.137.0 — VOCABULARY PHASE D: the condition sugar becomes the trigger index (D1 RESOLVED)
5.136.0 — VOCABULARY PHASE C: ci_ chart indicators in Smalltalk (SMALLTALK-SPEC §12.3)
5.135.0 — VOCABULARY PHASE B: cf_ fields in Smalltalk (SMALLTALK-SPEC §12.3)
5.134.0 — VOCABULARY PHASE A: interpreter completeness + the pure-evaluation door (SMALLTALK-SPEC §12)
5.133.0 — SMALLTALK P5+P6: MIGRATION, THE BROWSER'S AGENT, THE SUPERSESSION (the arc completes)
5.132.0 — SMALLTALK P4: THE SYSTEM BROWSER (SMALLTALK-SPEC §7, Pharo canonical)
5.131.0 — SMALLTALK P3: THE CEP ENGINE SENDS MESSAGES (SMALLTALK-SPEC §6)
5.130.0 — SMALLTALK P2: THE CAPABILITY BOUNDARY (SMALLTALK-SPEC §5)
5.129.0 — SMALLTALK P1: THE IMAGE IS THE STORE (SMALLTALK-SPEC §4)
5.128.0 — SMALLTALK P0: THE LANGUAGE (SMALLTALK-SPEC, owner directive)
5.127.0 — THE DOCK FOLLOWS CHAT CONVENTIONS (owner UX directives)
5.126.0 — THE EDITOR FILLS THE PANEL, AND THE CARET IS GUARANTEED
5.125.0 — THE EMBEDDED CODING AGENT (the IDE critique's third item)
5.124.0 — THE BUFFER (owner directive, 2026-08-21)
5.123.0 — THE IDE SHELL (owner critique, 2026-08-21)
5.122.1 — SIX EQUAL WEIGHTS SUM UNDER 100 (found by the first live init)
5.122.0 — THE CONVERGENCE MONITOR (P6): the portfolio-program arc completes
5.121.0 — THE PRODUCTION CONVERGENCE BINDING (P5)
5.120.0 — THE PROGRAM PATCH DOOR: propose_program_patch (P3b, LAW C)
5.119.0 — THE QA DOSSIER: run_program_suite (P4b)
- The lockfile — the stage ladder re-validated at this read; pin drift as findings (the program still runs
- The universe, resolved live — an empty resolution is a FAILED section naming why an init would refuse.
- The construction, run DRY — the same resolution ⚑ Init runs, with NO event published, proven on the event
- THE MANDATE PRE-FLIGHT — the section that earns the dossier: the constructed targets evaluated against the
- The policy rules, dry-evaluated through the engine's own no-side-effect evaluator, with lifecycle-phase
- The trading plane — a scan of the STORED canonical conditions (never a recompile of prose) for any rule
- Trajectories — NAMED, NOT BUILT: replaying the composed program over history is the strategy backtester's
5.118.0 — THE UNIFIED init(): establishment through the pipeline (P4a)
5.117.0 — THE PHASE-4 VOCABULARY: lot-level facts (plan-of-record gap 4)
5.116.0 — TCA: THE PIPELINE GRADES ITSELF (plan-of-record gap 3)
5.115.0 — SUPERSESSION + AMENDMENT LINEAGE (plan-of-record gap 2)
5.114.0 — PARTIAL FILLS: a working order fills in tranches (plan of record, gap 1)
5.113.0 — STAGED-QUANTITY RESOLUTION (owner, 2026-08-21)
5.112.0 — ONE PATH, TWO CLASSIFICATIONS (owner elaboration, 2026-08-20)
5.111.0 — THE OPEN ORDER BOOK (owner directive, 2026-08-20)
5.110.0 — THE ALLOCATION/EXECUTION SPLIT: the foundation (owner directives, 2026-08-20)
book.allocation.decided — the typed seam between the PM plane and the trading plane, registered with its
- The eleventh verb,
decide_allocation {targets, phase, note} — category record, touches NO book:
- The phase axis on policy pins — optional, closed, carried through deploys and the stage re-validation,
- THE END-TO-END PROOF, through the UNMODIFIED engine: the suite fires an allocation handler, finds the
5.109.0 — THE UNIVERSE HAS FOUR FORMS: the inline screen (owner directive, mid-review of 5.108.0)
{inline_screen: {filters:[…]}} — the screen DEFINED IN THE PROGRAM. Validated AT THE WRITE through the
- The lockfile says which kind of membership each form is:
symbols […] // hard-coded membership vs
- The New-program form gains the inline-filters input (JSON), taking precedence when supplied; exactly-one-of-four
- What resolves the membership: nothing on the program path yet, and that is stated — the pin is the SELECTION
- Suite: the four-forms test (valid inline verbatim, unknown-field refusal through the bridge, empty-filter
5.108.0 — THE IDE GETS ITS VERBS: create, edit, retract, test, execute — from the panel
- + New program — a form (name; universe as symbols/screen/cohort; construction from the registry picker;
- ✎ Edit (deploy) — select a module file, edit its INPUT JSON in a textarea, state the required
why →
- 🗑 Retract / ↩ Restore — reason required; nothing deleted; the pill flips.
- ⎈ Bind… / ⏏ Unbind — the paper-book picker from the books catalog; production still refuses naming P5.
- 🧪 Test — the per-rule DRY EVALUATION: the engine's own no-side-effect evaluator (
test_rule's function)
- ▶ Run tick — executes the event loop once, the SAME tick the daemon runs on its schedule, summary and
- The console — an IDE has one: every operation's result renders below the editor ($-prefixed, refusals in
- A real defect found by the rig and fixed: a refetch fired from a button the busy repaint had already
/cm/programs POST grows the op set (create | deploy | retract | restore | bind | unbind | tick | test), each a
- Suite extended (7 tests in the p3c file), battery green across all six shards.
5.107.0 — THE PROGRAMS IDE: code, not text (owner directive, 2026-08-20)
- The explorer tree — each program is a project; its five modules are FILES:
Program.lock, mandate.spec,
- The editor — monospace, line-numbered, syntax-highlighted. A policy file's code IS the rule's own sugar
- The pipeline strip — the derived stage ladder as CI nodes: ● draft — ● validated — ○ qa (P4) — ● staged —
- The approval queue as PR review — a pending hold is a review card whose held transaction renders as a
- ↻ Refresh — the renders-once wrinkle found live (open_programs focused the panel without re-fetching) has
program-report.js gains the IDE enrichment: policy_live (each pinned rule's live sugar, version, enabled,
- MEASURED (
scripts/measure-programs-panel.js, real Chromium, real trusted clicks on the shipped document
- Suite updated for the IDE shape (tree/pipeline/editor/diff assertions + the escape-free tokenizer); battery
5.106.0 — PORTFOLIO-PROGRAM-SPEC P3c: THE PROGRAMS SURFACE — measured in a real browser, real clicks, all nine readings PASS
- Administration of nothing new:
Analysis → Programs leads the rail (the tab is what the machine knows,
- ONE payload (
server/program-report.js = program.get() + approvals.list() and nothing else — the
- The cold states are kept apart: no programs is a sentence naming
create_program, never an empty table; an
open_programs ctl verb (and open_analysis {panel:"programs"} addresses it through the derived vocabulary);
- Suite:
server/tests/portfolio-program-p3c.test.js (5) — payload over planted state, cold states, the emitted
- WHAT THE MEASUREMENT DOES NOT ESTABLISH: the rig serves a minimal http shim rather than the full daemon (the
5.105.0 — PORTFOLIO-PROGRAM-SPEC P3a: THE PROGRAM ENTITY — the organizing principle becomes a visible object
- The five modules:
mandate (P1's object, pinned at the version in force — an unset mandate asserts NOTHING
- LAW B mechanical: a program is created WHOLE OR NOT AT ALL (every invalid pin listed, NOTHING WAS WRITTEN);
- THE STAGE IS DERIVED, NEVER STORED (the thesis-journey rule), **and the pins are RE-VALIDATED AT EVERY
- Staging is the renamed strategy↔paper binding:
bind_program {mode:"staging"} binds a PAPER book and earns
- A retraction is a RECORD with a required reason; nothing leaves the disk; restore reverses the reading.
pgm_ minted through the ONE registry; append-only JSONL under the lock; programs/ in the image pack
- Suite:
server/tests/portfolio-program-p3.test.js (7) — whole-or-nothing creation with every problem listed,
- NOT in P3a and named:
propose_program_patch (the agent door under LAW C) is P3b; the Programs terminal surface
5.104.0 — PORTFOLIO-PROGRAM-SPEC P2c: fields injection and playbook promotion — P2 COMPLETE
fields injection into condition-env.build — the bars_array mirror (audit R4): a BRANCH WORLD hands in the
- Playbook promotion (
rule-migration.promotePlaybook, tool promote_playbook) — the migrate_alerts
- The release's own guard bit during the build and is recorded: the first draft of the promotion's back-pointer
- Suite:
server/tests/portfolio-program-p2c.test.js (5) — injection end-to-end with the posited marking and both
5.103.0 — PORTFOLIO-PROGRAM-SPEC P2b: escalate, the approval queue, and book state at the tick (LAW C)
- Non-blocking, it is a typed ask: a first-class
escalation inbox kind (the 5.8.0 rule — never relabelled
- **Blocking, it HOLDS every LATER propose_* action of the same firing.** The transaction is computed EXACTLY as
- The ordering that could never work is refused AT SAVE: a blocking escalate listed after a propose would ask
server/approvals.js — the queue. Append-only JSONL under store.withLock from its first line, monthly
approve_proposal books the held record at the FIRING price through paper-book.append — THE ONE DOOR —
- Four event kinds —
book.action.escalated, book.proposal.held (rule-actions), book.proposal.approved,
- The standalone tick now supplies book state for a
book(...) rule (P2a's stated debt, one release old):
- The reach layer learned the book plane:
book:* predicates resolve to the book.ledger family — our own
- Tools:
get_approval_queue, approve_proposal, decline_proposal. Sugar: `=> ESCALATE "…" blocking=true
- Suite:
server/tests/portfolio-program-p2b.test.js (6) — the hold end-to-end (book provably untouched, firing
5.102.0 — the book leaf: portfolio-plane rules become expressible (PORTFOLIO-PROGRAM-SPEC P2a)
5.101.0 — limit vs actual everywhere the series lives, and the mandate outranks the strategy (PORTFOLIO-PROGRAM-SPEC P1b — P1 complete)
5.100.0 — the mandate: the program gets its spec, and real money gets its gate (PORTFOLIO-PROGRAM-SPEC P1a)
5.99.0 — the monitors get their clocks (PORTFOLIO-PROGRAM-SPEC P0b — P0 complete)
5.98.0 — the ledger made sound: the lock, the cause vocabulary, and retraction deletes (PORTFOLIO-PROGRAM-SPEC P0a)
5.97.0 — the governed board: the Worldview mode rebuilt as the adjudication surface (GOVERNED-WORLDVIEW-SPEC Phase 4, the arc complete)
5.96.0 — the worldview discussions: the argument becomes part of the record (GOVERNED-WORLDVIEW-SPEC Phase 3)
5.95.0 — the worldview refresh batch: the fifteenth declared process, proposing and never writing (GOVERNED-WORLDVIEW-SPEC Phase 2)
5.94.0 — the worldview becomes governed: proposals, adjudication, and the owner's prompts (GOVERNED-WORLDVIEW-SPEC Phases 0+1)
5.93.2 — the thesis strip reads as the journey is walked
5.93.1 — the thesis modes get their way back
5.93.0 — the body stops repeating the title bar
5.92.0 — Reader is the one renderer, and the split is yours to set
5.91.0 — Reader view: the route around publisher bot-walls
5.90.2 — the story frame grants everything an embedder can grant
5.90.1 — the description fits through the installer's own door
5.90.0 — the News view is a reader
5.89.1 — the published MCP metadata stops lying
5.89.0 — the agent contract stops rotting: the action vocabulary is derived
5.88.0 — the tab is called Community
5.87.0 — the security context follows the focus
5.86.0 — IRC is removed, totally: the chat layer speaks XMPP only
5.85.4 — the scrollback follows the conversation, and the documentation catches up
5.85.3 — both delay dialects, because the live server falsified one
5.85.2 — the join backlog is a replay too
5.85.1 — the tab is called Surface
5.85.0 — the share is a registry
5.84.0 — distributed cut and paste, the chart first
5.83.0 — browse, not recall
5.82.0 — the surface speaks the person's language
5.81.1 — room creation, confirmed
5.81.0 — the competitive client
5.80.15 — XEP-0313: the client stops missing messages
5.80.14 — the conversation panel polls too
5.80.13 — the door refused the view the renderer was built for
5.80.12 — the reconnect that dialed the wrong socket, and the say that sent nothing
5.80.11 — the poll that never existed
5.80.10 — the flight recorder, and the agent verbs the diagnosis needed
5.80.9 — a ghost socket cannot kill its successor
5.80.8 — the first live session, and the panel that did not follow it
5.80.7 — the reader restarts with EVERY stream restart, and there are two
5.80.6 — the certificate decision: allow self-signed, per account, disclosed
5.80.5 — the close that follows an error must not erase the error
5.80.4 — XMPP encryption is STARTTLS, not a socket kind
5.80.3 — every POST on /chat was broken, and the owner found it with one button
5.80.2 — three changes from looking at the Chat accounts panel
5.80.1 — two defects the owner found in the first minute of looking
5.80.0 — the chat layer: XMPP and IRC, boundary first
5.79.1 — two defects the owner's own screen found, minutes after publishing
5.79.0 — the portfolio report becomes a tearsheet, and the analysis behind it
5.78.0 — the Screens panel, a dead cohort click, and a Support page that crashed
5.77.0 — a book is marked at the prior close, and says so
5.76.1 — one screenshot, three defects
5.76.0 — named workspaces
5.75.0 — one terminal, not two
5.74.3 — the order of the bar becomes data
5.74.2 — the tab that took itself back
5.74.1 — a panel that could not say which book it was
5.74.0 — the tab bar stops growing with the data, and a book becomes a panel
- Table ids carry their book. 5.72.0 recorded this precisely: "a sort is
- The runtime registry clears per book at panel close, not per surface at
- **
sort_portfolio_table resolves a bare id, or refuses by naming the
5.73.4 — a blank terminal, and the guard that finally runs the code
5.73.3 — visible and dead
5.73.2 — the guard that cried wolf
5.73.1 — a disclosure an agent cannot reach is not a disclosure
5.73.0 — a panel bound to a book
5.72.0 — the renderers stop asking what is selected
5.71.0 — the portfolio rail stops growing with the data
- NAVIGATION is the left menu: the eight views, DECLARED ONCE. It answers
- CONTROL is a narrow strip above the page and below the tabs: which
- THE WORK is the sub-pages, below both.
5.70.2 — the fix that crashed, and the test that could not see it
5.70.1 — a refusal names its obstacle, and a moved panel is an obstacle with an address
5.70.0 — Analysis is data, events and processes
5.69.1 — the horizon toggle, and a sort that reaches the server
5.69.0 — two verticals, four tabs
5.68.0 — one calendar
5.67.0 — the Research tab: what is out there, and when
5.66.2 — the journey leads, the instruments follow
5.66.1 — nine dead panels, and the question that killed them
5.66.0 — the journey gets a tab
5.65.4 — one table, filtered, rather than eight shelves
5.65.3 — the trap at the far end of the ceiling
5.65.2 — the surface that hangs was the one that could not say what on
5.65.1 — four characters, four days, and three layers that made it worse
5.65.0 — the run beside the tally, and debt as a level
5.64.0 — two readings off the ladder the aggregates could not make
5.63.0 — one guard, two registries, and a place to see them
5.62.0 — derive, promoted to global scope
5.61.3 — an absence is not a fact to cache
5.61.2 — a tool nothing can invoke is a library
5.61.1 — the guards were red and the release went out anyway
5.61.0 — a vendor is a time series, not a verdict
5.60.0 — the evidence, not the count
5.59.0 — adjudication: the first release that can move a number
5.58.0 — the routing seam never ran, and the catch hid it
5.57.0 — governance over ingestion, not quality over it
- Administration → Data management, with Processes moved into it (one
- An observation is appended; an event is a fold. Forced by arithmetic
- Law 2, on every event. The incumbent persisting is the **absence of a
- Routing only. Three detectors that already existed — cross-block
- The observer may not take down the pass it observes — the routing is
- Three cold states kept apart: never-detected, clean, and all-suppressed.
5.56.0 — the tenth mapping, and the one that agreed twice before it refuted
salesTtm → Sharadar revenue at ART — ADMITTED. Measured against MRT
operatingMargin → Sharadar ros — REFUTED, and it refuted the hard way.
5.55.0 — a download cannot fix a load failure, and four sentences that outlived their release
- A seed that already holds a bulk artifact refuses to download again, names
- That pass does not blame the vendor. It gets its own yield arm, because
- Identical bytes are not written as a second artifact, recognised by
- The claim is now one declaration.
local-store.CONSUMERS, composed by
- **It is asserted equal to the set walked out of the tree, in both
- The three dead literals are forbidden anywhere in the tree by name.
5.54.0 — one column, two names
- An ordered candidate list, not a third rename.
vendor_column: 'date'
- Exactly one must be present, and a header carrying BOTH is refused. Two
- Which one answered is recorded on the build record as
- The required-column set is derived from the declared primary key through
columns_unmapped / columns_missing now survive the header, so the next
5.53.0 — every entry point is a skill, and six of them may not fire themselves
- The migration moves no behaviour, by construction. All seven carry
commands/ is gone. A command and a skill of the same name are two copies
- Every
/ots:… you already type still works. Names are preserved.
- A skill that needs an argument refuses to guess without one.
$ARGUMENTS
- The frontmatter is machine-clean — no commentary between the keys, because
- Docs updated — README and the user guide now state that the
/ skills are
5.52.0 — the seed that downloaded 113 MB and admitted none of it
- A ZIP READER, THROUGH THE CENTRAL DIRECTORY — never through the local
- THE DECOMPRESSED BUFFER IS WALKED, NOT STRINGIFIED.
MAX_LENGTH is 8 PiB
- ROWS ARE EMITTED, NOT ACCUMULATED. Phase 1's
rows array is right for one
loaded_nothing — a sixth state. complete tested bookkeeping and was
- A header without the key columns refuses at the header, naming the missing
- The artifact name carries the depth that was fetched.
'-full-' was
5.51.0 — one saved record per security, and two banners removed
5.50.0 — the save that was real and was never seen again
5.49.0 — the refusal that was half false
server/asfiled.js — the reader: the measured field map, the declared source
local-store.asOfRows — the first door in that module that returns a row of
pitVerdict()'s gated arm admits a covered, mapped leg and **publishes the
reach.js's filing.statements verdict moves from gated to
get_asfiled_mapping, get_asfiled_field, get_asfiled_coverage.
scripts/measure-asfiled-break.js — 255 as-of cells over the corpus, 75
5.48.0 — the price above the chart, and the build below it
- The change names what it is measured against. A declared three-rung chain,
- It carries its own timestamp, always —
at 08/13 17:44.
- One writer, at the one funnel.
syncHeader() is the only writer of the
- The hover readout no longer outlives its bars.
#ohlc is cleared on
5.47.0 — the second source, and a deletion somebody could swear to
5.46.0 — the notice that was owed, and the store that nothing reads
5.45.0 — Sharadar, rebuilt against the publisher; and §29 as a dated correction
- An unrecognised key is not refused. No key, an empty key and a well-formed
- An unsupported filter is silently ignored.
reportperiod.gte returned the
- An unknown dimension returns zero rows rather than an error, so the closed
- 403 is ambiguous between no key, an invalid key and an unsubscribed table,
5.44.0 — Sharadar, the point-in-time fundamentals source, and nothing reads it
- The ambiguity is disclosed rather than resolved. What was measured is that an invalid key
node scripts/measure-sharadar.js takes the measurement in one command and **refuses by
- No live
vintageDelta has ever run.
- 5.44.0 has never been opened in a browser.
Also in 5.44.0 — the documentation, recaptured from a live terminal
- Administration — Processes, Reach, Conditions and Events now show the thirteen-panel
- The portfolio tables — Holdings, Performance and Allocation. The old images were full-bleed
- Four surfaces that had never been pictured — the economic Calendar (the monthly zoom, a
5.43.1 — a read never waits for a model call, and a refusal that stopped guessing
5.43.0 — analyst rating changes, sliced by firm
- The window is the bar span because
barTimeFor returns null outside it: a count over
- EXACT or A FLOOR is derived from the stream's own first date, never asserted from the cap
- The provider spells one firm several ways and we DISCLOSE rather than merge. Measured on
5.42.0 — scroll in a size the machine chose, fit a size you chose; and one house table
5.41.0 — the panes divide the height the panel gives them
5.40.0 — discovery is a job with a poll, and the budget stops starving the evidence
5.39.0 — the thesis journey: four stages, one panel, and the screen as a reviewable candidate
5.38.1 — the retrofit plan called a criterion unrecoverable while folding four of them in
5.38.0 — the superset thesis: one model for a built and a discovered view
5.37.0 — zero is an error, the timeout was ours, and the gate rejected on the wrong axis
- one SURVEY call that names the structural changes worth writing up;
- one call per shortlist entry, asking for that thesis whole.
5.36.0 — the bound that was sent and rejected, and the report written in parts
- The shapes are declared in order (
enabled → adaptive → unstated); a 400
- A fallback never shrinks the ceiling.
max_tokens is carried across every
- A shape that cannot bound reasoning says so;
output_config.effort is named
- Every refusal now reports the request that was actually sent — the exact body
- The section plan is derived from the resolved prompt's own declared structure
- One system prompt for the whole run so the cached prefix reaches the bundle at
- A partial result is a recorded state: sections that arrived are kept, sections
- One
bundle_digest and one prompt version for the run; the **six axes are scored
- The wait states what it is waiting for — both ends of the fraction or neither.
- Thesis discovery shares the seam and states why it does not split: its answer is
5.35.0 — the whole token budget went to reasoning, and prompts are now editable in one place
5.34.0 — the thesis discovery agent, and the adversarial gate that never bound
- THE FORCED ADVERSARIAL PASS HAD NEVER BOUND AT THE FRONT DOOR.
- A CREATED THESIS DISAGREED WITH ITS OWN PROBABILITY HISTORY FROM LINE ONE
- **THE STORE WAS AN UNLOCKED WHOLE-FILE READ-MODIFY-WRITE THAT TWO PROCESSES
delete_thesis WAS A HARD DELETE of the statement, evidence, challenges,
list() THREW on a record missing linked.
- TWO UNDECLARED NATURAL KEYS BEHIND A RENAME, NOT ONE: the screen-run
/th/materialize HAD NO MCP TOOL — a daemon route the agent surface could
5.33.0 — every portfolio table sorts, and the chart default stops being a secret
5.32.0 — three lenses, a prompt library, and a bundle that says what it could not establish
- The transaction entry form has been invisible since 5.19.0. Choosing a
- **
valuation_surface_cell has refused for every symbol on every installation
- The bundle's
as_of wall clock, the silent over-budget ship, and the
set_view's view list was hand-written and had lost perspectives. The
get_decision_scorecard discarded its symbol unless list:true was also
#disc-overlay painted below every panel. The first-run legal gate is a
5.30.1 — what the cohort pass costs, and the refusal that quoted the provider
5.30.0 — the same events on the chart, under the same provenance
- A twelfth event source,
UE. It inherits the legend, the all-on/off row,
- Provenance on EVERY marker, first, above the label.
server/events.js
- A row with no provenance says so — *source not stated by the producer* —
- An event associated with a SERIES draws wherever that series is overlaid,
- Clicking one opens the calendar drill-down on its chain — the chain id
CHART_MARKERS_SHIPPED flips, and the *not yet available* sentence stops
5.29.0 — the owner's own calendar events, merged under visible provenance
5.28.0 — knowability: a DATED METHODOLOGY BREAK on every EDGAR series
- **FRED, price, index, model, container and expression series are
- FMP statement series are BYTE-IDENTICAL too. That branch has dated its
cleanPoints widens by exactly one field, on opt-in. Nineteen of twenty
- The correction can only ever REMOVE foresight. A filing date is never
- IT HAS NEVER BEEN OPENED IN A BROWSER.
- The
end-only dedup is disclosed and unfixed (above), as is the
- §10.5's broader vintage refusal is designed and NOT taken. A
- **
fmp: financial series still date their POINTS by the filing date while
- No FRED observation has a
knowable_at. §3.4 says it is the release
5.27.0 — the disclosure that reached nobody, and two deferrals closed by measurement
- The vocabulary is CLOSED to §10.2's four words (
current_vintage,
- **An undeclared source publishes
null AND a reason, never a borrowed
- AN EXPRESSION IS BOUNDED BY ITS WEAKEST LEAF, with the per-leaf reading
- NO NUMBER MOVED. Every change is additive: two fields on a payload, a
- NEITHER METHODOLOGY BREAK IS TAKEN.
server/series.js still drops filed
- The
end-only dedup is still a measured defect shipping unfixed, carried
submissions/CIK##########.json is still unwired, with the reason stated:
- IT HAS NEVER BEEN OPENED IN A BROWSER. Whether
⚠ period-end dated fits a
5.26.1 — the guard that manufactured a measurement
index.js / routes/cm.js — an absent effectiveTaxRateTTM became a **tax
model-scope.js — an absent per-share value became $0.00, in a block whose
rule-actions.js — an absent quote price **marked a paper-book position at
backtest-store.js — an absent keep_per_strategy became keep 0, a prune
search-ledger.js — an absent Sharpe was recorded as **0.0 in the trial record
strategy-loop.js — an absent days became 1, narrowing a walk to a day.
econ-calendar.js — link_calendar_event with a null release_id was
server/num.js is the ONE numeric admission door, deciding by TYPE
- **A derived law fails the build on any finiteness test applied to a
- The boundary moved. The defect lived in
scripts/, and every derived law
- The judgment reader is a pure function separated from every write, and the
--template ships, and the fetch emits one automatically, so the shape can
- A raw-fetch fingerprint is now required. A judgment is an assertion about a
- The backtest equity curve now drops an absent point instead of plotting it
- The measurement is still not taken, and nothing was fabricated to fill the
- The bare
isFinite family is only half closed, by decision: ~120 sites use
- It has never been opened in a browser.
5.26.0 — the vintages we were already paying for, and the contact you could not test
5.25.0 — the picker stops having a list
- never pulled — names
econ-calendar-pull **and the two doors that still
- warm-up short — prints what it has and names the shortfall as THIS
- warm — a release with no chartable series is the world being empty, which
- the method is FIXED IN SHIPPED CODE — twenty declared, owner-realistic
- the number is READ FROM A RECORD, never written as a literal. The surface
- the refusal rides the surface in the 5.22.0
link_coverage shape, with
- the script will not judge its own search. It fetches, prints every query's
5.24.0 — one identity, one boundary, one door
5.23.0 — the chart button asks where, and a security pane answers
5.22.0 — the lane was firing and had nothing to reach, and the calendar became a calendar
- The warm-up covers every release dated in the window, in a declared order — link candidates first (they decide the rate), then the rest by release id. 5.21.0 inherited
Set insertion order, so the outcome depended on which provider row arrived first.
CAL_SERIES_WARMUP_BUDGET (300) replaces the literal, sized to converge in one pull: FRED carries ~300 releases, the cache is per release, a warm entry is skipped free, and FRED's own 120/min bucket waits rather than failing — so a cold installation spends ~150 s of throttling once and nothing after. Uncapped was rejected: an unbounded loop against a provider is not a fix.
- A cold cache is named as THIS PROCESS'S shortfall in the
yield block, separately for each cost. It is neither a provider failure nor a configuration fact and may not be excused as either.
lane_available rides every verdict, every occurrence and link record carries series_lane: warm|cold, and link_coverage.series_lane rides every pull and every panel read. Below 100% the rate is a floor, not a measurement, and the panel says so Tier 1.
via_series was being thrown away — the lane read sr.series_id off a cached FRED row whose id field is id, so every live series-lane link recorded null.
- Supersession is not a conflict, and the difference is the evidence. A conflict is two derivations reading the same evidence and disagreeing, and it must refuse. A cold-derived link that a warm pull disagrees with saw strictly less of what FRED publishes: it is superseded and recorded, cold → warm only.
- The store can hold the third link state.
link was only ever linked or unmatched, so fmp_only was structurally zero — a state the spec names and the store could not hold, with a different remedy (search_series vs link_calendar_event).
- ANNUAL — twelve month mini-grids, every day a cell, density carried by intensity rather than text, anchor days ringed, today outlined; a month name opens the month, a day cell opens the day. A year is on the order of eleven thousand occurrences: this view shows SHAPE, and the density sentence says so.
- MONTHLY — a real seven-column month grid with the leading blanks computed from the first weekday. Every cell carries its full count, up to four ranked chips and a real +N more that opens the day.
- WEEKLY — seven day columns with the occurrences under each, time, consensus and actual, and the anchor strip beneath.
- DAILY — everything, in full, in release order, in the table that is the right shape for one day.
5.21.0 — the economic calendar, and the rate that was measured before it was assumed
- FRED: 914 dated occurrences over 170 distinct releases, across 31 days.
- FMP: 100 US medium/high-impact rows for the same month.
- The matcher links 22 of 100. Ten refused as ambiguous, 68 kept as unmatched, every accepted link correct by inspection.
server/econ-calendar.js — the entity, the store, the two-lane matcher with its ambiguity refusal, the four zoom levels, the drill-down, the chart-jump resolution, and the RELEASE_KINDS vocabulary. Nothing is dropped and nothing is guessed: linked / fred_only / fmp_only / unmatched, each with what it can and cannot do.
server/econ-archive.js — the point-in-time consensus archive. Append-only JSONL under store.withLock from its first line, and it lives beside pit.js, never inside it, because that module's *"IT MAKES NO PROVIDER CALLS"* is load-bearing — it is what lets pit-snapshot refuse by name rather than fetch. Consensus-before-print is only recordable before the print: once a number prints, every provider rewrites the row, and ALFRED — which does serve vintages of the actual — has no consensus at any price. Every release that prints before this file exists is a permanent hole.
- The
econ-calendar-pull process, twice on weekdays, with its own daemon clock. One pull a day is not a cheaper version of it: the dominant US release time is 08:30 ET, so an evening-only pull structurally cannot record a consensus before a print. The yield block carries the closed verdict vocabulary and the not_this_process table in which a provider 429 is not this process, a missing key is not this process, and an occurrence that could not be LINKED IS this process and is never excused.
- Macro-economic → Calendar — one
MACRO_PANELS row and one renderer, open_macro {panel:'calendar'} working with no new verb. Density is handled by aggregation with a published denominator, never a cap that hides: daily and weekly show everything, a monthly cell prints its full count with ranked chips and a real +N more, and the annual view shows density and anchors and says that is what it is. A derived test drives one window through all four projections and fails the build if the counts disagree.
- Seven tools and four ctl verbs, with
ctlState() publishing what the panel shows — including whether it is in FMP-only degraded mode.
- The merge.
unified-calendar's source 3 reads the store instead of becoming the fifth caller of FMP's economic-calendar, so the economic native type — the only one whose target was null, since 3.93.0 — now has a destination, and the same event id appears on both surfaces. get_calendar keeps every FMP key and only adds.
- §8.1 estimated *"a few thousand"* dated occurrences a year. Measured: 914 in one month — roughly 11,000 a year. Every density decision matters more, not less.
- §2.5 says
economy.MEASURED maps seven container variables onto FRED ids. It maps three. The other four live in economy.HISTORY, and the anchor join is over the union of both maps, naming both.
5.20.6 — one nesting behaviour, declared by whoever owns the children
- Portfolios *did* nest, correctly, and was never reported — with its own
.cat.open, its own .items box and its own caret, keyed on the selection. A second implementation of one behaviour is the thing this house does not keep. It now declares children.
- Alerts and Market are flat lists with nothing under them; they route through the same function so a child set declared next year nests with nothing for its author to remember.
- Administration is NOT unified with nesting, and that is a decision. Eleven panels in four group headings is a *different mechanism*: a heading is a divider over peers, a nested child is one click deeper under a parent that is itself a destination. Administration has the first and no instance of the second, so merging them would put chevrons on eleven panels with nothing under them. The day one declares children it will nest, through the same function, with no rail change.
rail — which of the six rails is rendered, from the same RAILS table renderRail dispatches on, so the payload cannot disagree with the paint. A null id is a real entry: Cohorts and Marketplace have no rail.
expandable[] — the parents that declare children, derived from the registry that rail iterates, each saying static or provider.
expanded / expanded_record, with an absent key distinguished from null.
- and
control_terminal set_rail_expanded {rail?, category, expanded?} — a fact the payload publishes that the agent cannot change is a half-built contract. Expanding a leaf refuses by name and lists the parents that do nest.
5.20.5 — the file and the program were two different texts
- 5.20.5 has not been opened in a browser — the only connected Chrome is remote and cannot reach the owner's loopback. What was done, and what no previous release did, is load the shipped document into a real DOM, reproduce the defect there, and watch it disappear.
\uXXXX and \xXX are deliberately not policed: they cook to a printable character on purpose.
- The template literal is not removed. Authoring the client in its own
.js file read at emit time is the structurally strongest fix and the stated direction; moving 18,000 lines was judged too large for the release that fixes the button.
- No operation, verb, geometry, panel record or stylesheet rule changes. The owner's arrangement is untouched.
5.20.4 — the measurement nobody had taken, and the stack that was an accident
- The interceptor is not named. This release establishes that nothing in the emitted stylesheet *can* cover a control — the only boxes in
[100, 999] are panels, and within a panel the cluster outranks every grip — and it makes his page report the answer. The next reading is a measurement rather than a fourth hypothesis.
- 5.20.4 has not been opened in a browser. The only connected Chrome is remote and cannot reach the owner's loopback; the sandbox that could serve the page has no inbound route (tested).
- The z collision is a proven defect and not a proven cause. Panels cascade downward, so a lower panel does not cover an upper panel's title bar; the tie would produce dead controls only where two panels actually overlap at the cluster. Whether that is what he hit is exactly what the new hit test measures.
- Nothing about the owner's arrangement changes, and his workspace was read read-only throughout.
5.20.3 — the controls were under the resize strips
- The 5.19.6 guard already declines to start a drag on a control, and
wsBindGesture captures only *after* start(ev) returns anything other than false. No capture is taken.
- Measured in a real browser with trusted clicks, on a rig carrying the shipped rules and the shipped construction: a click at the centre of all five controls produced
pointerdown → mousedown → click → the handler ran.
5.20.2 — the adopter list that rotted twice, and a wait that says what it is waiting for
- Several suites replace
server/fmp.js at the require seam with a fake carrying only what they need; withMemo is not one of those functions, so calling it directly threw. server/memo-scope.js is now the one door, falling back to a pass-through that costs requests and cannot change an answer.
server/serve.js returned the /cm route's promise from inside its try, so a rejection escaped the catch and no response was ever written — an open socket and a spinning panel, latent since the route was extracted. Fixed with an await, and pinned.
5.20.1 — the window that was never established (and the nine calls a panel paid for a pack it never reads)
5.20.0 — a sign test is not a floor, and a threshold that can only fire one way
- It does not rebase the historical per-share series. Every model's
series() still divides by the filed statement share count, never rebased to listed shares and never scale-checked per point, while basis.js's whole doctrine is that per-share value should be aggregate(t) × FX(t) ÷ TODAY'S LISTED SHARES. Fixing that re-derives every grid point in the system and every precision weight computed from them. It is a larger dated break than this one and it is named rather than done (OPPORTUNITY-SURFACE-SPEC §9.8).
- It does not rename
vintage_thin, and does not fix the 200-character truncation that cuts a vehicle_unmodeled reason mid-word.
- It sets no new colour, moves no cadence, no budget and no schedule, and adds no engine capability.
- It was not opened in a browser. The two client changes — the yield note on a process row and the evidence marker in the treemap tooltip — are asserted by derived tests over the shipped source and were not seen rendered.
5.19.11 — thirteen refusals nobody re-asked, and a screen that could not see what it could not see
- A sweep, not a per-cohort retry. A per-cohort retry runs *inside* the storm: the whole point of a 429 is that the next call is likelier to fail than the last. The sweep runs minutes of real work later — the cheapest possible backoff, and it costs no timer.
- Exactly one. A loop that retried until it succeeded would turn a provider outage into an unbounded pass. It is additionally bounded by count; a storm that refused more than that is an outage, and re-asking two hundred times spends the key to confirm what the first two hundred refusals already said.
- Pause is honoured at the same boundary as the main loop, and a halted pass does not sweep at all — that would be work the halt was pressed to stop.
- A cohort refused twice is a different fact. The first refusal is consistent with weather; the second, minutes later, is evidence about the cohort or about a provider that is still down. Both readings are kept, the counts ride the one sentence every surface prints, and a refusal that survives the sweep moves the run record off
success.
- it distinguishes fetched-and-genuinely-absent from the-fetch-failed — the provider answers an uncovered symbol with HTTP 200 and an empty array, so only a transport error or a non-2xx status is recorded;
- it carries the HTTP status, so a 429 (spend, retryable) reads differently from a 403 (entitlement, permanent);
- it carries the stage, so a failure that changes *who is in the answer* is told apart from one that changes only a displayed column.
5.19.10 — the stale record that was a lock
- The in-process flag outranks everything. A mid-tick gap in our own heartbeat is not death.
- A dead pid is proof of death — orphaned at second one, no threshold involved.
- A live pid is not proof of life and never extends liveness. Pids are recycled; a live pid only fails to *shorten* the window. Asserted in both directions, because getting it backwards trades a ten-minute stall for a permanent one.
- Host scope is explicit.
hostname is stamped on every new record. A record that positively says it was written elsewhere does not get its pid tested and says so on the payload; an *absent* hostname is unknown rather than foreign, so the check still bites on the records already on disk.
- A read never writes.
fillStatus is pure, and a derived test fails the build if writeFill ever appears inside it — a panel refresh must not be able to destroy a record a live process is keeping.
start refuses only against a live heartbeat and takes over a dead one, retiring the old record rather than silently overwriting it and disclosing took_over_from — the dead loop's pid, start time, last heartbeat, ticks and computed count — on both the reply and the new record. The queue resumes from the stalest names; nothing it computed is recomputed.
stop against a dead loop retires the record and says plainly that *nothing was stopped, because nothing was running*, keeping every counter the dead loop accrued.
- The daemon retires at boot what it cannot possibly own. It knows it just started, so a loop recorded as in-flight before that instant is not one of its own. Conservative by construction: it retires only what the one reading calls orphaned, so a fill legitimately running in the no-daemon fallback process — live pid, fresh heartbeat — is never touched.
- The ledger stops losing crashed runs.
processes.retireOrphans() records each as a failed run carrying its last progress, statusOf reads an orphan red *before* the ledger catches up, and processes.start no longer takes over an orphan in silence.
5.19.9 — five of every six requests were for bytes we already had
5.19.8 — four claims the code did not honour
- Economy — every phase built (4.39.0–4.46.0). Not built: multiple peer economies, the generic rung engine.
- Economy inheritance — 1a/1b/1c/1d built (4.42.0–4.44.0). At flag 0
forValuation() returns {} before any economy read, so the cost of equity is byte-identical to the incumbent while the disclosure computes the delta anyway.
- Industry container — phases 0 and 1 built (4.47.0, 4.48.0, 4.50.0).
- Industry dependency — 2a and 2b built (4.51.0, 4.62.0). 2c, the fitting program, is not: every elasticity is a template or an owner assertion, none carries
fitted provenance, and industry dispersion is therefore an honest zero width.
- Valuation structure — phases 0 and 1 built (4.0.0, 4.2.0). Phase 2 is blocked by design on an E11 verdict that has not been filed. Phase 3's UX is partial.
- Statement substrate — Phase 0 (4.67.0) and Phase 3 (4.75.0) built. Phases 1, 2, 4 and 5 are not, and the honest reading is stronger: NOTHING CONSUMES THE SUBSTRATE. Its only three call sites are the tool, the route and the test.
5.19.7 — the documentation caught up with the software
docs/index.html — rewritten. The three standing rules stated up front (*a missing input produces a refusal, never a zero*; *disclosure precedes adjustment*; *it shows you its own limits*), then the workspace as the organising idea rather than a feature list: any number of panels freely arranged, the charting workbench, cross-security comparison on one time axis, the statement substrate, Perspectives with its provenance pinned, the event engine and paper strategies, the backtester that declines rather than flatters, the portfolio analytics, and the screening vocabulary with the Modernized G & D standard.
docs/guide.html — rewritten and roughly doubled. New sections covering the workspace end to end (the panel-pins-its-security rule, opening/duplicating/closing, minimizing and arranging, per-panel chart state, the pinned surfaces, what it does not do yet, and driving it from the agent); Administration and the process status law; the one selector and two-tier disclosure; chart grids and relative views; valuation structure; the pro forma substrate; the economy and industry containers; the transaction vocabulary, cost basis and the reconciliation that must sum; and the whole event-processing arc — reach, typed events with their two timestamps, the condition language, rules and the tick, actions and the paper book, strategies, and the backtester with its refusals and its deflation.
- Fifteen new screenshots, captured this session from the live 5.19.6 terminal:
workspace, chart-workbench, chart-grid, cross-security, statements, model-of-models, portfolio-holdings, portfolio-performance, portfolio-allocation, cohorts, news, events, conditions, processes, reach.
- Five superseded images removed from the source tree —
attribution.png, chart.png, model.png, performance.png, settings.png, all dated 2026-07-17 and none of them still referenced by any page.
5.19.6 — the drag that committed perfectly and was invisible
5.19.5 — the drag that never ran, the charts that could not get a socket, and the count that was not accumulating
5.19.4 — the bar that spanned the wrong region, and the menu that looked like it never cleared
- which view the focused panel is showing —
.focused, accent background plus an inset accent bar, on EXACTLY ONE node, derived from state.workspace.focused. It goes to the most specific reachable node, so an expanded category hands it to its matching item;
- which views have open panels —
.railn, a quiet count pill. A COUNT, not a dot, because decision §21.3 makes two panels of one view ordinary and a dot cannot report three;
- a focused panel on ANOTHER security says which, because
active_symbol is the focused panel's security while the rail acts on context_symbol.
5.19.3 — the panels drew their frames and nothing else
5.19.2 — the workspace on a 49-inch screen
5.19.1 — the live read, and the five things it found
5.19.0 — the pane becomes a workspace
active_symbol_means — which of the two it currently is, in words;
context_symbol — the strip selection, i.e. what the LEFT MENU will open next;
context_differs — true when the focused panel and the strip disagree, with context_note naming both;
focused_panel — the panel id.
- A panel is a bound view. Opened against a security and a lens; neither changes for its life. No verb, no gesture and no agent call retargets a live panel. That bind is what makes eight panels readable and what lets AAPL sit beside MSFT.
- A reduced panel is still open. Minimizing sets
display:none and does nothing else: the DOM node, the scroll offset, the rendered DOM and the loaded data all survive, restoring issues no fetch, and the taskbar entry is the proof. A minimize that discards is a close with a friendlier name.
- The top strip is now a LAUNCHER. A strip entry is a security CONTEXT; clicking one chooses what the left menu opens and changes no open panel. Because the behaviour of the one control the owner said "makes sense" is what changed, the click visibly marks every panel bound to that security — a click that produces no visible result is how a user concludes the software is broken. The strip is the UNION of your contexts and the securities of open panels, and
✕ on an entry with bound panels asks BY COUNT and offers to keep them.
- Hand-rolled, zero dependencies: drag (transform-only, one geometry read at pointerdown,
pointer-events:none on bodies so a chart iframe cannot swallow the drag), resize from four edges and four corners, z-order, constrain-to-frame, maximize/restore, cascade and tile. Tile REFUSES BY NAME rather than producing unreadable boxes — *"cannot tile 9 panels in 1180×720: a cell would be 393×240 and AAPL · Chart 1D needs 520×360; 4 would fit"* — which is the refuse-rather-than-default law applied to geometry.
- Nothing is silently dropped. A restored panel whose view no longer exists opens as a REFUSAL PANEL in its saved geometry, naming what is missing, what would fix it, and its successor — taken from a data table beside the registry, not a branch in the restore code.
- The performance budget is structural.
renderAll() no longer calls the content dispatcher in a security context at all: it paints chrome, and a panel body renders EXACTLY ONCE, at open. Focusing, dragging, resizing or re-ordering a panel cannot reach any body, and a derived test proves it.
- Carried by the migration: the three duplicated retired-index migration blocks — at load, in
renderRail and in renderContent — collapse into ONE table and one migrateActive(). The build now fails if that table appears twice.
5.18.0 — the clocks start
pit-snapshot — new, monthly, and the owner's decision is BOTH LAYERS UNIVERSE-WIDE. THIN (membership, price, sector, industry, exchange, market cap for every name in the cached NYSE+NASDAQ universe; membership IS the presence of the row) and RICH (enrichment panel fields, surface model output, industry percentile ranks), both over the ENTIRE universe rather than the analysed names, with absence recorded as data in three states kept apart: held, absent carrying the producer's own reason verbatim, and never. The reasoning is recorded on every manifest — a screen backtest asks what the screen would have FOUND, which requires records for names nobody was looking at; an analysed-only archive makes that question circular and cannot be reconstructed later. It makes no provider calls: it reads local stores, and a missing universe cache is a REFUSAL BY NAME rather than a fetch. The monthly cadence carries its arithmetic on the record — it matches the rate the underlying stores actually change (panels rebuild on a 30-day window over quarterly filings, so weekly would record the same bytes four times), it clears the 60-day accrual floor and the two-snapshot minimum inside one quarter, it holds storage near 100 MB/year at FULL panel coverage against ~440 MB/year weekly, and it shares a clock with the universe archive so the two join by month without interpolation.
universe-archive gets the scheduler it never had, and the opportunistic freeze now records its own run naming itself as opportunistic — a process whose work happens through a door it does not know about reports the opposite of the truth.
cohort-refresh gets a daemon fallback clock at the hour the automation registry already declares, which fires only when no pass was recorded that local day, so an installed host task satisfies it and the fallback never runs. It honours the pause flag, never starts on top of a live pass, and runs at the default scope, so the 5.1.0 peer exclusion holds and the provider spend is the one already approved.
cohorts.writeCache returns the ledger outcome instead of discarding it inside an empty catch. A working producer and one failing on every cohort were indistinguishable on every path; now each cohort's row carries what the ledger did and the pass summary adds them up. A pass that resolved cohorts with real memberships and observed nothing cannot record success. A cohort that resolved to an EMPTY membership is counted separately and named, because refusing to persist an empty list is correct behaviour and a warning that fires on it is a warning nobody reads.
- The enrichment batch counts archived vintages the same way and downgrades its own outcome when it built panels and archived none.
check_alerts is DECLARED in the process registry as UNSCHEDULED and reads permanently RED. Nothing schedules it — not the automation registry, not a daemon timer, not a host task — so the two alert types the 5.10.0 migration deliberately protected have never fired, and until now the registry did not carry the job at all, so its absence was invisible rather than red. The status law puts unscheduled ABOVE the run history: a job nothing schedules that was once run by hand would otherwise read green off that run. Whether it gets a clock or is retired remains the owner's decision; this makes the decision visible rather than making it.
- Every process row now states WHO OWNS ITS CLOCK — daemon, host, or none — and a host-owned row names the case this code cannot distinguish: a task never installed and a task failing leave identical evidence.
backfill_event_log is the door event-producers.backfill() never had. Dry-run by default, idempotent through the append-only dedupe, non-destructive, carrying both timestamps from each source record.
- The run-summary cap moved 300 → 400 and NAMES its truncation. A silent slice cut a shipped sentence mid-number at "…; 21", which reads as a complete thought and is not one.
strategy.js required ./screens, a module that does not exist, inside a try/catch — so every screen-scoped strategy was save-time valid and resolved to ZERO SYMBOLS behind a swallowed MODULE_NOT_FOUND, with a refusal naming a module rather than a screen. Two defects were stacked: fixing only the require would have left scr.get undefined on the screener, so the correct accessor is getScreen.
- **
pf.* resolved to a silent empty set in the alert layer.** cohorts.get() succeeds for a portfolio cohort whose definition is {portfolio_id} with no stored membership by design, so the lookup returned a non-null result with zero members: every alert filtered out, a subscription validated that could never match, and inScope() false for every symbol forever. It now derives live from the ledger, and a book that cannot be read REFUSES — a scope that cannot be resolved is not an empty scope.
- The repricing scan's run summary printed
scanned 2026-08-06 — [object Object] on every machine for four months; headline is a count object and the ledger concatenated it. A scan that found nothing now says so, which is a result.
- The halted-tick refusal read
…restarting.). the tick was HALTED…; the spliced clause is capitalised at the splice rather than in the TICK_GATE table, because those strings are read mid-sentence elsewhere.
5.17.2 — the log that could be made to lie
5.17.1 — the panel that never came back
5.17.0 — the backtest results surface
5.16.0 — the running engine, made visible
5.15.0 — the drop-top-names re-run
5.14.1 — the id a run reports is the id the store holds
5.14.0 — Wave 5: the backtester
5.13.0 — Wave 4: the strategy layer
5.12.0 — Wave 3: actions, and the paper book
5.11.0 — the producers, because the engine was inert
5.10.0 — the rule engine
5.9.0 — the condition language
5.8.0 — the clock starts, and the limits go up before the engine does
5.7.0 — disclosure became noise
5.6.0 — five dialogs asked the same question five ways
5.5.1 — the merged dialog, measured
5.5.0 — two dialogs for one question, and now one door
5.4.0 — the chart knew a vocabulary the screen did not
5.3.0 — the standard, named and asked
5.2.0 — the standard, retranslated
5.1.0 — a nightly pass planned five hundred refreshes for groups nobody asked for
5.0.1 — the delete button answers, and the row stays deleted (patch)
5.0.0 — a cohort served a list its own screen forbade, because a cache outlived its source (major)
4.99.1 — a two-thousand-character description met a nowrap cell and pushed the controls off the screen
4.99.0 — a model read our own data and found the screen admitting a bank on a ratio that means two different things
4.98.1 — a thinking model answered and we called it empty: every vendor parser now reads past the reasoning to the text
4.98.0 — the top bar gets its shape back: the top-down surfaces gather under macro-economic, support joins administration, and a vestigial tab retires
4.97.0 — one vendor at a time, chosen and introspected rather than assumed: every assessment now remembers which model answered
4.96.0 — the utility surfaces move under one roof: administration, a left rail built to grow
4.95.1 — the watcher answers while the night works: the daemon yields between chunks, and the tab says so instead of staring
4.95.0 — the night's work gets a face: every OTS batch, its light, its reason, and the controls beside them
4.94.0 — the cost moves to the night: panels built once, screens answer instantly, coverage stated — eleven deferred field families arrive
4.93.0 — custom screens stop being write-once: edit in place with history, examples stay shipped truth, duplicate to diverge
4.92.0 — the screen learns the full vocabulary: every computable field of the reference taxonomy, one derivation and a refusal each — and what cannot be computed yet is named, not hidden
4.91.0 — the assessment gains a shape: six canon axes, scored with their reasons, drawn with the numbers beside them — and a malformed score refuses the chart, never invents one
4.90.0 — the second opinion becomes evidence: the prompt is yours to shape, and every report keeps the exact prompt and the exact data that produced it, forever
4.89.0 — the terminal gains a second opinion: a configured model reads everything the server knows about a name and writes the value-discipline assessment
4.88.0 — the portfolio's cohort stops being a copy: membership derives live from the ledger, so synchronization is structural
4.87.0 — the ledger vocabulary reaches the hands: a real transaction dialog, corrections with an audit trail, and the corporate actions become visible
4.86.0 — the corporate-action vocabulary completes: every action is a lot transform that must reconcile or refuse
4.85.0 — the report learns what practice pairs with brinson: factor attribution, ex-ante risk and stress — and refuses where history is too short to know
4.84.0 — ledger completeness: the ledger learns income, fees and corporate actions; the benchmark stops flattering; the statement reconciles or refuses
4.83.0 — full cost basis accounting: the method stops being a silent default
4.82.0 — the report meets the factsheet standard
4.81.0 — the book becomes a subject: CFA performance and attribution, and a report you can hand across a desk
4.80.1 — the population lens found three engine defects; all fixed before any note was written
4.80.0 — batches run against dynamically derived cohorts (owner directive)
4.79.0 — the cohort predicate builder's field pickers become the modal (owner UX)
4.78.0 — four closures: the chart's vocabulary reaches the screen (owner directive)
- Container stage (free):
econ_<variable> for all 9 economy container
- Surface history (free):
momw_chg_30d_pct, gap_chg_30d_pp — the
- Band percentiles (toggle SURFACE_BAND_PCTL_ENABLED): the model tick
- Pro forma distribution freeze (toggle SURFACE_PF_DISTRIBUTION_ENABLED,
4.77.1 — the rival index stage retired same-day: in_index was already the mechanism
4.77.0 — model values become screener vocabulary, and the green rule becomes a screen (owner directive)
run_screen description now catalogs the whole surface stage (composite,
momw_gap_pct registered as an alias of gap_pct — one computation, two
model_spread_ln (population SD of ln per-model fair value; the spec's
- NEW
index stage (free, before the enrichment cap): sp500_member,
4.76.2 — chip tooltips: the abbreviation gets its full story (owner UX)
4.76.1 — group rows select like every other row (owner bug report)
4.76.0 — the heat-map lens picker becomes the modal (owner direction), Phase A
4.75.1 — the rename must not strand saved layouts (owner live-run report)
4.75.0 — derivation takes the headline (owner decision: "modeling by agreement rather than derivation" is not a way to price a security)
4.74.1 — the fair-value line renames: a ref must never collide with its own suffix grammar (owner live-run defect)
4.74.0 — the fair value gets its fan (owner: "there should be [a dispersion] if we were doing it correctly")
4.73.2 — the fan refuses what it cannot draw; the band rides its base (owner live-run defects)
4.73.1 — the staged commit actually fetches (owner defect report: "nothing was displayed")
4.73.0 — dialog semantics: OK commits, Cancel discards (owner direction)
4.72.0 — the third column: nav | list | config (owner UX principle)
4.71.0 — the nav filters, it does not scroll (owner correction)
4.70.0 — the Displayed set is a first-class category in both modals (owner direction)
4.69.0 — the Indicators library gets the modal shell (owner direction: consistency)
4.68.0 — the series picker becomes a modal (owner UX direction)
4.67.0 — the substrate lands (Phase 0) and the fan looks like a distribution
4.66.0 — the Model of Models page catches up with the vertical (owner review)
4.65.0 — the stale banner acts (owner UX direction)
4.64.0 — the final loop closes: the old approach clears away (owner direction)
4.63.0 — the cascade closes its four stated gaps (owner direction: "let's begin fixing all of these gaps")
4.62.0 — Phase 2b: the drawn scenario reaches company drivers (INDUSTRY-DEPENDENCY-SPEC §3)
4.61.0 — the industry rung charts exactly like the economy rung (owner direction)
4.60.0 — every economic indicator gets its honest history (owner defect report)
4.59.0 — the fan is a switch on the overlay row (owner UX direction)
- The fan switch. An overlay whose container carries per-period dispersion
- Fan geometry. Refs differing only by
:pNN / :scenario:* now SHARE
4.58.0 — forward series actually render forward (the orange-dot defect)
- Forward rendering was broken for EVERY forward series. The overlay
- The history splice.
economy:<var> now splices its MEASURED source
- The fan was already refusing correctly — the live container carries no
4.57.0 — the picker renders FROM the catalog (owner direction: centralize)
4.56.0 — the Monte Carlo visual grammar reaches the upper rungs
:pNN — the analytic fan. economy:<var>:pNN and
:scenario:<name> — worlds as lines. An economy scenario is a WORLD with
- Downstream unchanged and coming: the simulated company fan
4.55.0 — container paths chart as overlays, and the Macro/Data catalog is cleaned
- New chart refs:
economy:<variable>[:<SYM>] (the container's per-period
- The catalog is curated around the architecture: the nine economy container
4.54.0 — an elasticity is a SERIES (owner correction), and Monte Carlo at every layer is the design
- Every elasticity is now a per-period SERIES on the pro forma grid, with
- The grid edits the path: each row shows the current figure, a sparkline
- Monte Carlo at every layer is now the recorded design
4.53.0 — lineage in the grid, and the accumulated hand-down
4.52.0 — the dependency model becomes a spreadsheet, not an editorial
4.51.0 — expert-level sector models as dependencies (INDUSTRY-DEPENDENCY-SPEC Phase 2a)
- COMMON (out of the box): the cross-industry baseline — revenue ↔ real GDP
- TEMPLATES (out of the box, per FMP sector and industry): the analyst
- OWNER: override any elasticity, REMOVE one, or ADD dependencies the
4.50.0 — two levels, from the industry's own classification (owner direction)
- The taxonomy is DERIVED, never curated.
refreshTaxonomy pulls both key
- Both levels are containers. SECTOR (coarse) and INDUSTRY (fine — the
- UX: the Sectors tab gains a level toggle (Sectors | Industries), the key
4.49.0 — Sectors becomes a top-level tab (owner UX direction)
4.48.0 — priors beside fitted drivers (INDUSTRY-CONTAINER-SPEC Phase 1)
- 1a — the prior discloses.
proforma-input.js attaches, per name, every
- 1b — adoption, configurable (
INDUSTRY_PRIORS_ENABLED, default 0; the
4.47.0 — the middle rung becomes a container (INDUSTRY-CONTAINER-SPEC Phase 0)
4.46.0 — scenarios reach Monte Carlo (ECONOMY-MODEL-SPEC Phase 3): the economy is drawn first
- Scenarios in the container (
setScenario / deleteScenario /
- The simulator draws the scenario FIRST (
ECONOMY_SCENARIOS_ENABLED,
- **Driver conditionality is deliberately deferred, and stated on every
- UX: a Scenarios sub-view in the Economy tab — the set, probabilities,
4.45.0 — the terminal-growth ceiling reaches the perpetuity (ECONOMY-MODEL-SPEC Phase 2)
- The projection stays pure.
proforma-input.js (the impure builder)
- Configurable, never hardcoded.
ECONOMY_CEILING_ENABLED (default 0):
4.44.0 — Phase 1 complete: the discount-rate family inherits (1c + 1d)
- 1c — the risk-free centralises. With the toggle on, the economy's
- 1d — cost of debt gains its first market input. `kd = risk-free + BBB
4.43.0 — the adoption is a TOGGLE, not a hardcoded flip (ECONOMY-INHERITANCE-SPEC Phase 1b)
- UX — a new Inheritance view in the Economy panel: the ON/OFF toggle, and
- MCP —
set_economy_inheritance {enabled} (and the generic tuning tool);
- The flag
ECONOMY_INHERITANCE_ENABLED defaults to 0 and stays there in the
4.42.0 — the economy reaches a valuation, gated (ECONOMY-INHERITANCE-SPEC Phase 1a)
ECONOMY_INHERITANCE_ENABLED (tuning, default 0). At 0 the gated wire
server/economy-inheritance.js — the gated wire (forValuation, empty
- The subtle mechanic made visible: where the minimum-equity-premium **floor
4.41.0 — the economy panel edits a PATH, not a point
4.40.0 — operational state that survives a session boundary (CONTINUITY-SPEC Phase 0)
CLAUDE.md (aliased by AGENTS.md) — the one file every session reads:
- The release doctor (
server/release-doctor.js, tool release_doctor,
- The session-state snapshot (
.ots/session-state.json, gitignored) —
4.39.0 — the economy becomes reachable: a container the agent can fill and the terminal can show
4.38.0 — literals promoted, and a measurement of how far the UX fell behind
4.37.0 — two numbers for one quantity
4.36.0 — a median describing a company that no longer exists
4.35.0 — limited liability, and a view carrying no view
4.34.2 — the fourth time, and an accessor instead of a fifth patch
4.34.1 — a wrong argument wearing missing data's clothes
.history — quarterly levels. Differencing them sequentially measures
.yoy — year-over-year same-quarter change. Seasonality-free, and already
4.34.0 — reachable is not usable
4.33.0 — the panels were never plugged in
4.32.1 — three guards on the publisher
- G0 — no default paths. Both arguments are required. The root cause was not
- G1 — the clone must be at origin. A commit built on a stale tree reverts
- G2 — the version must increase. Republishing the same version means the
- G3 — the manifest must describe the artifact. Version,
download_url and
4.32.0 — the loop closes
- Scoring before the horizon. The most tempting corruption of a learning
- Thin history. Below twelve resolutions the factor is itself a guess: it
- An absurd factor. Errors more than four times the claimed uncertainty mean
- Directional bias is reported separately. Being consistently too bullish is
4.31.0 — expected shortfall, risk budgets, and one refusal
4.30.0 — Black-Litterman: a view with a confidence, not an assertion
- A view without a stated confidence is refused as an assertion — the whole
- The prior must come from market capitalisation. A prior built from our own
- A singular covariance is refused, never pseudo-inverted — weights from
- τ is disclosed, because the posterior is sensitive to it and a reader
4.29.0 — the market-implied distribution: the right-hand curve
4.28.0 — bottom-up beta: the first real consumer of the industry rung
- Nothing is silently substituted. Both betas are returned with the
- Fewer than four usable peers is a refusal. Below that there is no precision
- The subject is excluded from its own peer set — otherwise its own beta
- A beta outside [0.05, 4.0] is rejected and named: that is a broken
4.27.0 — the equity risk premium, resolved by professional practice
4.26.0 — the last rung: the joint distribution reaches the portfolio
- The covariance source travels with the result. Two optimisations over the
- A supplied covariance with no source is labelled an assumption.
- Per-path values are required — a covariance assembled from per-name
- A name without a usable price is refused, not defaulted.
- The price path is unchanged. This adds an option, not a default.
4.25.0 — the first joint distribution: a book is not its names
- From filings, never from prices. A price covariance answers *"have these
- Shocks, not levels. Correlating levels mostly recovers that two companies
- Amplitude removed before pooling — the mechanical form of "pool the
- It refuses more than it reports: below 3 names, 12 observations per name or
4.24.0 — the layered cascade: the pipeline actually connects
- World generation: a content hash over each domain's
last_updated, so a
- Refresh is idempotent on an unchanged world — otherwise "generic" is a
- A per-name read never writes the shared layer — otherwise the cascade
- An unmapped sector is refused, not mapped to everything. The information is
4.23.0 — discovery becomes a process, and three defects the live read exposed
- Bounds, with numbers. A record carries what was *ruled out*: the duration
- A falsifier before any evidence. A candidate without one is refused at
regime needs a falsifier that binds. "The market pays more for this kind
- Evidence is signed and attributed, and marked as DATA — a filing that
- Adjudication is owner-grade. The agent raises, gathers and argues; it does
- Dormant, not deleted. Refuted candidates kept. Deleting either leaves the
4.22.0 — the release gate tests the artifact it ships
4.21.0 — the inversion, and a hole in the gate that hid five suites
- Every solve is univariate and says so on the payload — "if this were the
- A driver that cannot reach the price at any value in a range far wider than
- Gross margin is unreachable by design — EBIT comes from the operating
- A driver with no measured dispersion is unscored, never scored as zero.
- Non-monotone drivers report every root and take the one nearest the record.
- The wall: nothing the inversion computes feeds back into the valuation. A
4.20.0 — two signs, one computed and one asserted
4.19.0 — the financing layer
4.18.0 — the fan
4.17.0 — a move with a cause
guidance does not touch the anchor. Management guidance is a claim about
si_scale_revision does not touch the drivers. A bigger bet is not
si_capital_revision moves both the initiative and the cost structure,
inflation_regime moves terminal growth and the discount rate together,
4.16.0 — two bugs that were telling the truth
4.15.0 — a distribution, not a number
4.14.0 — a line to the right of the present
fcff — its negative stretch *is* the investment phase, and where it
revenue_initiative plotted separately from total revenue, because a
breakeven_revenue against projected revenue — §17.9 makes breakeven
4.13.0 — the pro forma becomes a series
4.12.0 — attribute the cost structure; the model changes as the company invests
4.11.1 — a rate divided by the wrong period
4.11.0 — quarters, and a term structure instead of a number
4.10.1 — what a live reading found in eight hours
4.10.0 — an initiative stops being a coupon
- Starts after the anchor period → it cannot be in statements that predate
- Started on or before the anchor, with no realized record → **refused by
4.9.2 — a guard belongs where the value is derived
4.9.1 — a percentage of a vanishing residual, and three releases made visible
- the anchor in force, its filing date, how many days newer the
- provider defects detected on this name, kept separate from the standing
- the fitted drivers, ordered by how badly the fit describes the present
4.9.0 — the projection stops being seeded and starts being fitted
- a sustained level shift — acquisition, divestiture, business-model change
- coverage too holey to be the window it claims to be
- too few quarters to fit at all
4.8.1 — a signal that fires everywhere is not a signal
refused — what was DETECTED in *this* name's data: a zero standing in for
omitted_by_policy — the standing rules applied identically to every name,
4.8.0 — the anchor: TTM from filed quarterlies, computed beside the annual one
- a zero standing in for a missing value — detected against sibling periods,
- a line derived from a refused line — the EBITDA case, prevented
- a component reported inconsistently against its own aggregate
- a short, gapped or duplicated window — a shorter sum labelled TTM is a
4.7.3 — a row that claimed a measurement nobody made
4.7.2 — a comment that became a regex, and an absent parameter that became zero
4.7.1 — an increment cannot sit against the whole price
4.7.0 — the top-down half, Phase 1: what must be believed
4.6.0 — the census was slow because nothing cached the provider
4.5.4 — the census fix routed itself to the wrong router (patch)
4.5.3 — a deleted cohort killed the heat map; the census hard-coded its set and could hang (patch)
4.5.2 — toast() was called 23 times and defined nowhere (patch)
4.5.1 — the Create button read a field from a different panel (patch)
4.5.0 — the right-hand side of a predicate can be a field
4.4.0 — the system's own valuations become screenable
momw_value, gap_pct, surface_price, momw_models_valued,
bracketing_regime, frames_above, frames_below
- and every registered pricing model as
m_<id> — m_intrinsic_dcf_fcff,
4.3.2 — the pack list was still curated by hand, and SpaceX paid for it (patch)
4.3.1 — the liveness check cried wolf (patch)
4.3.0 — four ways the system knew something was wrong and could not say so
4.2.0 — valuation structure, Phase 1: both numbers side by side, and the belief dial finds its anchor
4.1.2 — three numbers that did not name their own basis (patch)
- where nothing was scored,
weight and weight_year_ago are null,
- where a frame was scored and genuinely earned nothing, 0 stays 0 —
- both flags ride only on the not-scored path, exactly as the 4.1.1 branch
- the card is now "Initiatives / share (add-on only)", with a line beneath
- the waterfall total carries the reconciliation inline, with the three
- where an earned composite exists (the CAT case) the panel now
- the regression that would have caught defect 1: CROSS-PANEL AGREEMENT,
- not-scored vs scored-zero, pinned in both directions: the SPCX shape
- the client contract for the not-scored rendering, the add-on-only card
- the 4.1.0 golden digests are unchanged and still pass — the operating
4.1.1 — the equal-weight headline is retired; the two panels are made to agree (patch)
- where add-on books value the name — the payload publishes the BASE +
- where no add-on values the name — no headline number at all. The
4.1.0 — the census gets a door, a compact contract, and a correctness fix (minor)
4.0.1 — the tracked-frame diagnostic inherits the price-anchor exclusion (patch)
4.0.0 — valuation structure, Phase 0: the classification becomes visible and not one number moves (major)
3.99.0 — the dead control, and the owner's own path to identity (minor)
- works from any enrichment status —
ok, ambiguous, not_found,
- never overwrites the automatic verdict: results are written to a separate
- always records the proposal in the append-only journal — including when
- always renders an outcome: the candidate list, or an explicit
3.98.0 — the unweighted benchmark, and three views that agree (minor)
weighted_fair_value stays null — an earned composite genuinely does not
unweighted_fair_value: <number|null>;
unweighted_vs_price_pct: <number|null> — the gap computed once,
- `unweighted_basis: { reason, observations_available, observations_required,
composite_status: 'earned' | 'unweighted_benchmark' | 'absent' — a compact
- Tests: new suite
unweighted-benchmark.test.js — the under-min_obs
3.97.0 — identity resolution as owner-grade judgment (minor)
3.96.2 — Wikipedia title variants (patch)
3.96.1 — Wikipedia rate-limit honesty + the roster on-demand gap (patch)
- Compliant User-Agent on every Wikipedia request:
- 429 honored honestly — a rate-limited answer stores the new honesty
- One graceful fallback on 429/403 — the action API
3.96.0 — Full executive reports
- PER-PERSON ENRICHMENT FETCH + CACHE (
server/executives.js), stored at
- Wikipedia — the REST summary endpoint
- Career across companies — structural cross-references from data
- News — the 3.95.0 candidate mentions, unchanged.
- AGENT-REPORT SECTION. The profile page renders research notes whose
- PAGE ASSEMBLY. The profile page renders in a fixed order: header chips
- Tests: new suite
exec-enrichment.test.js (wiki honesty states with
3.95.0 — Executive drill-down + executive-change alerting
- EXECUTIVE PROFILE DRILL-DOWN. Every Key Executives row in Company
- ROSTER SNAPSHOTS + STRUCTURAL CHANGE DETECTION (
server/executives.js).
- EXECUTIVE NEWS TAGGING (ingest, display-only). The intake gains
- New
/cm/executives op (token-guarded like every /cm op): roster + history
- New suite
executives.test.js (roster diff fixtures per change type, pay
3.94.0 — User-journey integration: the four analyst journeys as click paths
- JOURNEY 1 — thesis → screen → cohort → security. The thesis view gains
- JOURNEY 3 — cohorts → securities, and onto the surface. Cohort member
- JOURNEY 4 — THE MODELING-GAP LENS. (a) Security side: the Model of
- JOURNEY 2 — completeness check (3.92.0/3.93.0 built this one). The
- Nothing new auto-edits anything: every added hop is a read or an
- New suite
user-journeys.test.js (read-time report slice on fixtures —
3.93.0 — Unified calendar: everything the feed knows, on one time axis
- **THE UNIFIED OP (
/cm/unified_calendar, token-guarded like every /cm
- VISUAL MONTH GRID (terminal, Watchlists → Alerts → Calendar). A month
- LIST MODE STAYS. The existing dated list is now a toggle (**List |
- FILTER PARITY WITH THE FEED, BY CONSTRUCTION. The type-chip strip is
- New suite
unified-calendar.test.js (five-source aggregation with type
3.92.0 — Alerts feed redesign: the typed stream is the attention-focusing instrument
- TYPE-FIRST FEED (terminal, Watchlists → Alerts → Feed). The sub-section
- PRICE RULES DEMOTED TO A TYPE. The legacy create-alert row
- COHORT-FIRST SUBSCRIPTIONS (owner refinement, approved). The
- FEED ENDPOINT (
/cm/alerts_feed, token-guarded like every /cm op). The
- Tests. New suite
alerts-feed.test.js (stream shape incl. the
3.91.0 — Alert subscriptions: deliberate control over which alert types fire on which securities
- THE SUBSCRIPTION MODEL (
server/alert-subscriptions.js). Three-layer
- GENERATION HONORS SUBSCRIPTIONS (
server/scan.js). Every
- ALERTS TAB UX (
server/terminal.js). The Watchlists→Alerts view
- TOOLS.
get_alert_subscriptions {symbol?} — the resolved view: what
- TESTS. New suite
alert-subscriptions.test.js (resolution
3.90.0 — Judgment-edit propagation, loading states everywhere, private-name reference inputs
- **JUDGMENT-EDIT PROPAGATION (
server/propagation.js, the TSLA
- LOADING INDICATORS EVERYWHERE (
server/terminal.js). ONE small
- **PRIVATE-NAME REFERENCE INPUTS (
server/pricing/reference.js + tool
- New suites
propagation.test.js + reference-inputs.test.js (edit →
3.89.0 — Divergence bubble-up: new ledger activity surfaces centrally
- NEW-TODAY SEMANTICS IN THE SCAN (
server/scan.js). Every run
- CENTRAL SURFACING IN THE TERMINAL (
server/terminal.js). The
- NOTIFICATION VIA THE EXISTING ALERT MACHINERY (
server/alerts.js).
- 3.88.0 INGEST JUDGED-UNIVERSE FIX (
server/ingest.js). The intake
- New suite
divergence-bubbleup.test.js — new-today diff
3.88.0 — Ingestion breadth: the external stream enters the intake, injection-hardened
server/ingest.js — the intake classifier + queue. The daemon polls
- CLASSIFICATION IS STRUCTURAL, NEVER SEMANTIC (module-header design
- THE QUEUE: append-only
<statedir>/ingest/queue.jsonl — entries
- INJECTION HARDENING (test-enforced): a queue entry whose headline
- SURFACING: the scan report gains an
ingest section (last cycle,
- TUNING:
INGEST_UNIVERSE_MODE, INGEST_MAX_ITEMS_PER_CYCLE,
- New suite
ingest.test.js (structural classification fixtures with
3.87.0 — UX parity for the four thesis instruments
- DIVERGENCE LEDGER RAIL VIEW (
id: divergence, label "Divergence
- RERATING VERDICT IN THE MODEL-OF-MODELS VIEW: a compact block runs
- EVENT-IMPACT PANEL IN THE SI VIEW ("Assess event impact"): kind
- BELIEF DIAL, both halves shipped (chart overlay was deferred from
- Tests: new suite
cm-thesis-ops.test.js (the four ops over real
3.86.0 — the event-impact loop + the belief-dial series (thesis gaps 5+6)
- EVENT-IMPACT ROUTING (
server/event-impact.js, tool
- SCAN COUPLING — attributed innovation (task #43's core): when the
- THE BELIEF-DIAL SERIES (
server/pricing/belief-dial.js, tool
- Shared lattice helpers: both features perturb the same book in
- Report/brief surfaces: the scan report gains
belief_dial
- New suites
event-impact.test.js and belief-dial.test.js (131
3.85.0 — the frames wave (thesis gap 4): mid-cycle + regime-median frames, holding-vehicle refusals, data-defect classes
- THE MID-CYCLE EARNINGS FRAME (
relative.pe_midcycle, band metric
- THE REGIME-MEDIAN (EROSION) FRAME (
relative.ps_regime, band metric
- INAPPLICABLE-FRAME REFUSAL: HOLDING VEHICLES (review P1.3, the MSTR
- DATA-DEFECT CLASSES IN THE SCAN (the SNDK/LQDA/SONY smells) —
- Five new tuning constants, each with a written basis:
3.84.0 — the rerating hypothesis test (thesis gap 2)
- THE TEST (
server/pricing/rerating.js, `testRerating(symbol,
- VERDICT SEMANTICS:
rerating_in_progress requires BOTH — the residual
- SCAN INTEGRATION: every
top_frame_rotation event now RUNS the test
- TOOL:
test_rerating {symbol, window?} — the on-demand form of the
- New suite
rerating.test.js (128 suites + UI check): static-frames price migration →
3.83.0 — the divergence ledger + three disclosure debts (thesis gaps 1 + 3)
- THE DIVERGENCE LEDGER (
server/divergence.js): append-only JSONL
- Writers (this wave): the daily repricing scan writes
- Readers: new tool
get_divergence_ledger {symbol?, limit?, since?}
- HYSTERESIS DISCLOSED AT POINT OF READ (alignment review P1.5): the
- CASE-STUDIES.md §TSLA: the Oct-2024 → Apr-2025 episode written up
- ARCHITECTURE-NOTES.md (alignment review P1.6): the two residual
- New suite
divergence-ledger.test.js (8 tests): both scan writer
3.82.0 — the hardening wave (alignment review P0.1 + P0.2)
- **P0.1 — price-anchored valuations no longer contaminate the
- P0.2 — the MoMW weight register archives, never truncates. The
- New suite
hardening-382.test.js (10 tests): the anchored run
3.81.0 — the Repricing scan view (UX parity for the daily scan)
- The pane computes nothing: every figure, ranking and proposal is
- Sections in report order: headline counts with deltas vs the
- Report-date selector across
reports_recorded — prev/next rail
- "Run scan now" POSTs
/cm/repricing_scan and re-renders; a
- First-run teaching panel (mirroring the heat map's): explains the
- Agent parity: `control_terminal open_market {view: 'scan',
- Contract suite extended: Market-rail label list (Heat map, Repricing
3.80.0 — the daily repricing scan
- Three detection families (
server/scan.js): (1) SI-NEED —
- All thresholds are tuning constants with written bases:
- Daemon-resident and data-triggered: the daemon runs the scan once
- Reports are dated, append-only history under the state dir
- Tools:
run_repricing_scan {date?} (on demand, routed to the
- New suite
repricing-scan.test.js pins the whole contract on a
3.79.0 — the live now-point, and the as-believed-now replay
- The live now-point: every
pricing:* overlay series now appends
- As-believed-now replay (
:as_believed_now ref suffix): the
- New suite
overlay-now-point.test.js pins all of it: the final point
3.78.0 — the normalized-earnings frame, and two hard-codings retired
- relative.pe_normalized: prices the AFTER-TAX OPERATING line at the
- The divergence gate: the raw P/E band refuses when TTM net income
- Band-metric registry (hard-coding #1 retired): metrics were a regex
- The tuning registry (hard-coding #2 retired): every judgment
- E7 registered in MOMW-METHODOLOGY-EXPERIMENTS: does P/S leadership
3.77.0 — surface-native filters: the query that makes observations actionable
3.76.1 — the cohort slice: reviewing the middle of the distribution
3.76.0 — marketplace versioning: immutable artifacts, latest pointer, any version by name
3.75.0 — Marketplace UX parity: browse, install, and the ledger in the terminal
3.74.0 — the marketplace is the system (and the only channel)
browse_marketplace: list and search the official catalog from
import_image {id}: install by marketplace id; the catalog's
- THE CHANNEL LOCK: a local archive imports only if THIS instance
- Docs: new in-app guide topic "Images, packs, and the marketplace"
3.73.0 — the image layer: the digital twin becomes shareable (IMAGE-SPEC Wave 1)
- Pack taxonomy (IMAGE-SPEC §1): reference (surface universe/models/
export_image / import_image: tar.gz .otsimage archives with a
- Provenance (§2): every import appends the append-only provenance
- Marketplace channel: the Pages repo gains
marketplace/ — first
3.72.0 — the SI frame learns from its first real book (owner direction)
- The ramp-input refusal: a logistic
midpoint_yr beyond the lattice
- The circularity cap: a name volatile BECAUSE optionality is priced
- Abandonment flexibility: a staged program is not a static commitment.
3.71.1 — clean-pass completion signal counted staleness the wrong way
3.71.0 — iteration three: scale identity, fundamental regime, and the clean pass
- The scale-identity gate (GNTX/NXTT class, 5× tolerance): statement
- The fundamental-regime gate (QXO class, same 25× threshold as the
- The clean pass:
surface_fill {action:"start", force:true} — the
3.70.0 — iteration two of the tail audit: regime, staleness, hygiene, evidence
- The regime gate (APVO class, threshold 25×): after the rebase, the
- The staleness gate (SUNE class, threshold 18 months): a shell whose
- Universe hygiene (EAI class): bond, notes-due, debenture, preferred-
- The evidence floor (3 models & 12 grid points): extremes are
3.69.0 — the per-share basis rebase: the green tail was unit errors, not opportunity
- Rebase everything to (price currency, today's listed shares):
- Honest edges: statements in a currency ≠ price currency with no FX
- The recurrence gate (WW class): the P/E band refuses when |TTM net
- The audit loop tool:
run_surface_batch {symbols: [...]} — forced
- Known remainder (next iteration): asset-model *historical* series and
3.68.0 — reconstructed history: 30 days of heat map, honestly labeled
surface_fill {action: "backfill", days} — a runner mode (same
- Static vintage, stated everywhere: every reconstructed day file
- Live records are sacrosanct: a live-recorded day is never
- Tests: exact arithmetic on reconstructed cells, the caveat traveling in
3.67.0 — heat-map lenses: every model, every unit, any recorded day
- Per-model freezing: the model tick now stores every model's per-share
- The lens selector on the Market tab: Model of Models (default), plain
- Δ% / Δ$ unit toggle: percentage gaps clamp at ±40% (valuation) or
- Calendar date selector alongside the ‹ › scrubber: pick any date; it
- API/tools:
getSurface and get_surface gain model and unit
- Tests: frozen-value arithmetic in both units, zero-weight lens
3.66.0 — the Market tab: the heat map arrives in the terminal
- The heat map — a sector-grouped squarified treemap of every covered
- The scrubber: step back one RECORDED trading day at a time (getSurface
- Honesty in the pixels: coverage counts (valued / uncovered, absences
- First-run instructions (owner direction): a new install with no
- Agent channel:
open_market {view?, date?} control op; terminal state
3.65.0 — surface_fill: the overnight job becomes a tool call
surface.fillStart/fillStop/fillStatus — an in-process async loop
- Honest lifecycle: one loop per machine — a start against a live
/cm/surface_fill (GET status; POST start/stop) — served by the
surface_fill tool — reaches the daemon over the loopback transport
- Registry heads-up,
/ots:automate step 6, and run_surface_batch
3.64.0 — /ots:automate: one command installs the schedules
- The automation registry (
server/automations.json, exposed via the
/ots:automate — the setup command. It reads the registry (never a
- Registry discipline, tested: automations.test.js pins the contract —
- Docs: README gains an Automation section and the /ots:automate row;
3.63.0 — the Opportunity Surface batch: every NYSE + NASDAQ common stock
- The universe (
surface_refresh_universe): every actively-trading
- Two cadences, deliberately. Model values move on filings — quarterly
- The scrubber's substrate: every recorded day is replayable
- Bulk fill:
node scripts/surface-batch.js --universe, then --all
- New tools: surface_refresh_universe, run_surface_batch, get_surface,
3.62.0 — the Pricing panel headlines the earned number
- The Pricing Models headline is now the precision-weighted value — the
- Equal weight is demoted to what it is: "Equal-weight benchmark",
- get_pricing_composite is relabeled as the equal-weight benchmark tool,
3.61.1 — pre-record replay uses the current book
3.61.0 — precision weighting: the Model of Models rewards closeness
- The weight is now precision: w ∝ 1 / max(MSE, (2%)²) where MSE is the
- Grading, not sign-gating. Under correlation weighting, an anti-tracking
- ρ survives as a descriptive column, joined by tracking error (RMS
- The register stays honest across the change: recorded weight vectors
- Governance told straight: this is the experiment plan's E4 adopted by
3.60.1 — the Model of Models blends the initiative book
3.60.0 — Strategic Initiatives: waterfall, tornado, probability cone (Wave 5)
- The valuation waterfall. The Strategic Initiatives view now draws
- Tornado sensitivity. Per initiative (🌪 on the card): one-at-a-time
- The probability cone. Three new chart overlays — Strategic Initiatives
3.59.0 — Strategic Initiatives: maturation and the closed scoring loop (Wave 4)
- The maturation fade. As an initiative's revenue starts appearing in
- Staleness. An active or scaling program with no journal entry in six
- Calibration — the loop almost nobody closes.
get_initiative_calibration
- Gates (Brier): mean (stated − outcome)² over every resolved gate —
- Ramps (MAPE): each dated realized observation vs the revenue the ramp
- Scores are DESCRIPTIVE, never corrective: they inform your revisions;
- Editor. Realized run-rate / as-of / fade-override fields per card, fade
3.58.0 — Strategic Initiatives: the event journal and immutable snapshots (Wave 3)
- The journal. Every revision — an edit, a gate resolution, an event —
- Immutable snapshots. Each revision freezes the entire book (initiatives
- Overlays are now true as-believed-then replays. The chart series values
- record_initiative_event — the manual capture path. An event may carry
- Editor. The Strategic Initiatives view renders the journal timeline —
3.57.0 — Strategic Initiatives: the risk register (Wave 2)
- Contingent capital. An outlay tied to a gate (
contingent_on) spends
- Shared risks. Declared ONCE per book, referenced by initiatives. A
- Gate resolutions — immutable, with the belief frozen. Recording a
- Editor. The Strategic Initiatives view gains the shared-risk register
3.56.0 — Strategic Initiatives: capital programs on dated cash-flow lattices (Wave 1)
- A dated cash-flow lattice per initiative. Explicit start date, horizon,
- Ramp profiles. Revenue (and optionally margin separately) reach scale on
- A staged capital schedule with funding sources. Outlays land on their
- rNPV gates. Discrete risks as milestone probabilities with expected
- The build-up method for discount rates. r = unified WACC (measured) +
- Migration, not amnesia. Existing venture books convert automatically on
- Renamed everywhere. The rail view is Strategic Initiatives (🎯) with
- Still an add-on, structurally: never blended into the composite or the
3.55.0 — Wave B: the review's structural batch
- Cross-process writes serialize (P1.1). The MCP server and the terminal
- The /cm routes have their own home (P1.2). serve.js had grown into a
- The pricing plane is exercised over real HTTP (P2.1).
cm-routes.test.js
- CI now guards the artifact itself (P2.2). Three new gates: an INVERSE
- Publishing is a script, not a ritual (P2.3).
./release.sh publish
3.54.0 — Wave A: the review's correctness batch, and a properly named signing key
- One discount rate for the basket (P0.5). DCF, EPV, EVA, Monte Carlo and
- Atomic state-directory migration (P0.3). The rebrand migration copied
- The client cache expires (P0.6). Terminal panel data was cached forever
- Duplicate
pct() deleted (P0.7). Two definitions of the percent
readBody settles on every terminal path (P0.8). The old body reader
- Signing told straight, and a properly named key (P0.9). RELEASING.md
- The model-of-models computes once (P1.3). One terminal render triggered
- Pricing stores route through store.js (P1.4). The venture book, judgment
- Silence diagnosed (P1.5). Daemon boot failures now leave a breadcrumb
- One definition of "filed by date" (P1.6).
reconstruct.inputsAt
- Add-ons never blend (P3.3).
venture_book and real_option are marked
- Venture discount rates bounded (P3.4).
set_venture_book rejects
3.53.4 — the terms name the product, and history is history
- The disclaimer's defined term now names OPEN TRADING SURFACE. The rename
- Changelog history restored verbatim. The rename sweep had mechanically
- The daily state backup no longer contains secrets. config.json (API
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
- Editorial note (2026-07-23): entries below this one predate the rename and
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 Orrery" 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 Orrery, 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 Orrery 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 ~/.orrery/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 ~/.orrery/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 orrery-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 (
~/.orrery/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 Orrery
- 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 Orrery logo/iconography assets under docs/assets/.
- Renamed the entire project from Surface-StockChart to Orrery — an orrery 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 → orrery), the MCP server name (tools are now mcp__orrery__*), the on-disk store directory (~/.surface-stockchart → ~/.orrery), environment-variable prefix (STOCKCHART_* → ORRERY_*), and the session token header (x-scp-token → x-orrery-token). Docs, release pipeline, and issue templates all point at the pauljbernard org.
- Hardened
.gitignore so secrets (config.json, *.env, *.key), all ~/.orrery 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.