Releases: WilliamSmithEdward/Epiphany
Release list
m8.9: top-level membership, de-duplicated rollups, and a hardening sweep
The dimension-editor and hardening release: editable MDX queries; Copy/Move/Pin and
explicit top-level membership in the structural dimension editor; de-duplicated
consolidation rollups and a lowercase, display-name-decoupled on-disk layout; the
suppress-zeros split and a consolidated view-save flow; plus a wide adversarial
hardening pass (an app-wide error boundary, WebSocket-reload data-loss guards,
accessibility, and performance) and the security fixes below.
Added
- Editable MDX queries in the cube view. The "MDX for this view" dialog is now
an editable textarea with a Run button that executes a full
SELECT … ON COLUMNS / ON ROWS FROM [cube] [WHERE ( … )]query against the cube
and renders the resulting cellset, surfacing parse/validation errors inline. The
epiphany-mdxparser gained aSELECT-query layer (parse_query) above its set
sublanguage, and a newPOST /api/v1/cubes/{cube}/mdxendpoint executes it through
the sameReadauthorization and element-masking path as the ad-hoc cellset query. - Copy to / Move to for dimension members. The member action menu now offers
Copy to (add the member to another consolidation as an alternate rollup, keeping
its existing parents; a consolidation it already belongs to is greyed as "already
contains") and Move to (reparent it under exactly one consolidation or the top
level, with a confirm when that would drop its other rollups). Self and descendant
targets are excluded so a cycle can't be formed. - Separate zero-suppression for rows and columns. The single "Suppress zeros"
option is now two independent toggles, Suppress zero rows and Suppress zero
columns, on the cube viewer's Save dialog and the view editor. Views saved before
this map a legacysuppress_zeros: trueto both flags. - Explicit top-level membership (ADR-0038). A member can be pinned to the top level
so it shows as a display root while still rolling up under its consolidations - it
appears both at the top and under each parent (the latter carry a "(pinned)" badge).
Pin via the row menu's Copy to -> Top level or a discoverable drag-to-top-level
drop zone; copying a top-level member into a consolidation auto-pins it so it stays at
the top; Move to -> Top level is the exclusive variant (reparent to a standalone
root). A display-only marker: no edge, weight, or value changes. Full stack - a
per-element flag (core), a name-addressed WAL record (persist), apinned_to_topDTO
field + pin/unpin edit ops (api), and the editor + pivot rendering. - Expand all / Collapse all in the dimension editor toolbar, matching the cube
viewer and the model tree.
Changed
- Views are created from the cube viewer. The duplicative "Add view..." authoring
path was removed; arrange a layout in the cube viewer and use Save view to create
one. The Views workspace now opens and edits existing views in place. - Consolidation rollups de-duplicate (ADR-0039). A member reachable via more than
one path to a consolidation - e.g. a direct child of a total that is also a child of
one of that total's sub-consolidations - is now counted ONCE in that total, at its
most-direct (fewest-hops) path weight, instead of summed per path. It flows through
every consolidated read, spreading, and element-security masking, and makes the
multi-parent / pinning structures safe from accidental double-counting. - On-disk cube folders are lowercase, filesystem-safe slugs decoupled from the
display name (ADR-0037). A cube "Sales" persists undercubes/sales/while still
presenting as "Sales" (the engine reads the name from the snapshot, not the folder).
Existing folders migrate by rename on first start (resilient: never deletes; a real
slug collision is left intact and logged), and cube names are now unique
case-insensitively. For cross-platform (Linux, case-sensitive filesystem) consistency.
Fixed
- Cube view: pivot rows/cells no longer duplicate or strand cells under an
alternate rollup (a member reachable from two consolidation parents). Rows, cells,
and header spans are keyed by each member's drill path instead of its bare name,
and saved-subset member lists are de-duplicated. - Cube view: the "Who can see it" dropdown in the Save-view dialog now paints
above the dialog instead of behind it (the dropdown layer ranked below the modal
overlay in the z-index scale). - Dimension editor: right-clicking a member opens the action menu at the cursor
(it previously anchored to the row's ⋯ button). - Cube tab strip: removed the spurious vertical scroll arrows.
- Durability: a checkpoint can no longer lose data on a crash. The snapshot is
fsynced (file and directory) before the write-ahead log is cleared, and a
reindexing dimension edit (reorder/delete/insert) folds outstanding writes and
empties the WAL before rewriting the snapshot, so recovery can never replay a
stale element index onto the new layout. A WAL truncated below its header by a
crash is now treated as fresh rather than failing the open. - A failed model-definition commit no longer leaks into the next one. If
persisting a definition change fails midway, the in-memory model is rolled back
to the last published version, so the partial change cannot ride out on a later
successful commit. - What-if sandbox overrides now follow structural dimension edits. Reordering,
inserting, or deleting a member remaps each sandbox override to its member's new
index (and drops overrides on a deleted member) instead of leaving them on the
wrong member or panicking the checkpoint. - Flows: the TypeScript stripper no longer drops
switchcase bodies such as
case 0: break;(a case/default colon was mistaken for a type annotation), and
no longer corruptsinterfaceused as an ordinary identifier. - Dimension aliases: reassigning an alias no longer leaves the old one
resolvable (which also falsely blocked another member from claiming the freed
name). - Cube view: an alternate-rollup member can now be expanded independently under
each parent (expansion is keyed by drill path, not member name); a slow cell
read can no longer overwrite a newer one; and a set that fails to resolve surfaces
the error instead of silently blanking the axis. - Dimension editor: rapid keyboard and context-menu structural edits are gated
so they cannot race a prior in-flight edit, and member-table range selection
survives a re-sort or filter. - A non-admin schedule author can open the jobs workspace again (the admin-only
run list no longer fails the whole load), and opening a saved view no longer
drops its layout when the cube loads slowly. - Windows service: a failed startup now reports a non-zero exit to the SCM
instead of a clean stop. - Dimension editor: drag-and-drop is a reliable, parent-aware move. A member can
now be dragged into a consolidation, and every drag emits the minimal, duplicate-free
edits: dropping onto a consolidation moves the dragged occurrence there while keeping
the member's other rollups, and the additive add runs before the detach so an
interrupted multi-step move can never orphan a member. Editor and pivot now agree on
child order, and the drop-inside target band is larger and recomputed at release. - Login: you stay signed in after reloading the page. The session is restored from
the cookie on load (it was previously held only in memory), including the forced
password-change screen, and the duplicate password-manager autofill prompt is gone. - Login: Enter signs you in, and autofilled credentials work. The form submits on
Enter even when a password manager swallows the browser's implicit submission, and the
submit reads the live field values, so a manager that fills the fields without firing
the usual events no longer sends blank credentials. - The what-if sandbox bar no longer shows in the structural dimension editor
(what-if overlays cell values; editing members and edges has no cells to override). - Dimension editor: a row's label click expands but never collapses (only the caret
collapses), and dragging a multi-parent member to the top level reports honestly
("Removed X from Y" when it still rolls up elsewhere) rather than a misleading "moved
to the top level". - A render error in one pane no longer blanks the whole app. A new error boundary
shows a recoverable fallback for the affected pane and leaves the tab strip and tree
usable; switching or closing the tab clears it. - No more silent data loss from background reloads. Unsaved rule edits survive a
WebSocket reload caused by activity in another tab; editing a flow or schedule that
was deleted elsewhere now disables Save (so it is not silently re-created) and says so;
entering the Administration view honors the unsaved-edit guard; and deleting an open
object no longer pops a nonsensical post-delete discard prompt. - Object tree stays correct under concurrent loads and search. A slower, stale
response can no longer overwrite freshly loaded children, and a delete or rename during
a search no longer leaves a stale row in the results. - Dimension editor robustness and accessibility. Adding a member as a child is now
all-or-nothing (a declined conversion or a failed step no longer strands an orphan with
the form trapped on a duplicate-name error), duplicate names are caught before the
round-trip, an empty dimension shows guidance, successful edits and keyboard moves are
announced to screen readers, and a pathological cyclic edge set can no longer overflow
the tree builder. - Cube viewer: stale numbers are not shown during a refresh. While re-querying after
a layout or filter change the grid dims and announces "Refreshing", so a previous
slice's values are never read as the current ones, and an...
m8.8: SQL sources, global automation, and dimension editing
The modeling release. Native SQL sources, global multi-cube automation, and full dimension structural editing, on top of the connector and spreading work that landed after m8.7.
Native SQL data sources (ADR-0034)
A flow can ingest directly from PostgreSQL or MySQL and MariaDB, pure-Rust over rustls/ring (no native-tls, openssl, or aws-lc-rs). Each driver is off by default and fenced at runtime by an enable flag, a host allowlist, a secret referenced by name, and a fixed admin query; ssl_mode supports verify-full, require, and disable. SQL Server is deferred (its current Rust driver pins a TLS stack that fails the supply-chain gate).
Global, multi-cube, code-first automation (ADR-0035)
Flows, schedules, and connections moved out of per-cube models into one server-global automation model. A single flow can read and write several cubes and grow several dimensions in one run, owned by none of them. Data sources are UI-driven (a global connection or a flow-scoped local one), while outputs stay code-first. Scheduled runs execute as the flow's owner, fail-closed. Existing per-cube flows, jobs, connections, and tests migrate into the global store on first boot.
Dimension structural editing (ADR-0036)
A dimension is now fully editable: reorder, reparent, convert kind, insert, delete, add a member to a consolidation, and remove a member from one consolidation, with every index-changing edit remapping stored cells transactionally (and fanning the same remap out to every referencing cube for a shared dimension). A standalone, hierarchy-only, table-driven, drag-and-drop dimension editor with full keyboard parity (WCAG 2.2 SC 2.5.7): every drag gesture has a row-menu equivalent. Delete is intent-aware: remove a member from selected consolidations (kept, with its data) or delete it from the dimension entirely (behind an explicit data-loss confirm).
Also new since m8.7
- One global dimension namespace with promote-to-reuse and attributes carried to referencing cubes (ADR-0031), and a scalable virtualized member table with inline attribute editing, per-column filters, and relationship set operators (ADR-0032).
- An object-centric explorer tree (consolidation hierarchy plus global object search), multiple dimensions per pivot axis, an MDX previewer, a tabbed object workspace, and saved Views and dimension Sets.
- HTTP fetch connector and an owner-only secret store (ADR-0030), and data spreading by equal, proportional, repeat, or clear (ADR-0029).
- Fail-closed element security on global dimension reads (ADR-0033).
See CHANGELOG.md for the full list. Prebuilt single-binary downloads (Linux x86_64/aarch64, Windows x86_64, macOS aarch64) are attached once the release build completes.
m8.7: performance (view cache + parallel aggregation)
The performance release, and the first tag to gather the post-roadmap programs that landed after m8.6.
Performance (ADR-0028)
- Deterministic parallel aggregation. Large cold view reads now aggregate across every core (scoped-thread workers, one per disjoint row band, above a cell-count threshold so small reads stay serial). Results are provably bit-identical to the serial path regardless of worker count or scheduling. Measured 4.5x to 6.9x faster on large consolidated views (a 40k-cell all-consolidated crossjoin drops from ~454 ms to ~72 ms on 14 cores). No new dependency.
- Persistent view cache. Repeat reads of an unchanged view are served from a bounded, version-keyed cache (cached-read budget p99 under ~100 ms). The key is lossless over the cube version, view shape, active sandbox, and the caller's exact element-deny set, so it self-invalidates on any write and never returns a stale or cross-user result. Configurable via
EPIPHANY_VIEW_CACHE_ENTRIES(default 256, 0 disables).
Also new since m8.6
- Web UI overhaul (ADR-0020), model editing from the UI and REST (ADR-0021), and an Excel add-in (ADR-0022).
- Shared, independent dimensions (ADR-0024); connector preview and admin dashboards (ADR-0027); in-house web syntax highlighting (ADR-0026); onboarding polish; native Windows service hosting.
- Modular per-object-kind permissions (ADR-0023), fail-closed by default, superseding the earlier object and global cube grant models.
- Tier-2/3 security hardening (ADR-0017 family + ADR-0025): login-timing dummy-hash, parser recursion-depth guards, password-strength policy, session idle-timeout with revoke-on-change, ingestion row caps, validated connector working directory, and a documented at-rest-encryption posture.
See CHANGELOG.md for the full history. Prebuilt single-binary downloads (Linux x86_64/aarch64, Windows x86_64, macOS aarch64) are attached once the release build completes.
m8.6: Optional TLS / HTTPS
Adds optional HTTPS (ADR-0019). It is off by default (the zero-config loopback-HTTP experience is unchanged) and designed to be trivial to turn on.
Enabling HTTPS
- One variable (self-signed):
EPIPHANY_TLS=ongenerates a self-signed certificate into{data_dir}/server/tls/on first run and serves HTTPS. No certificate to obtain; browsers show the usual self-signed warning, which is expected for local and internal use. - Your own certificate: set
EPIPHANY_TLS_CERTandEPIPHANY_TLS_KEYto PEM file paths (takes precedence over self-signed).
TLS serves on the same EPIPHANY_BIND address; the startup log shows https://. Graceful shutdown is preserved.
Implementation
Pure-Rust (rustls with the ring provider, rcgen for self-signed generation) behind a tls cargo feature, so the single static binary holds and the default core build stays lean. These release binaries are built with TLS included; a from-source build adds --features tls (and needs the platform C toolchain, since the crypto is compiled). The HSTS header added in m8.5 becomes effective once TLS is on.
Backward compatible: with no TLS configuration the server serves HTTP exactly as before. Verified end-to-end (HTTPS request to a self-signed instance), with the TLS feature linted on every platform in CI.
m8.5: HTTP-surface hardening
Adds routine HTTP-surface defenses from the security audit (ADR-0018), dependency-free and applied to every route. Backward compatible.
What's new
- Security response headers on every response:
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: no-referrer,Strict-Transport-Security(honored once TLS is fronted, harmless over HTTP), and a same-origin Content-Security-Policy. The CSP fits the bundled single-page UI (it loads its own script bundle and talks only to this origin, including the WebSocket) and is a constant operators can relax if they front the app differently. - Explicit request body-size limit (8 MiB): the per-request body bound is now intentional and documented (axum defaulted to 2 MiB), generous enough for batch writes and flow imports; an over-limit request is rejected with 413 before the handler runs.
A per-request timeout is intentionally deferred: a global timeout would cut off legitimately long synchronous flow runs and needs route-scoping, so it is a later slice rather than bundled with these zero-risk changes. TLS remains its own decision (it needs a dependency or a fronting proxy); the HSTS header is correct in advance of it.
Covered by tests/http_hardening.rs. Remaining hardening-bundle items (session idle-timeout and revocation, password policy, login dummy-hash, parser and input-size bounds) are follow-on slices.
m8.4: Complete automatic feeder inference
Makes the automatic feeder inference (ADR-0005) complete for the rule patterns the original cut missed, from a feeder-focused adversarial review. No computed value is affected: cell reads always use the dense, always-correct consolidation, so feeders are an optimization and a diagnostic, never the read path.
What's improved
- Chained rules are fed: a rule that reads another rule's derived output is now fed (inference is a fixpoint over "potentially non-zero" leaves, seeded by stored cells).
- Consolidated inputs feed via their contributing leaves.
- Multi-element target areas with a pinned input feed every target leaf (previously rejected as un-analyzable).
- Base contributions (a non-zero constant or attribute term, including inside a conditional branch) feed the whole target area, since such a rule is non-zero everywhere regardless of input population. Closes the constant/conditional under-feed.
- Validation false-negative fixed: a rule is a pure consolidation override only when its area selects no leaf coordinates, so a mixed-target rule (e.g.
{descendants of Total}) keeps its leaf targets validated -- an under-feed there is no longer silently skipped.
Unchanged by design: cross-cube-only rules remain reported as opaque (non-localizable) rather than guessed at, and the inference stays a sound over-approximation (it never under-feeds an analyzable rule; at worst it over-feeds, which is a warning). One consolidated input's expansion is bounded so inference cost stays bounded on pathological hierarchies.
Quality
- New unit tests for chained rules, consolidated inputs, multi-element targets, the base-potent constant/conditional cases, and the mixed-target validation.
- Hardened across two adversarial review rounds (the second re-reviewing the rewrite itself for soundness, termination, and regressions). Full workspace green, clippy + fmt clean.
m8.3: Authentication and credential hardening
Closes the highest-value authentication and credential gaps from the security audit (ADR-0017), with no new heavy dependencies. Fully backward compatible.
What's new
- Login lockout. After 5 consecutive failed logins (configurable) a username is locked for a cooldown (default 15 minutes), returning 429. The check runs before password verification, is recorded and audited, and resets on a successful login. It is clock-injected and deterministic. Tune with
EPIPHANY_LOGIN_MAX_FAILURESandEPIPHANY_LOGIN_LOCKOUT_SECS(0disables). - Forced password change is enforced. A user with a pending change (a first-run admin, or after an admin reset) is blocked from every data route (403) until they change their password; only change-password, logout, and identity are reachable. The gate lifts immediately on change, with no re-login.
- Owner-only secret files.
security.model(password hashes), the audit log, the run ledger, and the first-run admin-password file are created0600from the start on Unix, so they are never briefly world-readable. On other platforms the data directory's inherited ACL governs (protect your data directory). - No secret on stdout. The generated first-run admin password is written to an owner-only file and only its path is logged; read it once and delete it.
Quality
- New
LoginGuardunit tests, a config-knob test, and an end-to-end acceptance suite (lockout trigger and cooldown release, success-resets-counter, must-change gate then lift). - Hardened after an adversarial review: every secret file is created with restrictive permissions from the start (closing the write-then-chmod window), and the WebSocket route was confirmed to go through the same auth gate. Two flagged items were verified as a false positive (status-code enumeration: a non-existent username also locks, so 429 does not reveal account existence) and benign (a success-clears-lock race only triggers on a correct password).
Compatibility: with the defaults, existing deployments behave the same except that repeated failed logins now lock out and a first-run admin must change the generated password before using data.
m8.2: Global cube grants and explicit deny
Extends the cube authorization model (ADR-0016) with a global (all-cubes) grant scope and explicit deny, so broad baseline access with per-cube exceptions is now expressible for users and groups, on top of the closed-by-default posture from m8.1.
What's new
- Global cube grants. Grant a user or group a baseline level (read/write/admin) across every cube in one grant, instead of one grant per cube. A newly created cube is covered automatically.
- Explicit deny. Deny a user or group on a specific cube, or across all cubes, as a carve-out from a broader allow.
- Predictable precedence. Most-specific tier wins (a per-cube grant overrides a global one), an explicit deny wins within its tier, and an admin always has full access (a deny never locks out an admin). The deployment default posture (closed by default) remains the floor.
Example: give the analysts group read on all cubes, write on Budget, and deny on Salaries. Analysts then read everywhere, write Budget, and are blocked from Salaries; an unrelated user still sees nothing (closed default); an admin sees everything.
Surface
- REST:
GET/PUT /api/v1/acl/cube-grants(admin), with a single-knoblevelofnone | read | write | admin | deny, applied atomically. OpenAPI updated. - Web: a Cube grants tab in the security console listing the full cube-access picture and setting grants at the global or per-cube scope.
- Model-as-code: a new additive
[[cube_grant]]block in the security artifact; pre-m8.2 files load and re-serialize unchanged.
Quality
- New unit resolution matrix, end-to-end REST acceptance suite, and serialization round-trip tests; full workspace green, clippy and fmt clean.
- Hardened after an adversarial review: atomic single-knob writes (no half-applied state on I/O failure) and fail-closed, idempotent loading of hand-edited grant rows.
Compatibility: fully backward compatible. With no global grants or denies defined, behavior is identical to m8.1.
M8.1: Secure-by-default cube access
A security-hardening point release on top of M8.
Cubes are now closed by default
The Phase 7 security model shipped with ungranted cubes open to any authenticated user (chosen to avoid changing the pre-security behavior). That prioritized migration continuity over fail-closed, which is the wrong default for the phase that adds least privilege. This release flips it.
- Closed by default (fail-closed). A cube with no grants is now denied to non-admins; access is opened only by an explicit grant. This is the secure, least-privilege posture an operator gets out of the box. Admins always bypass; deleting a cube and managing its grants remain admin-only.
- Open by opt-in. Deployments inside one trusted organization that want zero per-cube grant friction can set
EPIPHANY_DEFAULT_CUBE_ACCESS=opento restore the open posture (ungranted cube writable by any authenticated user, never admin). The server logs a warning when this is enabled.
In both postures the first grant on a cube makes it "managed" and access is then exactly the grants plus admin bypass.
ADR-0015 decision 2a is amended to document both postures and why the default changed. A new acceptance suite (epiphany-api/tests/cube_default_access.rs) proves the secure default end to end -- a non-admin is denied an ungranted cube and admitted only by a grant -- and the opt-in open posture.
This addresses reviewer feedback that closed-unless-granted is the safer, standard secure-by-default posture. The full test suite is green and the supply-chain audit passes; binaries for Linux x86_64/aarch64, Windows x86_64, and macOS aarch64 are attached once CI builds them.
M8: Scheduling and hardening
Phase 8: scheduling and hardening, the final roadmap phase. Epiphany now runs unattended: a scheduled job refreshes data on its own, the server recovers cleanly from a kill, the audit log is bounded, and the performance and memory budgets are met.
Highlights
- Scheduled jobs (ADR-0013). A job is an ordered sequence of a cube's flows on an
Intervaltrigger, declared as model-as-code ([[job]], byte-identical round-trip). A single in-process reconcile loop wakes on the injected clock, computes which jobs are due as a pure function of the declared triggers and the durable last-fired ledger, and runs them through the same validated apply path an interactive flow run uses. - Determinism preserved end to end. Real wall-clock time enters at exactly one point: the loop's tick. That value is frozen as
fire_millisand carried down into the flow run, the run record, and the audit timestamp; no downstream code re-reads a clock. The whole scheduler is provable under aManualClockwith no real timers, which is how the acceptance suite drives it. - Durable run ledger with convergent recovery. Runs are recorded in an append-only, CRC-framed ledger (its own file, neither the WAL nor the audit stream) that reuses the proven torn-tail truncation. A run in flight at a crash recovers as interrupted and its firing re-derives as due; a scheduled write is committed through the all-or-nothing batch path, so it is durable and survives a restart. The ledger is bounded: it compacts to the newest runs plus each job's latest success, so a frequently-firing job cannot grow it without limit.
- Async run surface. Jobs have REST CRUD (each step validated against an existing flow), a manual kick that runs on a blocking thread, and run queries (
GET /runs,GET /runs/{id}). Every firing and run is audited (job identity only, no secrets, RG-13). - Audit retention and rotation (ADR-0010). The audit stream is now bounded by a record cap and an optional age window, compacted crash-safely via temp-then-rename, with sequence numbers preserved. Configurable via
EPIPHANY_AUDIT_MAX_RECORDSandEPIPHANY_AUDIT_RETENTION_DAYS. - Operations. Graceful shutdown and a zero-config
EPIPHANY_*surface (bind, data dir, sessions, scheduler tick, audit and run-ledger retention). The scheduler defaults on with a 1-second tick and can be disabled withEPIPHANY_SCHEDULER_TICK_SECS=0.
Quality
Gated end to end by a deterministic acceptance suite (epiphany-api/tests/m8_acceptance.rs): a job fires on schedule and commits, does not fire before its interval, an interrupted firing recovers and re-derives as due, a scheduled write survives a store reopen, and the job REST surface validates steps, runs a job manually, and lists runs. The performance and memory mandate is validated and documented (docs/PERFORMANCE.md): bulk-load about 13 M cells/sec/core (budget about 1 M), a cold consolidated read about 10 ms (budget p99 under about 1 s), bytes-per-cell within the 24-byte budget (CI-enforced), and a scheduler scale benchmark for the reconcile due-scan. Hardened by a multi-agent adversarial review that caught and fixed four real defects before release: an audit timestamp that re-read the clock, an unbounded run ledger, a manual kick that blocked the async runtime, and the scheduler authorization model (documented as a deliberate system-actor design). Tests stay dependency-free, the supply-chain audit is green, and the single self-contained binary is built for Linux x86_64/aarch64, Windows x86_64, and macOS aarch64.
This completes the nine-phase roadmap.