Changelog

What's changed.

Every release, every fix. Nimbus is in active development — this page tracks every change since the first public release.

July 10, 2026
1.3.4
Fixed
  • Aggregate queries that filter on a formula field and group by a relationship path no longer throw a spurious "No such column". A query that both filtered on a formula field in WHERE and grouped by a relationship-path field in GROUP BY — e.g. SELECT COUNT(Id), Answer__r.Question__c FROM Action__c WHERE isOpen__c = TRUE GROUP BY Answer__r.Question__c — failed at runtime with No such column 'isOpen__c' on entity 'Action__c', even though the field resolves fine in every other query shape. A field whose name starts with a lowercase letter (common for formula flags like isOpen__c) was resolved case-sensitively once the relationship GROUP BY introduced a join, so it missed the actual column; either ingredient alone worked. Such a field now resolves identically whether or not the query also groups by a relationship path, matching the platform. (#55)
July 10, 2026
1.3.3
Fixed
  • Parallel test runs no longer crash on classes that share a static counter. On large projects, running the suite in parallel could abort the whole run with a fatal concurrent map writes error when two tests incremented a static field (a ++/--) on the same managed-package/stub class at once — because that class's static state wasn't isolated per test. Each test now gets its own copy of every class's statics, so parallel execution is stable regardless of how tests touch shared static fields. A timed-out test also now has its transaction cancelled and rolled back instead of being left running (which previously left a database connection idle in transaction and could deadlock the next run), and the timeout message no longer assumes an infinite loop — it points at parallel contention and suggests re-running with -p 1 to confirm. (#9)
Added
  • nimbus validate now catches unimplemented interfaces. A concrete class that declares implements but doesn't define every method of the interface is flagged as a deploy blocker (validate exits non-zero), matching the compile error Salesforce raises — so you catch it before the deploy does, consistent with the language server. Interfaces and base classes are resolved from the local project; the check skips conservatively when a type can't be resolved (a managed-package or cross-file interface) so it never blocks a valid deploy. (#54)
  • nimbus validate now flags setting an AutoNumber field. Assigning an AutoNumber field in an SObject constructor (e.g. new Log__c(Name = 'x')) is reported as a deploy blocker — AutoNumber fields aren't writeable, and Salesforce rejects this at compile. This complements the runtime rejection added in 1.3.2, so the mistake surfaces at validate time too. (#52)
July 9, 2026
1.3.2
Fixed
  • Record-triggered flows now bulkify their Get Records elements. A flow that runs on record create/update executed its Get Records queries once per record, so a bulk DML of 200 records could fire hundreds of SOQL queries and hit Too many SOQL queries — while the same operation runs comfortably on Salesforce, which batches flow interviews. Get Records elements with an equality filter on a triggering-record field now run once for the whole batch (via an IN clause) and distribute the matching rows to each record, so query count scales with the number of elements, not the number of records — matching the platform. (#50)
  • AutoNumber fields now populate on insert, formatted per their display format. An AutoNumber field (including an AutoNumber Name field) stayed null after insert instead of receiving a generated sequence value. Inserts now assign the next value rendered against the field's display format — Log-{000000} yields Log-000001, and date tokens ({YYYY}/{YY}/{MM}/{DD}) are substituted anywhere in the pattern. Setting an AutoNumber field in Apex is now rejected with Field is not writeable, matching the platform (where it is a compile error). (#52)
  • Type.forName() returns null for a type that doesn't exist, instead of a phantom Type. Type.forName('Nonexistent__c') returned a non-null Type whose newInstance() produced a phantom object, silently breaking the standard null-guard pattern for dynamic factories (if (t != null) took the wrong branch). Unknown types now return null as on the platform, while real standard and project custom objects still resolve. (#51)
  • DescribeSObjectResult.childRelationships is now a non-null list. It returned null on every object, so any code iterating an object's child relationships threw a NullPointerException; on the platform it is always a non-null (possibly empty) list. Both access paths — getGlobalDescribe().get(name).getDescribe() and SObjectType.getDescribe() — now return the populated list, each entry exposing getChildSObject(), getField(), and getRelationshipName(). (#53)
  • Bulk inserts of records with a Text formula field that evaluates to a number no longer fail. Following the 1.1.11 fix for single-row inserts, the same case in a bulk insert — the common @testSetup shape — still failed with unable to encode 0 into text. The text coercion now covers every DML path (bulk insert, update, and non-transactional insert/update) and handles a whole affected column across all rows of a large batch in one pass. (#39)
  • nimbus doctor no longer flags valid dynamic seed keys. Config keys like nimbus.seed.queue.<DeveloperName> and nimbus.seed.group.<Name> — documented and working at test time — were reported as unknown config keys by the doctor's syntax check. The doctor now recognises all documented dynamic seed-key prefixes. (#49)
July 8, 2026
1.3.1
Added
  • --local-shards auto now sizes itself by suite length, not just core count. Splitting a run into isolated local shards only pays off once a suite is long enough to amortize the extra process and database startup — on a short suite it can be slower than a single process. auto previously looked only at your CPU count and could pick multiple shards for a suite too short to benefit; it now also considers the wall-clock time of your last full run and stays single-process when sharding wouldn't help. The recorded duration only ever comes from a genuine full run, never a --shard slice or an --impacted subset.
  • --local-shards runs now report where time went. The merged summary from a sharded run now includes the same SOQL / DML / code / setup time breakdown a single-process run shows — summed across shards, in both the human-readable summary and --json — instead of dropping it.
Fixed
  • Validation-rule formulas now follow Salesforce's three-valued null logic. A comparison against a blank field — for example Date__c == null or Field__c != null — was evaluated as a plain true/false, so a rule written that way fired on records the platform accepts. On Salesforce such a comparison is undefined, and a validation rule fires only on a strict true, which means these rules are effectively inert for the blank case. nimbus now matches: == null/!= null against a genuinely blank value no longer triggers the rule, while ISBLANK() remains the explicit way to test for blankness and still fires as before. (#44)
  • Querying a profile by Id now returns the same profile as querying it by name. When a profile that isn't one of the standard seeded profiles was looked up by name, then re-queried by its Id or reached through a User's Profile.Name relationship, each path could return a different profile for the same Id — so profile-gated logic silently evaluated false. A profile resolved on demand is now a real, consistent record: by-name, by-Id, relationship traversal, and an unfiltered SELECT ... FROM Profile all agree on its Id and name. (#46)
  • equals() now works on Boolean values. Calling .equals(...) on a Boolean failed with "Unknown method equals on Boolean", even though every Apex type supports it and String and Integer already worked. This surfaced most often in generic code comparing Object-typed field values, such as dirty-checking a Checkbox field. (#45)
  • SOQL binds that call a method on a string literal now translate correctly. Completing the bind-expression fix from 1.3.0: a bind whose receiver is a string literal — for example WHERE Name IN :'a,b'.split(',') or = :'a,b'.substringBefore(',') — still leaked its raw text into the generated SQL and failed with a Postgres syntax error. Such binds are now evaluated and bound as values like any other. (#43)
  • String filters in relationship subqueries are now case-insensitive, matching flat queries and the platform. A WHERE filter on a text or picklist field was case-insensitive in a flat query but case-sensitive inside a parent-to-child subquery — so (SELECT Id FROM Contacts WHERE LastName = 'SMITH') returned no rows when the stored value was Smith, while the same flat filter matched. Both positions are now case-insensitive, as on Salesforce. Relatedly, DescribeFieldResult.getType().name() now returns the uppercase enum name (STRING, PICKLIST) instead of mixed case, matching the platform and the bare Schema.DisplayType enum. (#48)
  • JSON.serialize now preserves the exact casing of member names. Serializing an Apex object emitted its field and property names in all-lowercase (fileName became filename), producing JSON with the wrong keys — a consumer reading result.fileName found nothing, and nothing threw. Member names now serialize with their declared casing verbatim, in the platform's member order. (#47)
  • Fixed non-deterministic ordering that could make a few tests flaky between runs. A handful of describe- and schema-backed maps (global describe, a field collection, record-type infos) didn't preserve insertion order, so iterating them fell back to Go's deliberately randomized map order — a test asserting on the order of values()/keySet() could pass or fail from one run to the next. Those maps now keep a stable order, and a related dynamic WHERE Id = :x lookup on RecordType now filters correctly instead of matching everything.
  • Cold-starting --local-shards with several shards is more reliable. Launching many shards at once made their embedded databases initialize simultaneously and contend for disk, which could push a shard past the startup deadline and fail the run. Shard startup is now staggered and the readiness timeout is more forgiving, so large shard counts start cleanly on a busy machine.
Performance
  • Lower memory churn across large test suites. A round of interpreter and SOQL profiling removed several hot allocation sources that fire on nearly every method call, field access, and query: return-value tracking is no longer done for every statement unless something consumes it, case-insensitive name resolution reuses cached lowercased identifiers at many more call sites, class- and method-name bookkeeping shares immutable string wrappers, and parsed SOQL bind expressions are cached instead of re-parsed on every query execution. Several fixed regular expressions in the SOQL translator are also compiled once instead of on every query. Results are unchanged; large runs allocate substantially less and spend less time in garbage collection.
July 4, 2026
1.3.0
Added
  • Sharded runs now split by execution time, not test count. nimbus test --shard i/N assigns test classes to balance each shard's wall-clock time, using per-test durations recorded from previous full runs, instead of dividing purely by class count. A few slow classes no longer pile onto one shard while the others finish early — on a large suite we track, the imbalance between two shards dropped from 2.2x to 1.26x. With no recorded timings yet (a new project, or a cleared cache) it falls back to the previous count-based split, and independent CI processes always compute the same assignment without coordinating.
  • Experimental: run isolated shards on a single machine with --local-shards N. nimbus test --local-shards N|auto spawns N shards as concurrent processes against the same project checkout, each with its own isolated local database, and merges their results into one report (human-readable, --json, or JUnit). Today this is about isolation, not speed — a single-process -p N run is still faster, and --coverage is not yet supported with --local-shards. The correctness and merge machinery is complete; a future release turns it into a speed win.
Fixed
  • SOQL bind expressions that call a method with arguments now translate correctly. A bind like WHERE Name IN :someString.split(',') or :MyClass.ids(x) left the argument parentheses stranded in the generated SQL, so the query failed with a Postgres syntax error near (; only no-argument calls such as :UserInfo.getUserId() happened to work. Bind expressions with arbitrary arguments now resolve properly. (#43)
  • Validation rules now run after before-insert triggers, matching the platform's order of execution. On insert, custom validation rules were checked before before-insert triggers had a chance to run, so a trigger that defaults a required field (for example setting a blank Name) could not satisfy a rule that required it — the record was rejected even though a real Salesforce save would have passed. Insert now runs before-triggers first, then validation rules, consistent with how update already behaved. (#42)
July 3, 2026
1.2.0
Added
  • Run only the tests affected by your changes with --impacted. nimbus test --impacted builds a dependency graph from your local edits and runs just the test classes that exercise the changed code, skipping everything else. The class-dependency map is persisted between runs and wired into watch mode, so saving a file re-runs only its impacted tests automatically. Pass --no-impact to force a full run.
  • The MCP server can now run anonymous Apex, SOQL, and schema lookups against the local runtime. New execute_anonymous, query, and describe_schema tools let an AI agent or editor execute a snippet, run a SOQL query, and inspect an object's fields without an org connection. Test failures returned over MCP and JSON now carry structured diagnostics — the failing assertion, the exception, and the execution path that reached it — instead of a bare message.
  • Time-travel debugging: record a test run, then replay it in the debugger. The debug adapter can capture an execution trace and step through it after the fact — forward and backward over the recorded run — so you can inspect how state reached a failure without repeatedly re-running to the same breakpoint.
Fixed
  • Identifiers with non-ASCII characters now match case-insensitively, as Apex does. A field, variable, or type name containing non-ASCII letters (for example accented or non-Latin characters) could fail to resolve when referenced with different casing, because the case-folding fast path left such names unchanged. Case-insensitive resolution now covers the full Unicode range, matching the platform.
Performance
  • Large test suites run substantially faster. A round of database and interpreter work removed the main bottlenecks on big projects — the largest suites we track finish in roughly half the time. The improvements below apply automatically, with no configuration.
  • Hierarchy and list Custom Settings resolve from cache instead of the database. Repeated getInstance(), getOrgDefaults(), and getValues() lookups — which fire constantly in framework-heavy code — are now served from an in-memory snapshot taken once per run, including the very common case where the setting has no matching row for a user or profile. On settings-heavy suites this removes the bulk of query traffic. Tests that modify a setting still see their own writes.
  • Parallel runs no longer stall behind on-the-fly schema changes. When a record used a field the local schema hadn't materialized yet, nimbus added the column inside the test's own transaction — taking a lock that blocked every other worker touching that table until the test finished. Those columns are now provisioned up front, so parallel workers stop serializing on the first insert of an affected object.
  • Roll-up summary recalculation issues far fewer database queries. When a save recalculates several roll-up summary fields on the same parent record, nimbus now computes them in one combined query per parent instead of one query per field, cutting round-trips on trigger-heavy save paths. Results are unchanged.
  • Lower memory churn in the interpreter. Case-insensitive name resolution — which runs on nearly every field access, method call, and variable lookup — now reuses cached lowercased identifiers instead of recomputing them, cutting total allocations on a large run by roughly a quarter and easing garbage-collection pressure.
June 30, 2026
1.1.12
Fixed
  • Stopping the daemon no longer breaks a test run that's already in progress. If a separate nimbus test/nimbus exec was using the database, stopping the daemon — by command or signal — used to clear the tables and shut down Postgres underneath it, failing every remaining test with "Failed to begin transaction". The daemon now detects an in-flight run and leaves the database up for it, completing its own shutdown without disturbing the run. (#10)
  • Abandoned local Postgres instances are now cleaned up automatically. A daemon killed with SIGKILL, a crashed run, or a daemon stopped mid-run could leave a background Postgres process with no owner — invisible to nimbus daemon list and, if you never returned to that project, lingering until reboot. nimbus now reaps these on its next run in any project: an instance with no live daemon and no active connections is stopped and its data removed. Instances that are in use or daemon-managed are never touched. (#10)
  • An unclean stop no longer hangs the next start. A crash mid-recovery could leave the data directory in a state that stalled the next Postgres startup; that case is now detected and the cluster rebuilt deterministically on start. (#10)
June 29, 2026
1.1.11
Fixed
  • Calling a method on a static Map field whose name matches a system class now works. A field named CACHE collides with the Platform Cache class, and CACHE.put('k', 'v') failed with "Unknown method put on Type CACHE" — while .get() quietly returned null. The bare field name resolved to the system class instead of the field. A static field now shadows a same-named system class, just as instance fields and local variables already do, so the idiomatic static-Map cache populated via .put() (often in a static {} block) holds its entries. (#38)
  • Inserting a record with a Text formula field that evaluates to a number no longer fails. A Text-typed formula field is stored as text, but when its formula reduced to a number (for example arithmetic with blanks treated as zero), the insert aborted with "unable to encode 0 into text". A Text formula now yields text — matching the platform, where a Text formula field always returns a string — so the value is stored as "0" and records whose @testSetup touched such a field save cleanly. (#39)
June 28, 2026
1.1.10
Fixed
  • nimbus sync --include-setup-data no longer aborts the whole pull when one setup object can't be queried. An org without Experience Cloud has no queryable Network, so SELECT Name, Status FROM Network errored and that single failure stopped the entire pull — no Group, queue, or UserRole entries were written. Each object type is now pulled independently: a type the org can't query is skipped with a warning and the rest still land. The pull only fails outright when no setup object can be queried at all (a bad org alias or no connection). (#33)
  • The setup-data seeders now appear in nimbus config properties and nimbus config show. nimbus.seed.group.*, seed.queue.*, seed.role.*, seed.network.*, and seed.user.* were honored at runtime but absent from the config listings, so there was no way to confirm a hand-authored key was recognized. They are now documented alongside the other seed keys. (#33)
June 26, 2026
1.1.9
Added
  • Seed Networks (Experience Cloud sites) for local runs. New nimbus.seed.network.<Name>=<Status> key. Code that gates on a community — [SELECT Id FROM Network WHERE Name = 'Member Hub'] — resolves the seeded row instead of failing on an empty single-row query. Status defaults to Live. Querying Network with no rows now returns 0 rather than erroring on a missing object. (#33)
  • Seed named Users by Name. New nimbus.seed.user.<Name>=<Username> key. Factories that look up integration or system users by name — [SELECT Id FROM User WHERE Name = 'System Salesforce'] — now resolve. The Name is split into FirstName/LastName so the generated Name field reconstructs exactly; the Username is optional. (#33)
  • nimbus sync --include-setup-data now also pulls Networks. Alongside Groups, queue routing, and UserRoles, Network rows are read from the org and written as nimbus.seed.network.* entries. Named Users are left as an explicit seed list rather than bulk-synced, since a large org has thousands. (#33)
Fixed
  • Record-triggered flows no longer blank Date, DateTime, and Time fields. A flow that updated its own triggering record (Update Triggering Record, or any update of $Record) silently nulled every temporal field on the saved record, on both insert and update — String and reference fields survived, which made it look field-type-specific. Those fields are now preserved when $Record is built for the flow. (#35, #36)
  • Unfiltered SELECT COUNT() FROM Profile now counts the backing rows. The aggregate returned 0 while the same query with WHERE Name = '…' returned 1; the unfiltered count path didn't enumerate the Profile pseudo-table's rows. Filtered and unfiltered counts are consistent now. (#37)
  • More standard objects are provisioned for queries. ProcessInstanceWorkitem, ProcessInstanceStep, UserLogin, RecentlyViewed, and NavigationLinkSet no longer fail with "relation does not exist", and ContentFolderItem.IsFolder resolves instead of "No such column". (#32)
June 26, 2026
1.1.8
Added
  • Seed UserRoles and Queues for local runs. Two new keys join the existing group/site/label seeders: nimbus.seed.role.<DeveloperName>=<Name> and nimbus.seed.queue.<DeveloperName>=<SObject1>,<SObject2>. A queue seed inserts the queue plus its routing for each listed SObject, so case and lead assignment automation resolves owner candidates; role seeds make WHERE DeveloperName = … lookups against UserRole resolve. These are setup objects that test factories assume exist but repos rarely check into metadata. (#33)
  • nimbus sync --include-setup-data pulls setup objects from the connected org. Groups, queue routing, and UserRoles are read from the org and written back as nimbus.seed.{group,role,queue}.* entries in nimbus.properties, under a delimited block. Hand-authored seed lines outside the block are preserved across re-runs, so you can reproduce org-resident setup data locally without authoring every entry by hand. (#33)
Fixed
  • Configured governor limits are now honored by the Limits API and by nimbus exec enforcement. nimbus.governor.soql-queries and nimbus.governor.dml-statements were accepted and shown by nimbus config show, but Limits.getLimitQueries() / Limits.getLimitDmlStatements() always returned the standard 100 / 150 regardless of the override, and nimbus exec kept enforcing at the defaults. Both now reflect the configured values, so projects on orgs with Salesforce-raised limits can match their runtime ceiling locally. (Enforcement under nimbus test already honored the override.) (#34)
  • SOQL query limit now varies by execution context. Asynchronous contexts (Queueable, @future, Batch) get the platform's 200-query ceiling instead of the synchronous 100, in both enforcement and the Limits class. Previously async code was capped at 100, so async tests that legitimately exceeded 100 queries failed locally despite passing in the org. An explicit nimbus.governor.soql-queries override stays authoritative across both contexts. (#34)
  • Group seeds are now resolvable by DeveloperName, not just Name. Factories that looked up a seeded group by its DeveloperName previously came up empty because only Name was written. Both columns now carry the seed key.
  • Warm-daemon runs reseed after edits to seed config. The daemon's seed cache fingerprint omitted group, label, role, and queue seeds, so editing those entries in nimbus.properties silently had no effect until a cold start. Changes are now picked up on the next run.
June 25, 2026
1.1.7
Performance
  • --fetch-missing is no longer ~20x slower. The retriever was issuing one sf sobject describe per missing object — each its own process spawn + org round-trip — serially. It now runs them across up to 8 concurrent goroutines. For N missing objects the wall time drops from N×t to roughly ceil(N/8)×t. (#8)
Fixed
  • Pool stalls on large projects (~1,800+ tables). The connection pool formula (workers + 5, cap 100) was too tight when per-worker-schema cloning and test execution overlapped — setup transactions competed with test transactions and the pool ran dry. Changed to workers × 2 + 10, cap 200, with a DropWorkerSchema retry loop for transient deadlocks matching the existing CreateWorkerSchema retry. (#9)
  • Flow sub-executor was missing ctx and metadataProvider propagation. executeSubflow created its sub-executor without forwarding the parent executor's context and metadata provider, so a subflow that queried custom metadata or used ctx-dependent features (timeouts, cancellation) could fail or behave differently from the calling flow.
  • Standard objects without an embedded describe were missing their DB tables. The schema provisioner creates tables for standard SObjects from bundled JSON files, but some objects declare no embedded describe at all (only custom-field metadata). Those objects now get their tables created anyway from the standard-schema scan, so SELECT from them works instead of reporting a missing-relation error.
June 24, 2026
1.1.6
Fixed
  • Cross-object formula references on the parent evaluated to blank. A formula field on a child object that referenced a formula field on the parent — Parent__r.ParentCode__c where ParentCode__c is itself a formula — returned null because the resolution logic stopped at the parent's stored columns and never resolved its computed fields. The cross-object resolver now detects when the requested field is absent from the parent's DB row and recursively resolves it via resolveFormulaFields on the parent SObject. The resolved value is cached so subsequent lookups avoid re-resolution.
June 23, 2026
1.1.5
Fixed
  • DML after a prior transaction rollback no longer crashes Nimbus with a SIGSEGV. On the platform, a rolled-back transaction returns a catchable exception on any subsequent DML attempt. Under Nimbus, a record-triggered Flow that performed DML after the underlying Postgres transaction was already closed (via a prior deadlock recovery or explicit rollback) dereferenced a nil transaction handle, crashing the process with no test-framework recovery path. DML methods now check for a valid transaction before any operation, so a rollback mid-test produces a catchable error instead of a segfault.
June 23, 2026
1.1.4
Fixed
  • Record-triggered flows now count toward DML governor limits. Every DML operation on the real platform counts against the 150-DML transaction limit. Nimbus's flow executor was calling the DML engine through a path that bypassed the DML counter, so flow-heavy tests could exceed limits without detection and the TUI write counter under-reported actual DML volume. Flow-initiated inserts, updates, and deletes now count correctly.
  • A before-save flow with a Loop + Create/Update Records no longer re-dispatches the trigger chain indefinitely. On the platform, a before-save flow that creates records on the same object type converges — each DML fire is bounded. Under Nimbus, a Loop element iterating a collection with a Create/Update Records inside could re-enter the before-save trigger chain thousands of times, with each iteration adding ~5 SOQL, culminating in System.LimitException: Too many SOQL queries or a 25 s timeout. The trigger re-entry is now capped at one dispatch per flow per (object, trigger event) pair.
June 22, 2026
1.1.3
Fixed
  • Building a Map<Id, SObject> from records without Ids throws, matching the platform. new Map<Id, SObject>(records) where a record has no Id — an unsaved list, or new Map<Id, X>(Trigger.new) in a before-insert — now throws System.ListException: Row with null Id at index: N (the first offending row) instead of silently producing an empty or partial map. Maps built from queried records, and the copy constructor, are unaffected.
June 22, 2026
1.1.2
Added
  • Schema.describeTabs() returns your project's tabs. The returned tab set is now populated from the *.tab-meta.xml (CustomTab) metadata in your project — source-driven, like the rest of the schema. An object tab resolves its getSobjectName() and label from the object; web, Visualforce, and Lightning tabs carry their own label. Code that resolves a tab or icon for an SObject now finds it instead of getting an empty list.
June 22, 2026
1.1.1
Fixed
  • A custom exception's own message field is distinct from getMessage(). When a class that extends Exception declares its own public String message field, reading the field now returns the assigned value while getMessage() returns the exception's built-in message (defaulting to Script-thrown exception when none was set) — matching the platform, where the two are separate slots. Previously both resolved to the first constructor argument.
  • Type.newInstance() on an inner class of an @IsTest class upcasts correctly. Instantiating a test-local subclass reflectively — the common pattern of registering SomeType.class in a Map<…, Type> and later doing (Base) registry.get(key).newInstance() with a mock subtype defined inside the test class — now resolves the inheritance instead of throwing Invalid conversion from runtime type.
  • Schema.describeTabs() is implemented. It returns a non-empty List<Schema.DescribeTabSetResult> (matching the platform guarantee that the call is never empty) instead of aborting execution with a malformed error. Tab/app metadata isn't synced, so the set carries no per-object tabs.
  • Standard OpportunityStage reference data is seeded. The standard sales-process stages (Prospecting … Closed Won / Closed Lost, with IsClosed / IsWon / DefaultProbability / ForecastCategory) are now present, so common @testSetup idioms that pick a valid stage from org config no longer return zero rows. Orgs with a customised stage picklist can override via nimbus.seed.record.
June 21, 2026
1.1.0
Added
  • Code generation in the editor. Yellow-bulb refactors on a class declaration: generate a constructor from the class's fields, generate getters and setters, implement the methods of every interface in the implements clause (including Queueable, Schedulable, Comparable, and Database.Batchable), and override a superclass's virtual/abstract methods. Served by the language server, so they appear in both the JetBrains plugin and the VS Code extension.
  • Relationship-aware SOQL and SOSL completion. Completion inside a query now follows relationships — typing Owner. or Account.Parent. resolves the chain and offers the related object's fields — and child-relationship subqueries ((SELECT … FROM Contacts)) complete the child object's columns. SOSL RETURNING Account(…) completes that object's fields too. Standard objects work with no sync. Relationship fields are also validated, so a typo like Owner.Namee is flagged once the chain resolves to a synced object.
  • Apex and SOQL live templates. A bundled set of about twenty templates — sysdSystem.debug(), tm → an @IsTest method, soqlf → a bulk-safe SOQL for-loop, bulkt → a 200-record test-data block, and more — ship in both the JetBrains plugin (native live templates) and the VS Code extension (snippets).
  • Coverage delta in the gutter. Snapshot a coverage run as a baseline ("Set Coverage Baseline"), then "Toggle Coverage Delta" to overlay what changed since — green for newly-covered lines, orange for regressions — in both editors.
  • Three new CLI commands. nimbus new <kind> <Name> scaffolds a class, test, trigger, trigger handler, batch, queueable, or schedulable (with its -meta.xml) into your default package. nimbus fixture <SObject>… generates a TestDataFactory with required fields filled in from the schema. nimbus coverage diff <base.json> <pr.json> reports the coverage delta — overall, per file, and the exact newly-uncovered lines — with --fail-on-drop for CI gates.
  • Parallel mutation testing. nimbus mutate takes a -p/--parallel flag and runs mutants across multiple workers, the way nimbus test already parallelizes test runs (Pro).
  • More signals for AI agents over MCP. The MCP server adds get_governor_usage (per-test SOQL/DML/CPU/heap from the last run) and run_mutation_tests (mutation score plus surviving mutants), so an agent can spot un-bulkified code and weak assertions without leaving its loop.
Fixed
  • Summer ’26 (API v67) semantics. Classes declared on API version 67.0 or later now enforce two Summer ’26 changes: an unannotated class runs as with sharing instead of system mode, and a SOQL query using WITH SECURITY_ENFORCED throws a System.QueryException (the clause was removed). Classes on earlier API versions are unchanged.
  • Child-relationship subqueries no longer report a false error. A parent query with a subquery — SELECT Id, (SELECT Id FROM Contacts) FROM Account — previously flagged the relationship name (Contacts) as an unknown SObject. It's now recognized as a child relationship.
June 20, 2026
1.0.9
Added
  • Code coverage in the editor, with a per-class Coverage button. A new Coverage view shows the line coverage for the run you just did — an overall score plus a per-file breakdown sorted most- to least-covered, labeled with the test it came from. A 🪄 Coverage action now sits at the top of every test class (alongside each test method), so you can collect coverage for a whole class in one click. The class-level button is served by the language server, so it appears in both the JetBrains plugin and the VS Code extension.
  • A tabbed execution-trace viewer. Traces open with Execution Log, Call Tree, Timeline (a flame graph), and Flow (a call-flow graph) tabs, plus an inspector for the selected span — its attributes, events, and the variable snapshots captured inside it.
  • Detect the local runtime from Apex via Organization.InstanceName. Querying InstanceName on Organization returns Nimbus by default, so code can branch on whether it's running locally ([SELECT InstanceName FROM Organization].InstanceName == 'Nimbus'). Real Salesforce instance names are always uppercase, so there's no collision; override it per project with nimbus.org.instance-name.
Fixed
  • null instanceof T follows the platform's API-version rule. It returns true for code on API 31.0 or earlier and false from 32.0 (Spring ’15), governed by the class that contains the expression — so frameworks that rely on the legacy behavior (for example apex-lambda’s comparers, where compare(null, null) must report equal) behave correctly.
  • Typed Decimal collections coerce integer literals. new Set<Decimal>{60, 150} now stores Decimal values rather than Integer, so membership checks against Decimal field values match instead of silently missing.
  • Collection-type instanceof and casts match the platform. A List<SObject> satisfies instanceof against any concrete SObject subtype, and an explicitly cast collection argument — Collection.of((List<Account>) Trigger.new) — resolves List/SetIterable<T> overloads instead of failing with “no matching overload.”
  • SOQL binds that are an inline cast translate correctly. WHERE Id IN :(Set<Id>) raw previously produced a database syntax error; the cast operand is now captured correctly.
  • System.-qualified valueOf(…) works. System.Id.valueOf(…), System.String.valueOf(…), and the other qualified forms previously returned null; they now reach the same handlers as the unqualified forms.
  • JSON.deserialize rebuilds compound address fields. Deserializing a record with BillingAddress/ShippingAddress now materializes a read-only System.Address, so record.BillingAddress.getCity() works instead of failing on a raw map.
  • Coverage is collected on every run, not just the first. When the background daemon was warm, only the first coverage run in a session reported line data — every run after it came back empty until the daemon restarted. Coverage now loads the program sources it needs on each run, so repeated coverage runs against different classes always report correctly.
  • Editor-driven runs honor managed-package stub namespaces. Tests launched from the editor (rather than the CLI) now apply the nimbus.stubs.namespaces setting from nimbus.properties, so references to auto-stubbed managed packages resolve the same way they do on the command line.
June 19, 2026
1.0.8
Added
  • A JetBrains / IntelliJ plugin. Nimbus now runs inside IntelliJ IDEA and the other JetBrains IDEs, alongside the VS Code extension. Run and debug Apex tests with breakpoints, step-through, and variable inspection; see results in the IDE's test runner; collect coverage; and get Apex completion, hover, navigation, diagnostics, code lenses, and inlay hints from the language server. A Nimbus tool window adds test history, a schema explorer, and governor-limit usage, and a watch mode re-runs tests as you save. Requires the nimbus CLI on your PATH.
June 18, 2026
1.0.7
Performance
  • Sync reads project metadata concurrently. Building on v1.0.6, the three independent passes a sync makes over your metadata — bundled standard schemas, custom fields on standard objects, and custom objects — now run at the same time instead of one after another. On large orgs the heavy passes read separate sets of files, so overlapping their disk I/O shortens the scan further, most noticeably on slower or contended filesystems. The result is unchanged; only the time to produce it.
June 18, 2026
1.0.6
Added
  • Object-scoped sync: nimbus sync -s Account,Contact,My_Object__c. Re-sync just the objects you name instead of the whole org. When you're iterating on a handful of objects, there's no longer any reason to wait on a full-org sync — other tables are left untouched. (--sobjects is the long form.)
  • Sync is now incremental by default. A re-sync creates missing tables and adds missing columns without dropping anything, so running nimbus sync when nothing has changed is now near-instant rather than a full rebuild. Pass --rebuild to force the old drop-and-recreate behavior — the only mode that applies column type changes (and it wipes table data).
Performance
  • The first sync on a large org is dramatically faster. On orgs with hundreds of objects and heavy custom-field counts, the initial sync could take many minutes. Two changes cut that down: object and field metadata is now parsed in parallel across CPU cores rather than one file at a time, and table creation runs as a single batched operation instead of committing each statement separately — which is where most of the time went on slower or contended disks. Redundant per-table indexing was also removed. The schema produced is identical; only the time to build it changed.
June 18, 2026
1.0.5
Fixed
  • Compound address fields now populate across parent relationships. v1.0.4 reconstructed a System.Address from the components of a directly queried record; this extends it to relationship-traversed compounds. SELECT Account.BillingAddress FROM Contact followed by record.Account.BillingAddress.getCity() — or the equivalent Parent__r.Address through a custom lookup — now hydrates the parent record with the address components and rebuilds the compound on access, matching the platform.
June 18, 2026
1.0.4
Added
  • String.fromCharArray(List<Integer>) is now supported. Building a string from a list of character codes — for example String.fromCharArray(new List<Integer>{160}) to produce a non-breaking space — previously failed with an "Unknown method" error. It now returns the expected string, so code that strips or inserts specific characters by code point runs as it does on the platform.
Fixed
  • Compound address fields are now populated on queried records. Selecting a compound address field — Address on Lead/Contact, BillingAddress / ShippingAddress on Account, and the equivalents — and then reading it back (record.Address.getCity(), System.debug(record.Address)) returned null. Nimbus now reconstructs the System.Address from the underlying street/city/state/postal/country components for records returned by SOQL. Fresh in-memory records still return null for the compound field, matching the platform (the value only materializes on query).
  • Static methods inherited from a parent class now resolve. Calling an inherited static method without a class qualifier — directly inside a subclass method, or from a static field initializer such as static final String x = baseMethod(); — failed with "Unknown function". Nimbus now walks the superclass chain when resolving an unqualified static call, so classes that extend an abstract base behave as they do on the platform.
  • Regex patterns that escape ordinary characters now compile. Apex (Java) regex treats a backslash before any character as that literal character, so patterns that escape punctuation or non-ASCII characters are valid. Nimbus rejected some of these with an "invalid escape sequence" error because its primary regex engine is stricter than Java's. Such patterns now fall back to a Java-compatible engine and compile and match correctly across Pattern / Matcher and String regex methods.
June 17, 2026
1.0.3
Added
  • Default-value formulas are now evaluated on insert. A field whose default value is a formula — a Time field that defaults to TIMEVALUE(...), a Date that defaults to TODAY() + 1 — now has that value computed and filled in when you insert a record without setting the field, matching the platform. Defaults are applied before before-insert triggers, so triggers and any value you set explicitly still take precedence. Literal defaults (text, number, checkbox) are unchanged.
June 17, 2026
1.0.2
Fixed
  • A field with a formula default value no longer breaks database setup. When a custom field's default value was a formula (e.g. a Time or Date field defaulting to TIMEVALUE(...) or TODAY()), the formula text was written into the table schema as a literal default — which PostgreSQL rejected, and the failed table creation aborted the whole run, so no tests could execute. These defaults are now handled correctly. (v1.0.3 goes further and evaluates them on insert.)
June 16, 2026
1.0.1
Added
  • nimbus mutate now mutates the class under test when you point it at a test class. Running mutation testing against a test class (e.g. nimbus mutate FooServiceTest) previously found zero mutants — test code is never mutated — and dead-ended at a misleading 0.0% score. Nimbus now derives the class under test from the conventional Test prefix/suffix, mutates that, and scopes each mutant run to the named test suite. Behavior is unchanged when a production class is already in the selection.
June 16, 2026
1.0.0
Released
  • Nimbus 1.0. Local Apex execution, SOQL, DML, triggers, and Flows against an embedded PostgreSQL — no scratch org, no Docker, no JVM. The runtime, coverage, debugger, parallel execution, mutation testing, LSP, and VS Code integration are stable and run thousands of tests daily across real Salesforce codebases.
  • Pro is free. Every Pro feature is available at no cost with a Pro license — no card required. Claim one from the portal and run nimbus login to sign in your CLI.
June 15, 2026
0.3.7
Changed
  • XML coverage and test-results output is now included with Pro. Cobertura coverage (--coverage-report *.xml) and JUnit results (--results-xml) — previously a separate tier — are now available on any Pro license, including the CI license. CI already requires Pro, so your pipeline can produce SonarQube/Codecov/GitHub-quality-gate reports with no extra entitlement.
June 15, 2026
0.3.6
Fixed
  • CI license keys are now accepted regardless of how the license is issued. Some valid, active Pro/Team keys were rejected on CI runners with an authentication-policy error — so nimbus test failed even with a correctly configured NIMBUS_LICENSE_KEY secret. License validation now falls back to a key-based check that works for every license type, and trusts the licensing server's current expiry rather than the date embedded in the key at issue time (which can lag a renewal).
  • License errors in CI report the real reason. A key the server actively rejects (expired, revoked, or refused by policy) was previously reported as "please connect to the internet to activate your license", even on an online runner. Definitive rejections are now surfaced with their actual cause, instead of being masked as a network outage or carried by the offline grace period.
June 13, 2026
0.3.5
Fixed
  • Platform events now deliver with faithful transaction semantics. Each delivery generation runs as its own transaction with fresh governor limits — exactly how the platform bounds each subscriber transaction — so a subscriber that re-publishes is bounded by the SOQL/DML caps instead of delivering inline forever (an exception-logger that published on failure could previously loop until it exhausted limits). And a Publish-After-Commit event published in a transaction that then fails is now discarded rather than delivered: a rolled-back transaction publishes nothing, just like the platform.
  • Governor limit configuration is now honored. nimbus.governor.soql-queries and nimbus.governor.dml-statements in nimbus.properties are now applied at runtime (an unset value falls back to the platform default of 100 / 150). Previously only nimbus.governor.mode took effect and the numeric caps were ignored.
  • Record-triggered flows with only an async scheduled path no longer block DML. A flow whose <start> has only an AsyncAfterCommit scheduled path (no synchronous connector) used to fail to parse and fail every DML on the triggering object. It now degrades to a no-op for synchronous execution — the async path wouldn't run during a synchronous test anyway.
  • The standard emailSimple (Send Email) flow action is now a no-op instead of an error. It was being resolved as an Apex invocable and failing with no @InvocableMethod found in class: emailSimple, which failed every DML that triggered the flow. Standard side-effect actions (emailSimple, emailAlert, chatterPost) are now skipped — they have no record state a test asserts on, and the platform doesn't send email during tests.
  • nimbus exec now evaluates validation rules, like nimbus test. An insert that violated an active validation rule used to succeed under exec but fail under test; both paths are now consistent.
  • nimbus doctor detects dotted managed-package references. It now flags usages like dlrs.RollupService.triggerHandler() (previously only the ns__Object / field form was matched), while still ignoring ordinary SObject field access such as acc.Name — so it can nudge you to stub a managed package before tests fail at runtime.
  • State & Country picklists: the country text field is derived from the country code. With State and Country picklists enabled, setting only BillingCountryCode = 'NL' now populates BillingCountry = 'Netherlands' before triggers and validation run, using the field's own picklist mapping — so country validation rules stop misfiring on records that are valid in the org. (Country only for now; state codes aren't globally unique and need country-scoped resolution.)
Added
  • nimbus daemon list and nimbus daemon stop --all. Daemons are per-project, so stray daemons from a previous session (or an MCP client on a different org) used to linger invisibly and daemon stop only stopped the current project's. daemon list now shows every running daemon (pid, org, project) and daemon stop --all stops them all in one command.
June 12, 2026
0.3.4
Fixed
  • USER_MODE queries inside System.runAs now honor the System Administrator profile. When a runAs user had any permission set assigned, AccessLevel.USER_MODE access was derived from those permission sets alone — the user's profile contributed nothing. A System Administrator has implicit View All / Modify All plus full field-level security, and permission sets only add to that, so a query for a field the profile already grants (a standard field like Account.Industry, or a custom field not named in the perm set) wrongly threw System.QueryException: No such column. Sysadmin runAs users now get full access regardless of assignments; Standard User and restricted-profile semantics are unchanged.
  • USER_MODE field-level security now unwraps SOQL function calls in the SELECT list. A select-list entry like toLabel(My_Picklist__c), FORMAT(Amount), or convertCurrency(Amount) was checked for FLS as if the whole expression were a column name, so it failed with No such column 'Tolabel(my_picklist__c)' even when the underlying field was perfectly readable. The check now resolves the inner field before verifying access, matching the platform.
June 12, 2026
0.3.3
Fixed
  • Newly created test classes are discovered without a restart. Long-running Nimbus processes — the background daemon and the MCP server that AI coding agents drive — captured the set of test classes when they started and never refreshed it. A test class you (or an agent) added mid-session, or one a git branch switch brought in, stayed invisible until the process was restarted. The runner now reconciles its in-memory view against the filesystem before each run, the file watcher waits for a freshly written class to finish flushing before parsing it, and a bulk change like a branch switch triggers a full re-scan — so the set of tests that runs is always the set that's on disk.
  • Child relationship names are no longer mangled when a lookup field's API name looks like a namespace. A custom lookup or master-detail field whose API name is a single lowercase segment — e.g. a field named region__c with relationship name Stores — had its parent's child relationship derived as region__Stores__r instead of Stores__r, because the leading region was misread as a managed-package namespace. Every SOQL subquery against the real relationship silently returned no rows, and getChildRelationships() reported the wrong name. A genuine namespace prefix carries two underscore pairs (ns__Field__c), so a single-underscore field name is now treated as un-namespaced, matching the platform.
May 25, 2026
0.3.2
Added
  • The remaining ConnectApi enum gaps are closed. 37 enums across the D/L/O/P/Q/S/W range — SObjectFieldDataType, SavedPaymentMethodStatus / SavedPaymentMethodType / SavedPaymentMethodUsageType, WorkflowProcessStatus, StreamingAppDataConnectorSubTypeEnum, DelimiterTypeEnum, SortOrder, and 30 more. Code that references these types at compile time — declarations, String.valueOf(MyEnum.MEMBER), equality checks, switch over enum values — now resolves locally instead of erroring on an unknown identifier.
  • Corrected member list on ConnectApi.MlRetrieverCitationConfigurationModeEnum. An earlier draft carried the canonicals from a sibling enum (DISABLED / ENABLED); the spec values are CUSTOM / DEFAULT. Customer code that switches on this enum will now see the right shape.
May 17, 2026
0.3.1
Added
  • Slack namespace support. Apex code that builds Slack Block Kit payloads, fires Slack.PostMessageAction, handles slash commands and interactive components, or reads Slack.UserInfo / Slack.ConversationInfo describe shapes now runs end-to-end against Nimbus's local stubs. Slack integrations no longer need a connected app or a live workspace to test.
  • ConnectApi surface broadened across 13 sub-clusters. Managed Content (variants, versions, status enums), Chatter Messages, Files (preview, sharing, delimiter, import status), Errors, Mentions / Communities / Records, Topics, Record Fields, Datacloud, Action Links, Wishlist, Recommendations, Chatter Groups, and Chatter Users. Tests for Connected Apps and Experience Cloud surfaces that touch the ConnectApi REST layer now have substantially more of the namespace available locally.
  • Closed remaining System.* gaps. Type metadata, lightweight introspection, and runtime info round out the core platform surface so it's fully resolvable without leaning on the generic fallback path.
Changed
  • SObject field keys are now lowercased everywhere they're stored. Apex treats field names case-insensitively at the call site (acc.Name and acc.NAME are the same slot), but pre-seeded shells inside Nimbus had been storing some fields in their declared case — so a read after an assignment occasionally missed because the two paths normalized differently. All field storage now goes through a single lowercased key, matching what Apex does.
  • Fresh SObject and EnumValue slots default to their platform-correct zero values. Reference and Datetime fields default to null, Boolean to false, and Integer / Long to 0 — so customer code that guards on obj.SomeField != null, !obj.SomeBool, or obj.Count == 0 short-circuits the way it does in the platform instead of seeing a sentinel value from an uninitialized slot.
May 12, 2026
0.3.0
Added
  • nimbus app — local dev server for Salesforce Multi-Framework React+Vite bundles. Replaces sf ui-bundle dev for offline development. Mounts your ui-bundles/<name> source at http://localhost:<port> with Vite's HMR, and serves the Salesforce API surface your bundle calls into locally against Nimbus's embedded Postgres — no scratch org, no proxy, no internet round-trip. Sub-commands: list, schema, build, test, preview; --all serves every bundle in the project simultaneously.
  • The local API surface is version-agnostic — any /services/data/vXX.Y/ path works. Implemented: GraphQL UIAPI (queries, mutations, introspection, aggregates, cursors), UI API REST (object-info, records CRUD, picklist-values, list-ui, MRU, related-list, layout, record-defaults, record actions, duplicates), Composite API (composite / batch / tree / sobjects), Bulk API 2.0 (ingest + query lifecycles), Search / SOSL, Limits, OIDC userinfo, Connect / Chatter (users/me, feeds, communities, CMS channels), Apex Invocable Actions, and ContentVersion multipart upload. Whatever your bundle hits, it lands on the same local data your tests use.
  • The nimbus serve live dashboard at /__nimbus/dashboard is reused by nimbus app. Inspect the same database, replay the same requests — one mental model across both commands.
Fixed
  • Long-running nimbus serve sessions no longer surface driver: bad connection errors after idle periods. The server was opening a fresh per-request transaction against the embedded Postgres on every call, which the pool would eventually time out and close mid-request. The server now reuses the live interpreter's connection pool — the same path the test runner already used — so connections survive idle stretches and reconnect cleanly when the pool prunes a stale handle.
May 11, 2026
0.2.4
Changed
  • Platform-fidelity compat warnings are now scoped to the code your test actually exercises. Previously, nimbus test emitted compile-time-rule diagnostics for every class in the project at registry-load time — including classes the test never touched — and under the default nimbus.compat.mode=strict would fail the run because of violations in unrelated code. Diagnostics are now deferred and surface only when the interpreter actually invokes the offending class (static call, instance construction, or test-method entry). The strict-mode exit gate fires for the same scope. Project-wide compat audits stay in nimbus validate for explicit "scan the whole repo" workflows.
May 10, 2026
0.2.3
Changed
  • Code coverage and governor limit enforcement are now Free-tier features. nimbus test --coverage with console, JSON, and HTML output runs on every install — no license required. Governor limit enforcement (SOQL/DML/query-row counters, strict/warn/off modes) was already part of the core runtime and stays free. The Pro tier focuses on developer-productivity features (background daemon, parallel execution, watch mode, debugger, mutation testing, VSCode coverage gutters); JUnit XML and Cobertura XML output remain Team-tier for CI integration. The principle: Free lets you trust the result, paid tiers make you faster.
May 10, 2026
0.2.2
Added
  • Nimbus writes manifest/nimbus-missing.xml whenever a test run surfaces missing metadata. The file is a standard SFDX manifest you can hand to sf project retrieve start --manifest manifest/nimbus-missing.xml -o <alias> to fetch every missing item in one command. The filename is distinct from package.xml so it never clobbers a user-managed deployment manifest. --fetch-missing now drives a single sf invocation internally — replacing up to four per-category calls with one — so retrievals are faster and immune to argv length limits on projects with many missing items.
  • Custom Metadata Types are now detected as missing when the type schema isn't deployed locally. Previously, a SOQL query against a __mdt table that wasn't in the project source returned an empty list silently — the test would pass on data that didn't actually exist. The detector now surfaces the missing type in the run summary and (with --fetch-missing) retrieves both the source XML and the runtime JSON describe in one pass, so a re-run sees the type and its records.
Fixed
  • --fetch-missing no longer redundantly re-fetches items the org doesn't have. The previous retry loop attempted up to three retrieve passes for the same set of names; if the org didn't have them on the first attempt, the second and third were guaranteed to fail too — burning ~10–30s per pass on a real sf call. Two root causes: sf project retrieve can return success with empty result arrays (when none of the requested items exist) or with entries marked state: "Failed" in the response — both shapes were being miscounted as successful retrievals. The retriever now parses sf's response correctly, surfaces the per-file failure messages to the user, and the runner remembers which entries it has already attempted so the same name is never re-fetched within one run.
  • The Custom Metadata Type manifest mapping would have been rejected by sf. CMDT entries were emitted under <name>CustomMetadata</name> with bare type names — but that manifest type expects record-level members in Type.RecordName form. Type schemas live under <name>CustomObject</name> in package.xml, with the __mdt suffix on the member name as the signal to sf. The mapping is now correct, so the auto-generated manifest round-trips cleanly through sf project retrieve.
May 9, 2026
0.2.1
Fixed
  • A static-property getter on one class can now read another class's same-named static property. The recursion guard that stops X { get { … X … } } from looping was keyed on the field name only — so when class A's getter for FOO read class B's FOO, B's getter was short-circuited and returned its raw (uninitialized) backing storage instead of running. Nebula Logger v4.18's new declarative ignored-class config (LoggerStackTrace.IGNORED_APEX_ORIGINS chains into LoggerParameter.IGNORED_APEX_ORIGINS) was the visible breaking case — the ignored-class list came back empty regardless of the configured custom metadata. The marker is now scoped per (class, field), so cross-class reads land on the real getter.
  • SOSL RETURNING blocks now honor their WHERE / ORDER BY / LIMIT filters when running against Test.setFixedSearchResults. The fixed-results stub previously returned the raw Id list verbatim, ignoring whatever filter the caller had appended inside the RETURNING parens. Strict-search patterns (FIND … RETURNING LogEntry__c (Id WHERE RecordId__c = :recordId)) couldn't exclude records that didn't match. The stub now translates the RETURNING spec into an equivalent SOQL query scoped to the fixed Ids and runs it through the normal SOQL evaluator, so bind variables and platform filter semantics apply.
May 9, 2026
0.2.0
Added
  • nimbus mcp exposes Nimbus to AI coding agents. A Model Context Protocol server that lets Cursor, Claude Code, and other MCP-aware agents run tests, list test classes, fetch coverage, and surface failures the same way you would from the CLI — so agents can iterate on Apex code without leaving the editor.
  • nimbus skills manages installable agent skills. Skills are bundled task playbooks (list, add, remove, path) that an agent loads on demand. Ships with the apex-coverage-uplift skill, which reads Nimbus's per-line coverage and drives an agent to write targeted tests for the highest-leverage gaps rather than synthetic call-only tests.
  • The MCP list_test_classes tool now returns file paths alongside class names. Saves a round-trip when an agent needs to open the source.
Fixed
  • nimbus upgrade no longer clobbers Homebrew and Scoop installs. An in-place binary swap was wrong for package-manager installs — brew and scoop track the installed version in their own metadata, and any subsequent brew or scoop command would revert the swap, so the user appeared to upgrade and then snapped back to the old version. Nimbus now detects the install source from the resolved binary path and routes you to brew upgrade nimbus or scoop update nimbus. The passive "new release available" notice and the VS Code extension's update prompt use the same detection.
  • After a successful upgrade, Nimbus warns about stale duplicates on PATH. If a second nimbus binary lives elsewhere on PATH (e.g. a leftover ~/go/bin/nimbus or ~/.local/bin/nimbus from a prior install method), nimbus upgrade now lists those paths so a fresh shell doesn't silently fall back to an older copy.
May 8, 2026
0.1.61
Changed
  • (Id) "15char" casts no longer expand to 18-char form. The expansion was added to support a single use case (case-only-different 15-char Ids producing distinct 18-char forms for unique-constraint purposes) but breaks every Map<Id, V> and Set<Id> keyed off raw 15-char record Ids — the cast site is too far upstream to keep collections in agreement, and a half-dozen frameworks (apex-test-kit's mock relationships, apex-fp's pluckIds/groupBy, forceea's data factory) regressed once a cast Id no longer matched its raw counterpart. The unique-constraint path needs to be solved at the SOQL/DML layer instead.
May 8, 2026
0.1.60
Fixed
  • SObjectType.getDescribe().getName() now returns the proper API casing regardless of how the type was reached. Previously, calling Account.SObjectType.getDescribe().getName() returned "Account" while account.getSObjectType().getDescribe().getName() on a queried record returned "account", breaking frameworks (fflib UoW) that key a Map<String,V> by the result and look it up the same way. The dispatcher now normalizes the lookup name through the schema provider so both paths agree.
  • Typed Map<Id, V> map literals lowercase their keys. Apex Id values are case-insensitive, but the literal initializer was using the raw key string. A subsequent .get(otherId) would compute the lowercased key and miss, returning null even when both Ids referred to the same record. Map literal evaluation now mirrors what put() does for Map<Id, V>.
Changed
  • Internal: platform-special maps now self-identify as case-insensitive. The schema describes (fields.getMap(), getGlobalDescribe(), RecordTypeInfo maps), OrgLimits.getMap(), HTTP headers, QueryException.getInaccessibleFields(), and SObject.getPopulatedFieldsAsMap() now carry an explicit case-insensitive flag. No behaviour change for users — regular Map<String, V> still defaults to case-insensitive lookup as before — but it sets up the correct foundation for the planned switch to spec-correct case-sensitive defaults.
May 8, 2026
0.1.59
Fixed
  • JSON.serialize(Map<String, V>) emits keys in reverse insertion order, matching Salesforce. Verified live across maps of size 1, 3, 5, 10, and 20: SF's JSON.serialize iterates string-keyed maps in the exact reverse of put()-order. Nimbus was emitting forward insertion order, which broke any test asserting on full-string JSON output (DLRS's RollupEditorController.testValidateRollupConfig). The fix also covers nested maps inside lists, SObjects, and custom objects — those previously fell through to Go's encoding/json map encoder, which alphabetizes keys, so they got the wrong order even when the top-level value was already correct.
May 8, 2026
0.1.58
Fixed
  • COUNT-only aggregate queries track one row per result group. Salesforce's Summer '18 governor accounting tracks one query row per group for COUNT() / COUNT(field) aggregates regardless of how many underlying rows contributed; SUM / AVG / MIN / MAX still bill per underlying row. Nimbus was applying the underlying-row count uniformly, over-counting tests that asserted on the platform's post-Summer-'18 COUNT semantics. The dispatcher now detects COUNT-only aggregates and uses the result-row count.
May 8, 2026
0.1.57
Fixed
  • Child subqueries resolve multi-level relationship paths. A subquery that selects a grandparent field ((SELECT Id, PricebookEntry.Product2.Name FROM OpportunityLineItems)) was projecting only the first-level FK column and dropping the grandparent reference, leaving the in-memory relationship null and causing NullPointerException on access. The dispatcher now recurses through dotted segments — fetches each level's parent rows in turn and chains them onto the SObject so oli.PricebookEntry.Product2.Name resolves correctly.
  • Subquery ORDER BY drops cross-relationship terms instead of failing the query. A subquery sorted by a grandparent field (ORDER BY PricebookEntry.Product2.Name) couldn't resolve against the flat single-table SQL nimbus emits and failed with missing FROM-clause entry — the subquery returned zero rows even though the FK filter would have matched several. The dispatcher now drops dotted ORDER BY terms and falls back to the default Id ordering when none remain, so the rows still surface (in insert order rather than the requested cross-object order).
May 8, 2026
0.1.56
Fixed
  • TextArea / LongTextArea fields trim trailing whitespace on direct field assignment. The platform strips trailing whitespace when saving values to TextArea / LongTextArea / Rich Text fields. Patterns like logEntry.ErrorMessage__c += someMsg + '\n'; (DLRS's rollup-error logger) accumulate values terminated with newlines, and the matching equality assertions exclude the trailing newline. Nimbus was preserving the value verbatim through the in-memory obj.field = … path, so the assertion failed by exactly one character.
May 8, 2026
0.1.55
Fixed
  • Database.update with allOrNone=false now commits successful records when others in the batch fail. Salesforce processes per-record failures independently — a single validation-rule failure on row 2 doesn't block rows 0 and 1 from committing. Nimbus was running the batch as a single SQL update, so any failure rolled back every record. The partial-success path now retries record-by-record after a group throws so each row's outcome is recorded individually.
  • Database.Error.getMessage() on a partial-success failure now returns just the validation-rule message. The DML exception nimbus throws on validation failure carries the platform-style framing (Update failed. First exception on row 0; first error: STATUS_CODE, message: [field]). Surfacing that whole string through Database.Error.getMessage() broke any framework that concatenates getMessage() + getStatusCode() + getFields() (DLRS's rollup-error logger does this). The dispatcher now parses the platform-format message and exposes the trio (statusCode, validationMessage, fields) on the underlying Database.Error object.
May 8, 2026
0.1.54
Fixed
  • Private OWD sharing now filters records the running user can't see. Custom objects with <sharingModel>Private</sharingModel> in their .object-meta.xml are recorded on the schema, and SOQL run from a with sharing class under System.runAs(testUser) filters records by ownership: rows owned by the running user, plus rows where a matching row in the object's __Share table grants the user explicit access. Earlier nimbus passed every record through regardless of OWD; rollup tests asserting that User B sees only their own children failed because the engine summed everyone's children.
  • Aggregate queries (GROUP BY) now have the OwnerId predicate injected before grouping. The previous WITH SHARING filter skipped queries with GROUP BY entirely on the assumption that the OwnerId column wasn't projected; in practice the predicate belongs in the WHERE so it filters rows before they reach the aggregate. The injection now finds the GROUP BY position and inserts the predicate ahead of it.
May 8, 2026
0.1.53
Fixed
  • StandardController.getId() now returns the 15-character form of the record Id. Visualforce reads the id from the URL id query parameter, which is the case-significant 15-char form. Code that builds a value from standardController.getId() and asserts equality against String.valueOf(record.Id).substring(0, 15) (DLRS's scheduled-rollup cron name pattern) was reading 18-char from nimbus and failing the comparison. The constructor now truncates to 15-char on retain.
  • String equality treats matching 15-char and 18-char Salesforce Ids as equivalent. When an Id-typed field (18-char) is compared to a string sourced from the 15-char form, Apex code historically expects the comparison to succeed since both name the same record. Nimbus was doing strict string equality. valuesEqual now falls back to a 15-char prefix check when both operands are Id-shaped strings of length 15 or 18.
May 8, 2026
0.1.52
Fixed
  • WITH SHARING SOQL no longer injects an OwnerId filter on setup objects. Setup / metadata objects (ApexTrigger, ApexClass, Profile, PermissionSet, __mdt, etc.) have no record-level sharing — they're universally readable subject to object-level CRUD, which is checked separately. Nimbus was treating them like any other queried object, appending OwnerId = '' to the WHERE clause, which either filtered every row out (the records aren't owned by the running user) or failed against tables with no OwnerId column. A trigger-deployed validation rule that queries ApexTrigger from inside a non-admin System.runAs block (DLRS's rollup-summary insert validates that the configured rollup target trigger is deployed) silently came back with zero rows. The dispatcher now skips the OwnerId injection for setup/metadata objects.
May 8, 2026
0.1.51
Fixed
  • SOQL row-count tracking on aggregate queries now matches Salesforce. Apex's Limits.getQueryRows() reports the number of underlying rows that an aggregate (SELECT … GROUP BY or COUNT(field)) consumed; PostgreSQL returns one row per group, so nimbus was reporting the count of grouped result rows. Tests that asserted on the platform's underlying-row accounting (rollup-style verifiers that expect 9 child rows for 3 groups of 3) under-counted by the difference. The dispatcher now runs a SELECT count(*) sibling against the same FROM/WHERE and uses that for the SOQL row tracker.
  • Database.update against soft-deleted records now raises ENTITY_IS_DELETED before validation rules fire. A trigger pattern that deletes a parent record then runs a rollup recalculation needs the update against the now-deleted parent to surface as a deleted-row error so the rollup framework can swallow it. Nimbus was running the update through, firing the validation rule on the recalculated value, and producing a FIELD_CUSTOM_VALIDATION_EXCEPTION instead. The dispatcher now pre-checks soft-delete state and throws System.DmlException with statuscode ENTITY_IS_DELETED before the update reaches the validation pipeline.
  • System.schedule duplicate-name error now matches platform wording. Apex throws System.AsyncException with the message The Apex job named "X" is already scheduled for execution. when scheduling a job whose name already exists. Nimbus was throwing the wrong wording (Duplicate CronTrigger name: X), so any test asserting the exception message via String.equals failed.
May 8, 2026
0.1.50
Fixed
  • SOQL "No such column" errors now match Salesforce wording. When a query referenced a non-existent field, callers caught a System.QueryException whose message read like SOQL execution error: query failed: ERROR: column foo.stage does not exist (SQLSTATE 42703) — fine for debugging, but anything that asserted on the message with String.contains('No such column') failed. The dispatcher now reformats column-not-exist errors to the platform string: No such column 'Stage' on entity 'Foo'. If you are attempting to use a custom field, be sure to append the '__c' …, with original casing recovered from the SOQL source.
  • Custom-object describe field maps now include the IsDeleted system field. Code that walked SObjectType.<X>.getDescribe().fields.getMap() looking for standard audit fields on a custom object came back empty for isdeleted — fflib's fflib_SObjectDescribe wrapper exposes the same map and DLRS's rollup-criteria validation rejected any criteria field that referenced IsDeleted with "Field isdeleted does not exist on the child object." The standard-fields injector now appends IsDeleted alongside CreatedById, OwnerId, SystemModstamp, etc.
  • AFTER UNDELETE triggers now receive the full restored record, not just the Id stub. When undelete was called via Id (Database.undelete(opps[0].Id)) the trigger context was populated with a stub SObject whose Fields map only contained id. Triggers that read foreign keys or aggregated values (rollup recalculators are the obvious case) silently produced wrong results because the lookup field they needed was missing. The handler now re-queries each undeleted record after clearing isdeleted and merges the row into the trigger payload before firing.
  • The (Id) cast on a 15-character string now expands to the 18-character form. Real Apex normalises a 15-character Id to its 18-character canonical form when cast to Id, encoding the original case into the three-character checksum so two 15-char strings that differ only by case map to two distinct 18-char Ids. Nimbus left the value unchanged, so (String)((Id) "Aaa00000000") and (String)((Id) "aAA00000000") produced identical strings — patterns relying on case-insensitive unique constraints over a casted Id incorrectly tripped the constraint.
May 8, 2026
0.1.49
Fixed
  • Database.getQueryLocator now raises System.QueryException on parse errors. The locator was eagerly executing the query but silently dropping any error result, so callers using it as a "validate this string" probe (DLRS's RollupSummaries.onValidate, fflib's QueryFactory validation paths) couldn't catch malformed SOQL via try/catch. The dispatcher now propagates the underlying ThrowValue.
  • String.format now follows Java MessageFormat quoting rules. A single quote starts/ends a quoted region (placeholders inside aren't expanded), and '' represents a literal single quote both inside and outside that region. Templates like 'aaa\'\'{0}\'\'bbb' now correctly produce aaa'x'bbb instead of aaa''x''bbb — matches platform behaviour and unblocks any test that does startsWith on a formatted error string.
May 8, 2026
0.1.48
Fixed
  • SOQL ORDER BY now defaults to NULLS FIRST for both ASC and DESC. Salesforce SOQL treats null as the smallest value regardless of direction; PostgreSQL's default is the opposite (ASC → NULLS LAST, DESC → NULLS FIRST). Tests that omit the explicit NULLS FIRST / NULLS LAST clause and depend on the platform's ordering — common in concatenation rollups — were getting differently-ordered result sets and asserting the wrong concatenated string. The translator now emits the SOQL-default NULLS FIRST when the caller didn't specify one.
  • Custom Metadata Type access checks now grant universal read. X__mdt objects are universally readable on the platform — every profile, including Read Only and Standard User, can read MDT records and fields. fflib's assertIsAccessible() wrapper around an MDT query (used in DLRS's LookupRollupSummary2__mdt selectors) was throwing under any System.runAs(readOnlyUser) context. CheckObjectPermission and CheckFieldPermission now short-circuit to true for read on __mdt suffixed types.
  • Database.undelete now accepts Id and List<Id>. The handler only accepted SObject / List<SObject>; calls like Database.undelete(opps[0].Id) erroried with Cannot undelete *interpreter.StringValue. The dispatcher now resolves the SObject type from the Id's key prefix and synthesises a stub SObject so the rest of the path (savepoint, isdeleted clear, AFTER UNDELETE trigger) is unchanged.
May 8, 2026
0.1.47
Fixed
  • Describe-style methods on Schema.SObjectField now resolve through the field-describe lookup. The token returned by SObjectType.<Sobj>.fields.<Field> is a Schema.SObjectField per platform semantics; Apex routinely accepts the same value where a Schema.DescribeFieldResult is expected (Schema.DescribeFieldResult dfr = Account.fields.Description; followed by dfr.getType()). Nimbus emitted the SObjectField but only routed describe methods (getType, getName, getSObjectType, getLength, isAccessible, …) for the DescribeFieldResult class — calling them on an SObjectField returned null, breaking common rollup-validation patterns. The dispatcher now forwards both classes through the same describe-style lookup so both usage shapes resolve identically.
May 8, 2026
0.1.46
Fixed
  • Test class static {} blocks no longer eagerly execute during class-graph load. When loading the dependency graph for any entrypoint (anonymous Apex via nimbus exec or a focused nimbus test SomeTest), every transitively-pulled-in class's static initializer ran up front. For test classes that's wrong: their static {} blocks routinely insert seed records (Accounts, Contacts, custom-setting rows) that real Salesforce would isolate to that class's test transaction. Running them eagerly landed the inserts in the shared database with no rollback boundary, causing later [SELECT … LIMIT 1] queries to return rows from another test's fixture. The runner now skips static {} blocks for @isTest-annotated programs during eager init; field initializers still run so static fields hold their declared defaults, and the blocks fire lazily on first reference (inside the active test's transaction, where rollback applies).
May 7, 2026
0.1.45
Added
  • Bundled Calendar standard-object describe. SOQL referencing Calendar (commonly via WHERE OwnerId IN (SELECT Id FROM Calendar WHERE Type = 'Public') on Event-related controllers) was failing with "relation calendar does not exist" because the platform's standard Calendar object wasn't in nimbus's embedded describe set. Run nimbus sync to materialise the table in projects updating from earlier versions.
May 7, 2026
0.1.44
Fixed
  • SOQL field NOT IN (…) now matches Salesforce's null-tolerant semantics. The platform treats NULL NOT IN (…) as TRUE — so a row with a null lookup-traversal value (e.g. Lead.ConvertedContact.Status__c when the lead isn't converted) still satisfies the predicate. Standard SQL and PostgreSQL return NULL for the same comparison, which evaluates as false in WHERE, silently dropping the row. The translator now wraps every <lhs> NOT IN (literals) as (<lhs> NOT IN (literals) OR <lhs> IS NULL) so the LEFT-JOINed null falls back to TRUE. Subqueries on the right (NOT IN (SELECT …)) keep their existing semantics.
  • SOQL format(field) [alias] projections now resolve to the underlying column. The translator was leaving the format(...) wrapper untouched, so the SELECT list quoted it as a literal column name ("task"."format(status) statusvalue") and Postgres rejected the query. format() on the platform is a locale-formatting projection for Date/Datetime/Number/Currency fields; nimbus emits the raw column under the requested alias so downstream .get('alias') reads return a sensible value.
  • Bare keyword IS is no longer mis-quoted. The simple-identifier quoting pass treated any uppercase-starting token as a column reference; IS NULL emitted by the new NOT IN wrap (and any user-written same-case form) had its IS stamped as "<table>"."is", breaking the surrounding clause. IS is now in the keyword skip list alongside AND / OR / NOT / IN.
May 7, 2026
0.1.43
Fixed
  • SOQL self-qualified WHERE references no longer produce db.schema.table.column. A WHERE clause like SELECT … FROM Event WHERE Event.StartDateTime != null sent the relationship resolver into the catch-all path because Event isn't in the join map (it IS the main table). The simple-identifier quoting pass then stamped both segments independently and produced "event"."event"."event"."startdatetime", which Postgres rejected as a cross-database reference. The relationship resolver now treats a parent prefix matching the main table as a self-reference and emits a single "event"."startdatetime".
  • SOQL multipicklist INCLUDES/EXCLUDES now translate to PostgreSQL = ANY(ARRAY[…]) / <> ALL(ARRAY[…]). The translation was wired into a legacy WHERE path that was never called from the active translator, so the operator passed through to PostgreSQL untouched. The downstream identifier-quoting pass then matched the bare keyword as a column name and stamped "table"."includes", surfacing as a syntax error pointing at the table identifier. The translation now runs in the live WHERE pipeline, and ARRAY/INCLUDES/EXCLUDES are added to the keyword skip list so subsequent passes don't mis-quote them.
May 7, 2026
0.1.42
Fixed
  • System.SelectOption.getLabel() now returns the stored label. The generic describe-class getLabel() dispatcher returned the receiver's ClassName as a fallback when none of its enumerated classes matched, which shadowed the dedicated SelectOption dispatcher further down — calls of opt.getLabel() on a Visualforce SelectOption were resolving to the literal string System.SelectOption. Visualforce extensions that match CSV header components against field labels via opt.getLabel() failed to map any field; the import flow then quietly skipped its main insert. Fixed by handling SelectOption explicitly in the getLabel case.
May 7, 2026
0.1.41
Fixed
  • Malformed JSON now raises System.JSONException from the parser instead of looping forever. JSON.createParser(s) on a malformed string was returning a parser with zero tokens; consumer code shaped like while (parser.nextToken() != JSONToken.END_OBJECT) { … } would then loop because nextToken() kept returning null and null != END_OBJECT stays true. The parser now captures the underlying tokenize error at createParser time and replays it as a System.JSONException on the first token-reading call (nextToken, getCurrentToken, getText, get*Value, skipChildren) — matching real Salesforce, where createParser is lazy and the failure surfaces on consumption. After the throw the parser transitions to an exhausted state so a subsequent consumeObject call (often called outside the try/catch) doesn't immediately re-throw.
  • while and do-while conditions now propagate ThrowValue. The condition expression was only checked for ErrorValue; a method call inside the condition (e.g. parser.nextToken() raising JSONException) had its ThrowValue fall through to IsTruthy, which returns true for unknown types — running the loop body and re-evaluating the condition forever. Both loops now treat the condition the same way the body already did: throws and errors return immediately so the surrounding try/catch can handle them.
May 7, 2026
0.1.40
Fixed
  • Assigning a SOQL list result to an instance SObject field now auto-unwraps to the first element. Apex semantics: this.profile = [SELECT … FROM Foo__c WHERE Id = :id] assigns a single SObject (the first row) when the field is declared as a singleton, throwing if the result has > 1 row. The local-variable assignment path already coerced via coerceSoqlListToSObject; the instance-field path was storing the raw List<Foo__c> instead, so a downstream this.profile.Bar__c = X later in the same method failed with "UNIMPLEMENTED_ASSIGNMENT" because the receiver wasn't an SObject. The field-assignment path now applies the same coercion.
  • Auto-property field reads no longer break member assignments. Reading an auto-property field (public Foo__c profile { get; set; }) returns the inner value wrapped in a FieldReferenceValue; the profile.Bar__c = X assignment path was matching against the wrapper rather than the underlying SObject and falling through to the catch-all "UNIMPLEMENTED_ASSIGNMENT". The wrapper is now unwrapped before the type-switch.
May 7, 2026
0.1.39
Fixed
  • Datetime ± Integer arithmetic now returns a Datetime offset by N days. system.now() - 10 previously errored with "Unknown operator". Apex semantics: integer addition/subtraction on a Datetime advances/regresses the value by that number of days (matching Date). Both Datetime + Integer and Datetime - Integer are now handled, preserving the operand's IsGmt flag so subsequent comparisons stay on the correct reference frame.
  • Limits.getFindSimilarCalls() / getLimitFindSimilarCalls() are now defined. The legacy duplicate-management counter was missing; codebases that defensively read every governor counter via reflection or that simply log the full set hit "Unknown method" when they touched it. Stubbed to 0 / 3 to match the platform shape.
May 7, 2026
0.1.38
Fixed
  • Inline // … comments inside SOQL brackets no longer truncate the query. The translator was stripping Apex line comments AFTER collapsing whitespace; once newlines became single spaces, the regex //[^\n]* happily ate everything from the first // through to the closing bracket. Multi-line SOQL with field-trailing annotations dropped most of the SELECT list and arrived at the DB as a truncated query. Comment stripping now runs first.
  • Database.executeBatch now writes the AsyncApexJob row shape Salesforce actually produces. Real SF emits one parent BatchApex row at executeBatch time plus one BatchApexWorker row per chunk processed; nimbus was emitting only a single row with one or the other JobType. The parent row is now inserted on enqueue and worker rows per scope iteration in executeBatchLifecycle, so empty-input batches produce one row (parent only) and chunked batches produce one + N.
  • Lookup FKs are restored on undelete. The 0.1.36 fix that "preserved FKs across soft delete" was wrong about platform behaviour — Salesforce DOES null SetNull child FKs at soft-delete time (visible to SOQL between delete and undelete) and then restores them when the parent is undeleted from the recycle bin. Soft delete now snapshots and nulls the FKs; undelete repoints them at the now-restored parent.
May 7, 2026
0.1.37
Fixed
  • Invalid relationship paths in SOQL once again raise System.QueryException. The 0.1.36 chain-break that suppressed broken JOINs for the parent-to-child WHERE pattern (WHERE Contacts.Individual.Id = …) was too eager: it also swallowed genuinely invalid field paths like UserLicense.InvalidPath.Name, where the first segment resolves but a later one doesn't. The translator now silent-breaks only when the depth-0 segment matches a known child-relationship name (handled later as a semi-join); every other unresolved segment falls through to the broken-JOIN path, so the downstream SQL error is wrapped to System.QueryException at the SOQL boundary.
May 7, 2026
0.1.36
Fixed
  • Salesforce-managed implicit defaults now apply on insert (Account/Contact/Lead.CleanStatusPending). The platform stamps these on insert even though the field describe lists no defaultValue; nimbus stored them as null, which broke any rollup or evaluator that COUNTs over CleanStatus through a one-to-many path. Verified by running anonymous Apex against a real org: insert new Account(Name='X') followed by SELECT CleanStatus returns Pending. Structured as a per-SObject map so future implicit defaults drop in without touching call sites.
  • SOQL DAY_ONLY() now uses GMT semantics, matching the platform. Salesforce extracts the date from the stored UTC instant; nimbus was using the raw locally-stored components, so a CaptureDate at local 23:59:59 was incorrectly matched as today by DAY_ONLY = TODAY. The translator now wraps the column with AT TIME ZONE before CAST AS DATE, lifting the stored components into a real UTC instant first.
  • Datetime comparisons now resolve mixed-IsGmt operands to a true UTC instant. Datetime.newInstanceGmt(d, t) and Datetime.newInstance(d, t) produce instants offset by the user's timezone; nimbus stored both as raw components, so a GMT-built reference compared against a local-flavoured stored value with raw component equality got the wrong answer for boundary records across midnight. When the operands disagree on IsGmt, each side now resolves to a UTC instant; same-IsGmt comparisons keep the historical component path, so codebases that store and compare exclusively non-GMT values are unaffected. addDays/addMonths/etc. now preserve the IsGmt flag, and Datetime.valueOf(stringWithTzMarker) returns an IsGmt=true result so downstream comparisons stay on the right reference frame.
  • System.Assert.areEqual Datetime semantics now match Datetime.equals (second-precision). The Assert path fell through to a string-form fallback that included milliseconds, so a Datetime that round-tripped through SObject field assignment (which truncates millis to match Salesforce's stored precision) compared unequal to its pre-store twin under Assert.areEqual but equal under System.assertEquals. Apex's documented Datetime.equals() ignores millis; both paths now agree.
  • Lookup foreign keys are preserved across soft delete (recycle bin). Salesforce nullifies SetNull child references only on hard delete; soft delete (default delete records;) keeps them so a subsequent undelete restores the chain end-to-end. Nimbus was nullifying immediately, so delete parentApps; undelete parentApps; left children with permanently null lookups. Cascade-delete (master-detail) is preserved unchanged — those still cascade into the recycle bin and restore on undelete.
  • Set<T> instanceof Set<Object> now respects Apex's generic-parameter invariance. Set<String> is not a Set<Object> for instanceof purposes (it is for assignment, but those are different rules). The earlier permissive behaviour conflated the two and miscategorised typed-instanceof chains that branch on Set<Object> vs Set<SObject>. The legitimate covariance case (Set<Account> instanceof Set<SObject>) is unchanged.
  • Switch when literals now apply leading sign tokens. The Apex grammar permits when -7 as a single literal with a SUB child; the tree walker was reading only the integer part, so when 4, 5, 6, -6, -7, -8 silently collapsed to when 4, 5, 6, 6, 7, 8. Codebases that map negative offsets through Math.mod got off-by-one fiscal-month routing.
  • Async work queued without a matching Test.stopTest is now discarded between test methods. Real Salesforce tears down each test method's queued Queueables/Schedulables/Future jobs/Platform Events at method boundary; nimbus was carrying them on the shared interpreter, so a stale Schedulable from test N could fire during test N+1's Test.stopTest drain — running with whatever static state happened to be in scope. The runner now resets these queues alongside ClearAllStaticVars at the start of every test method.
  • SOQL COUNT() with LIMIT N now caps the returned count at N. Salesforce semantics: SELECT COUNT() FROM X LIMIT 1 returns 0 or 1, never the full row count. PostgreSQL applies LIMIT to the result set (always one row for a count-only query), so the standalone clause was a no-op. The translator now folds the limit into the projection via LEAST(COUNT(*), N) when the SELECT is a single count and there's no GROUP BY.
  • Polymorphic Type filters now recognise JOIN-aliased relationship targets. Grandparent rollups can generate queries like LEFT JOIN account AS what ON … WHERE what.type = 'Account' — the join alias is named after the relationship (what), not the table (account). The earlier polymorphic-Type-tautology pass only stripped the redundant filter when the alias name matched the literal directly. It now also accepts matches where the alias resolves through the joins map to a table matching the SObject literal.
Added
  • Parent-to-child WHERE predicates are rewritten as semi-joins. Some real-world rollup paths emit SOQL like WHERE Contacts.Individual.Id = :recordIds — strict bare SOQL doesn't permit child-relationship traversal in WHERE, but the platform appears to silently rewrite to the equivalent EXISTS shape. The translator now does the same up front: id IN (SELECT AccountId FROM Contact WHERE Individual.Id = :recordIds). Operators supported: =, !=, <>, IN, NOT IN.
May 1, 2026
0.1.35
Added
  • toLabel(picklist) in SOQL now returns the picklist's display label. Salesforce's toLabel(Field__c) returns the human-readable label (e.g. Long Range) rather than the stored API name (Long_Range). The translator was stripping the wrapper and projecting the raw column, so tests asserting against labels passed in the org but failed locally. Per-value labels are now captured in FieldSchema, propagated through the SFDX scanner, embedded JSON describe, and source cache; the translator emits a CASE expression mapping API names to labels, aliased to the underlying column. Falls back to the prior raw-value behavior when labels are unknown (global value sets, missing schema), so queries that already worked still do.
May 1, 2026
0.1.34
Fixed
  • Unlinked __r placeholders now compare equal to null. The Apex pattern if (rec.Rel__r != null) { …rec.Rel__r.field… } was failing locally because the interpreter returned an unlinked SObject placeholder for __r reads on records with a null foreign key — so the guard fell through and the chained access blew up. Equality (== / !=) now treats these placeholders as null while preserving safe field reads on the same value, matching real Salesforce semantics.
May 1, 2026
0.1.33
Fixed
  • Test.loadData now coerces CSV cells to typed Apex values. Every cell was previously stored as a String regardless of the SObject's field schema — so test code that did acc.AnnualRevenue.intValue(), lead.IsConverted == true, or opp.CloseDate.year() on loaded records blew up because the value was a String, not a Decimal/Boolean/Date. The CSV reader now consults FieldSchema.Type per column: numeric fields parse to Integer/Decimal, checkboxes to Boolean, date/datetime via the same flexible parser the rest of the interpreter uses, and blank cells become null (matching Apex's blank-cell semantics). Reference fields and unknown columns fall back to String.
May 1, 2026
0.1.32
Fixed
  • Auto-generated stubs are now folded into the AST cache and the daemon's program map. nimbus stubs auto and nimbus test --write-stubs previously wrote new stubs/<Name>.cls files to disk but left them outside the runner's in-memory state, the consolidated AST cache, and any running daemon — so the next test run paid a re-parse cost, and a daemon serving LSP/IDE features stayed blind to the new classes until restart. The writer now hands new files back to the runner: programs map and class registry are mutated in place, .nimbus/cache is updated, and the daemon (if attached) is invalidated explicitly. The daemon also watches stubs/ on its filesystem watcher and pre-parses it at startup, so file events from any source — CLI, LSP, hand edits — flow through without a restart.
April 30, 2026
0.1.31
Fixed
  • nimbus mutate now loads nimbus.properties for the baseline pass. The mutate command built its test runner from CLI flags only, skipping the project-config block that nimbus test uses — so seeded labels, mock profiles, stub namespaces, org config, and a dozen other fields were silently zero-valued during the baseline. Projects that override custom labels via nimbus.seed.label.* (or rely on any other nimbus.properties seeding) saw baseline has 1 failing tests — fix them before mutation testing on tests that pass cleanly under nimbus test. Mutate now mirrors the test runner's config loading so the baseline matches.
April 30, 2026
0.1.30
Changed
  • Free tier now re-syncs org schema before every nimbus test. A green test built on stale schema is the worst kind of false confidence — pass locally, fail on deploy when a custom field added in the org wasn't in the local DB. Free now pays the sync cost up front so the result is honest. Pro keeps the existing first-run-only behavior because the daemon owns sync lifecycle and keeps state warm; trigger an explicit refresh with nimbus sync when needed. The setup output prints a one-line hint so the speed cost lands paired with the upgrade path.
Fixed
  • nimbus upgrade works on Windows. The self-update path assumed every release artifact was a .tar.gz with a bare nimbus binary inside; on Windows the asset is a .zip containing nimbus.exe. Upgrade now extracts both archive types and matches the OS-correct binary name, so nimbus upgrade from a Windows shell completes instead of erroring with binary not found in archive.
April 29, 2026
0.1.29
Performance
  • Windows now gets the same PostgreSQL tuning macOS and Linux do. An upstream quoting bug in the embedded-postgres library prevented our perf flags (shared_buffers=512MB, jit=off, fsync=off, autovacuum=off, etc.) from reaching pg_ctl on Windows — postgres booted with conservative defaults instead, leaving warm test runs 2-5× slower than the Unix path. We now apply the same parameters via ALTER SYSTEM + a quick restart, so Windows users get near-parity warm-run performance with Mac and Linux. CI on windows-latest verifies the tuning actually lands via SHOW assertions on every PR.
April 29, 2026
0.1.28
Fixed
  • Embedded PostgreSQL now works on native Windows. First-run initdb previously aborted at the post-bootstrap stage on Windows because of a path-layout mismatch between the bundled binary and the extension DLLs. The extractor now lifts dict_snowball.dll, plpgsql.dll, and friends to the directory the Windows binary expects, and we work around an upstream quoting bug that caused pg_ctl to reject our perf-tuning flags. No first-launch download required — the bundle ships in the binary and just works.
  • Daemon, named-pipe IPC, and AST/label caches verified end-to-end on Windows. Three new integration tests — embedded postgres lifecycle, daemon transport, and Pro-tier cache machinery — run on every PR against a real windows-latest runner, so the Pro experience won't silently regress on Windows.
April 29, 2026
0.1.27
Added
  • Native Windows support. Nimbus now runs on Windows without WSL. The daemon uses Windows named pipes for IPC, detaches cleanly from the parent shell without flashing a console, and ships as a real nimbus-windows-amd64.exe binary alongside the macOS and Linux builds.
  • Cross-platform CI. Every PR now builds, vets, and tests on macOS, Ubuntu, and Windows runners — so the Windows experience won't silently regress.
April 29, 2026
0.1.26
Fixed
  • Fewer background network requests from licensed CLIs. The activity heartbeat is now throttled to once every 24 hours instead of firing on every invocation. The portal's last used timestamp now updates daily.
April 29, 2026
0.1.25
Fixed
  • Sibling test methods no longer interfere with record-triggered flows. A flow that fired during one test method could be silently skipped on the same record in a later test in the same class, even with a fresh transaction. Affected classes that update shared @testSetup data across multiple methods. Sibling tests now see fresh flow state.
April 29, 2026
0.1.24
Performance
  • Test suites run ~60% faster. No flag, no config change. Same workload, less wall time.
Fixed
  • Spring '26 platform-event and scheduled-job fields added to the standard schema (EventBusSubscriber.LastProcessed / LastPublished, CronTrigger.JobType / CronJobDetailName). Resolves intermittent failures on parallel runs.
April 27, 2026
0.1.23
Added
  • nimbus upgrade — self-update from the CLI. Pulls the latest release tarball from GitHub and replaces the running binary in place. Mirrors the curl install script — same source, same naming. Aliased as nimbus update and nimbus self-update. Use --check to compare versions without installing. (This should have shipped in 0.1.20; better late than never.)
  • nimbus login — browser-based machine activation. Sign in via your browser and the CLI registers itself; no key paste, no config files. Companion commands: nimbus logout frees a seat, nimbus whoami shows account + tier. Existing installs keep working untouched — same 7-day offline grace.
  • Self-service portal at testnimbus.dev/portal. Switch plans with prorated billing, view invoices, deactivate machines, invite team members, manage account + data — all in one place.
  • Per-user machine quota. Each user is capped at 3 active machines, even on Team plans where the underlying pool is larger. Tagged automatically at login.
  • CLI heartbeat. The portal's last used timestamp now reflects actual usage rather than activation date. Async, fire-and-forget; never blocks tests. Skipped in CI.
  • Per-package stub layout. Managed-package stubs are organized one folder per package (stubs/Nebula/, stubs/fflib/, …) with optional objects/ subdir for namespaced custom objects. Walkthrough in berlinbrew-demo.
Changed
  • nimbus license activate / deactivate / status are removed in favour of nimbus login / logout / whoami. NIMBUS_LICENSE_KEY still works in CI and AI-agent environments. Existing installs keep working until reinstall.
Fixed
  • Re-activating a machine after a Nimbus reinstall (same fingerprint, fresh local state) now recovers the existing Keygen activation instead of erroring with machine already activated.
April 26, 2026
0.1.21
Added
  • apiVersion validator. A class compiled at apiVersion=52.0 can't use a v66 feature like Database.getCursor or Blob.toPdf — Salesforce eventually rejects the deploy, but the discovery loop is slow. Nimbus now catches it before the deploy. The check reads each *.cls-meta.xml for the declared apiVersion, looks up every referenced symbol against a per-version catalog, and emits a warning when the symbol's introduction version is newer than the class's. Surfaces in three places:
    nimbus validate — inline alongside parser errors.
    apex-version-lint — standalone CLI for CI gates. Add -strict to exit non-zero on any finding.
    VSCode — inline squiggles via the LSP. Editing the meta.xml to bump apiVersion clears the squiggle immediately (no need to re-save the .cls).
  • Catalog: v31.0 → v67.0. ~1000 symbols from every Apex release between Summer '14 and Summer '26, sourced from Salesforce release notes. Catalog policy is GA-only — Beta / Pilot symbols are listed at their GA version, not their first appearance. Per-overload arity tracking distinguishes Database.query(String) (baseline) from Database.query(String, AccessLevel) (v57) so the validator doesn't false-positive on the older overload.
  • Per-line suppression. // nimbus:ignore on the same line or the line directly above suppresses the apiVersion warning. Add a comma-separated rule list to suppress only specific rules. Bare // nimbus:ignore is a catch-all for everything Nimbus emits.
  • Type-position checking. The validator flags too-new types in declarative positions, not just call sites. A v52 class declaring System.Domain d as a parameter, local, field, cast target, or generic type argument now warns on the type itself — same diagnostic the call site would produce, attributed to the declaration line.
  • Missing-meta.xml warning. A .cls without a sibling *-meta.xml silently disables apiVersion checks. VSCode now flags the file at the top so the user knows the gate isn't running. A meta.xml that exists but omits <apiVersion> gets a softer hint (the file is still deployable with Salesforce's default).
  • ~150 new Apex symbols across v31 → v65. Backfills the long tail between the runtime's baseline and the recent (v66/v67) headline additions. Highlights:
    Canvas.Test.mockRenderContext + Canvas.RenderContext / Canvas.ApplicationContext getters (v31).
    System.SandboxContext getters (v36), Metadata.DeployContainer.addMetadata + DeployCallbackContext.getCallbackJobId (v40).
    ConnectApi.NextBestAction.executeStrategy (v45), Formula.recalculateFormulas returning one FormulaRecalcResult per record (v47).
    Auth.VerificationPolicy.HIGH_ASSURANCE + UserManagement verification methods (v49–v50), Messaging.CustomNotification setters + send() (v50).
    Database.LeadConvert person-account fields + Setting__mdt.getInstance(QualifiedAPIName) (v51), B2B Commerce ConnectApi.* stubs + Messaging.SingleEmailMessage.setRelatedToId (v52).
    functions.FunctionInvocation getters (v53), System.Domain getters + DomainParser.parse overloads (v54).
    Invocable.Action.createCustomAction(type, ns, name) + getName / getNamespace / isStandard / setInvocationParameter (v56).
    System.Label.translationExists + ConnectApi.NamedCredentials.create / get / delete (v58), System.Collator + DataWeave.Script.createScript(ns, name) (v59), EventBus.TriggerContext.getRetries / lastError / setResumeCheckpoint (v62), FormulaEval.FormulaInstance.getReferencedFields (v63).
Fixed
  • Database.getCursor now returns a Database.Cursor (the v66 paginated cursor) instead of falling through to Database.QueryLocator. Apex code following the official docs (Database.Cursor c = Database.getCursor(query); c.fetch(0, 10);) was crashing with Unknown method fetch on object Database.QueryLocator.
  • FormulaRecalcResult.isSuccess() returns the stored success flag instead of always false. Same fix for getSObject() and getErrors() — all three now read from the result actually populated by Formula.recalculateFormulas.
  • __mdt.getInstance(key) accepts the record Id and QualifiedAPIName forms added in Spring '21 (v51), not just the legacy DeveloperName. Setting__mdt.getInstance('MySetting.Alpha') now resolves; previously it returned null.
  • Auth.VerificationPolicy.valueOf("HIGH_ASSURANCE") now returns the enum value instead of throwing NullPointerException.
  • Metadata.DeployContainer.getMetadata() and similar season-specific accessors no longer get shadowed by a generic getter fallback that returned null. Affects every namespace stub class added in this release.
April 23, 2026
0.1.20
Added
  • The compatibility release. Broad interpreter, SOQL, and DML fidelity work to bring Nimbus closer to real Salesforce behavior across the board.
  • nimbus serve — local Salesforce Pub/Sub API (Pro). gRPC endpoint with full Avro encoding, auth headers, replay-id support, and keepalive. Point CometD- or EMP-style consumers at localhost and develop platform-event flows with no org. Backed by 26 new exception types, 7 enums, and 15+ stdlib classes. See the Local Server page for setup.
  • DAP debugger. Step, breakpoint, and inspect locals from any DAP-aware editor (VSCode, JetBrains, Neovim).
  • --assign-perms on nimbus test grants permission sets to the running user before execution.
  • --quiet flag, transparent parallel gating, and honest defaults — nimbus test no longer over-promises about parallelism when the workload can't take advantage of it.
  • Strict validator and governor defaults. Limits are enforced by default to match real Salesforce; opt out per-test with config keys when you need looseness.
  • 25+ new standard SObject schemas shipped in stubs/: Site, SiteDetail, ContactPointEmail, ContactPointAddress, ContactPointPhone, ContactPointConsent, OperatingHours, DandBCompany, CallCenter, Individual, OpportunityContactRole, EntityDefinition, BusinessHours, TaskStatus, CaseStatus, Event. Embedded standard SObjects in .nimbus/schemas/ are now auto-recognized.
  • Classic .object metadata loading: inline record types, picklist values, and child relationships from old-style metadata files. CumulusCI unpackaged/ directories are scanned for record types.
  • MDAPI files: .labels and .translation are loaded. The labels parser tolerates bare <labels> fragments that ship without the <?xml?> header or <CustomLabels> wrapper.
  • nimbus.seed.label.<FullName>=<value> overrides a Custom Label's resolved value at runtime. Use when a label's source-of-truth lives in the deployed org (e.g. an admin-set environment-specific profile Id that ships as PLACEHOLDER in the repo).
  • nimbus.seed.group.<Name>=<Type> seeds standing Public Groups into the database. For regional or team groups that admins create once in the org and user-trigger automation queries by name — not usually checked into repo metadata. Deterministic 00G-prefix Id from the name.
  • Test.loadData defaults OwnerId to the running user when the CSV omits it, mirroring what Apex DML does on insert.
  • New system APIs: System.Version, Test.testInstall / InstallContext (managed-package upgrade simulation), Approval.lock / unlock with real LockResult / UnlockResult, System.SelectOption VF class, Schema.DescribeFieldResult.getController() for dependent picklists, Schema.SObjectField.getPicklistValues(), Network stubs (Communities/Experience Cloud), Database.Cursor.fetch(position, count), Metadata.* deploy-container ops (stubs), Messaging.SendEmailResult.isSuccess(), ApexPages.StandardController stub.
  • VSCode 0.1.20: coverage gutter shows a left-border indicator; the extension warns when a newer Nimbus CLI is available.
Fixed
  • SOQL string equality, IN, and ORDER BY are now case-insensitive for string-typed fields, matching Salesforce behavior. Same for __mdt.getInstance() DeveloperName lookups.
  • RecordBeforeSave flows now fire BEFORE Apex before-triggers, matching Salesforce's documented order of execution. Previously reversed, which let a trigger overwrite a field on stale state before a downstream flow could read it (e.g. a trigger flipping Lead.Status back to Open before a flow filtered on Status = Unqualified). Flow $Record writes now also sync back onto the save unconditionally — nimbus.compat.flow-record-sync is removed and the Salesforce-truthful ordering (flow writes → before-triggers → validation rules) is the default. Previously gated flag-off suites may surface validation-rule failures that the old path was hiding; those are real bugs on the platform too.
  • EncodingUtil.base64Decode now strips whitespace from the input, matching Java's MIME base64 decoder (what Salesforce uses). Fixes round-trips through urlDecode (which turns + into space under form-encoding) for ContentVersion upload flows and similar.
  • Schema.getGlobalDescribe().get() is lenient about trailing whitespace in the lookup key, matching Salesforce. Fixes code that reads an SObject name from a Text custom field (not auto-trimmed) and passes it straight to getDescribe().
  • Flow-to-interpreter value conversion handles Date and Datetime. Previously they fell through to the default branch and silently became null, which dropped $Record.SomeDate__c = Now writes from RecordBeforeSave flows.
  • SOQL bind :sobj.Id falls back to the struct-level Id when the Fields map doesn't contain an "id" key. Some paths (notably Test.loadData) populate only the struct Id, so WHERE filters that bound :c.Id silently matched zero rows even when the record was present in the DB.
  • Custom-metadata-type relationships: __r returns null when the FK is null (instead of an empty SObject); EntityDefinition / FieldDefinition FKs resolve end-to-end; multi-line MDT SELECT parses cleanly; dynamic __mdt queries count toward Limits.getQueries(); SELECT COUNT() on __mdt returns Integer.
  • DML semantics: AFTER-INSERT / AFTER-DELETE addError() now rolls the save back; partial-success Database.delete / update respects per-record addError; partial-success unique-constraint violations become failed SaveResults; heterogeneous List<SObject> updates dispatch per-type; Database.merge(master, Id) properly deletes the duplicate; required-field validation on custom __c insert; restricted picklist values enforced.
  • Trigger correctness: trigger-throw DmlException preserves the original stack; trigger body pushes a stack frame so traces name the trigger; before-insert mutations to Name don't leak back to the caller's local; the Trigger.new copy barrier matches Salesforce.
  • Exception handling: stack trace preserved on re-throw of caught exceptions; NPE message format matches Salesforce's bare format; user toString returning Apex null stringifies as "null"; exception toString matches the Type:[]: msg shape.
  • Type system: Date / Datetime / Time casts throw TypeException for incompatible sources; Decimal preserves explicit scale from setScale; List and Set match instanceof Iterable<T>; super() chain terminates cleanly at built-in Exception; Type.forName for List / Set / Map and primitives; Set<T>.add / contains / remove honor user hashCode / equals; subclass static method doesn't shadow ancestor instance method; inner-class shadowing of standard SObjects is now scoped.
  • JSON: deserialize handles Base64 fields, numeric-offset datetime formats, single-key SObject wrappers, and enum strings, and resolves user-class field types; Map.serialize preserves insertion order and original-case keys.
  • Async / scheduling: System.schedule returns the CronTrigger Id (08e), not the AsyncApexJob Id, and rejects invalid day-of-month/week combos; SchedulableContext.getTriggerId wired up; EventBus.publish increments getDmlRows alongside getDmlStatements and stamps CreatedById / CreatedDate.
  • Profiles & permissions: Minimum-Access profile requires explicit FLS for edit/create; restricted profiles with default CRUD get synthesized grants; with-sharing DML honors record ownership; setup-object auto-grant skipped for minimum-access.
  • SOQL miscellany: child subquery LIMIT honored; subquery LIMIT :bindVar resolves at execute time; aggregate ORDER BY translates SOQL date functions (CALENDAR_YEAR, etc.); trailing AND / OR trimmed cleanly; LAST_N_* / NEXT_N_* / N_X_AGO use calendar boundaries instead of rolling windows; child subquery rows always include Id; parent-relationship SObjects carry QueriedFields from the SELECT, so untouched fields throw on access (matching real Salesforce); :true / :false / :null bind syntax resolves to literal constants.
  • Decimal.valueOf(Integer) returns the value instead of 0.
  • Schema casing: standard fields get English labels; custom SObject names preserve API casing; getPopulatedFieldsAsMap returns PascalCase keys.
April 17, 2026
0.1.19
Added
  • Nimbus Language Server (LSP 3.17) on stdio. nimbus lsp starts a server every LSP-aware editor can launch — VSCode, JetBrains, Neovim, Zed, Helix, Emacs. 20 advertised capabilities: schema-backed completion (71+ real Account fields from .nimbus/schemas/, no org round-trip), live SOQL column validation, hover with mutation-score and flaky-rate overlays, signature help, go-to-definition / typeDefinition / implementation / declaration, find-all-references, rename, workspace symbol search, document symbols, document highlight, folding and selection ranges, semantic tokens, call hierarchy, code actions (SOQL typo auto-fix, generate test stub with schema-aware required-field constructors, extract-to-@TestSetup, add-@IsTest), inlay hints (coverage hit counts, surviving mutants, flaky warnings), code lenses (Run / Debug / Coverage / View Trace / Mutate), opinionated conservative formatter, 12 Apex snippets with Tab-through placeholders. See the Language Server page for per-editor setup.
  • VSCode extension auto-launches the LSP when the extension activates; gated on nimbus.lsp.enabled (default on). Disables the extension's legacy code-lens provider when LSP is active so Run Test / Debug / Coverage buttons don't double up.
  • New CLI flags on nimbus test: --dry-run / -n, --sandbox, --feature MultiCurrency,PersonAccounts, --faketime 2030-01-15T12:00:00Z, --timeout N, --readonly, --profile ci. All seven also available as nimbus.properties keys under nimbus.org.* and nimbus.test.* with CLI winning when both are set. --profile is a persistent root flag — activates Quarkus-style %ci.nimbus.foo=bar profile overrides for every subcommand in the same invocation.
  • nimbus stub list / add / remove / path scaffolds plain-.cls stubs under stubs/ with --namespace and repeatable --method name:returnType options. Each generated stub exposes callCount and calls static counters so assertions are a one-liner.
  • nimbus config show now loads on demand when invoked outside a test run instead of erroring configuration not loaded.
  • TUI subcommands (nimbus history / analytics / schema / trace) fall back to JSON / text when stdout is piped, so CI pipelines no longer crash on could not open a new TTY.
Fixed
  • Decimal.valueOf(Integer) returns the value instead of 0. Three dispatch sites (type.go:118, methods.go:7110, methods.go:8287) now accept Integer, Decimal, String, Boolean, and Null operands. Integer.valueOf(Decimal) extended symmetrically.
  • Empty string literal no longer equals null in comparisons. String x = ''; x == null now correctly returns false per the Apex spec — matched against real Salesforce behaviour. The DML round-trip behaviour (text fields stored as NULL) is preserved separately via dmlValueToInterface.
  • Inline :new Set<Id>{a.Id, b.Id} / :new List<Integer>{1, 2} bind variables now parse and evaluate inside SOQL literals. The previous SQLSTATE 42601 leak on WHERE Id IN :new Set<Id>{...} is gone; the parser's bind-variable scanner and a new resolveInlineNewBinds pre-pass rewrite inline constructors into literal IN lists before translation.
  • <required>true</required> on custom __c fields is now enforced on DML insert. Skips fields with a DefaultValue, formula fields, and master-detail parents (handled elsewhere). Standard objects keep the existing business-field whitelist to avoid false positives on system fields like OwnerId.
  • SOQL errors no longer leak raw PostgreSQL SQLSTATE codes. The new apexifySOQLError helper maps syntax error at or near … to unexpected token: …, column does not exist to No such column on entity: …, and wraps everything in System.QueryException with a Salesforce-truthful message.
  • Test method name casing preserved in runner output and LSP responses. GetTestMethods and the runner's cached registry now use methodDef.Name instead of the lowercased map key, so testReverse stops showing as testreverse.
  • nimbus.test.parallel and profile overrides now actually work end-to-end. The --parallel flag defaulted to runtime.NumCPU(), which is non-zero, so every downstream caller thought it was explicitly set and the config value never got consulted. Fix: check cmd.Flags().Changed("parallel") before trusting the flag. Profile overrides (%ci.nimbus.test.parallel=1 + NIMBUS_PROFILE=ci) now visibly drop worker count.
  • UserInfo.getLocale() / getLanguage() / getDefaultCurrency() and Organization.IsSandbox now read from the configured nimbus.org.* keys instead of being hardcoded to en_US / USD / false. UserInfo.isMultiCurrencyOrganization() observes the MultiCurrency feature toggle.
  • Record-triggered flow execution: flows are now actually fired on DML (previously they were parsed and counted but never run). Test.enableRecordTriggeredFlows path also works. (Historical note: 0.1.19 initially gated before-save $Record writes behind nimbus.compat.flow-record-sync; that flag was removed in 0.1.20 and the Salesforce-truthful ordering is now unconditional.)
  • nimbus test and nimbus exec build every interpreter with the resolved OrgConfig installed, so currency / locale / sandbox / features / faketime / readonly propagate through triggers, async jobs, and @testSetup blocks uniformly.
April 16, 2026
0.1.18
Changed
  • VSCode extension icon: slashed-N brand mark (replaces the previous cloud silhouette) for better contrast on both light and dark Activity Bar themes.
April 16, 2026
0.1.17
Added
  • Daemon now picks up brand-new custom objects and custom fields live. Adding objects/Foo__c/fields/Bar__c.field-meta.xml (or a whole new Foo__c/ directory) while the daemon is running propagates to Postgres as CREATE TABLE / ALTER TABLE ADD COLUMN automatically — no nimbus sync required. Verified end-to-end with an editor-simulated live-add scenario.
  • Daemon watches sfdx-project.json. Adding a new package directory (e.g. force-app-secondary/) is picked up on save: the new dir joins the watch set, its Apex files are pre-parsed, and the metadata provider is rebuilt.
  • Daemon invalidates custom-metadata-type discovery on .md-meta.xml changes and on new __mdt object definitions. New MDT types no longer require a daemon restart.
  • nimbus test without the daemon now also reconciles the Postgres schema on every run via the same idempotent ApplySourceDelta. Users on the free tier get live field/object pickup too.
Fixed
  • File watcher now covers intermediate directories like force-app/main/default/objects/. Previously only terminal dirs with watched files were registered, so a newly-created child directory (objects/Foo__c/) fired no fsnotify event and the new object was invisible until daemon restart.
  • New directories created by atomic mkdir -p are now watched unconditionally. Empty fields/ subdirs no longer slip through the selective startup scan and miss subsequent field-meta.xml creations.
  • Source scan now records every parsed field, not just formulas/picklists/rollups/defaults. Newly-added regular Text/Number fields flow into the interpreter's SchemaProvider without a re-sync, so insert new Foo__c(NewField__c = …) stops erroring with "Field not found in schema".
  • Added a 150ms settle delay before the watcher re-scans on CREATE events. fsnotify delivers CREATE before the writer flushes, which was leaving parseCustomField with an empty file and dropping new fields from the scan.
  • DB-seeded cache hash now covers every seed-affecting CLI flag: --seed-records, --seed-sites, --seed-list-views, --org-defaults, --default-record-types, --seed-record-excludes. Changing any of them correctly forces a re-seed instead of silently reusing stale rows from the previous run.
April 16, 2026
0.1.16
Added
  • EventBus.publish now routes against the correct governor counter based on the event's publishBehavior metadata. PublishImmediately events increment Limits.getPublishImmediateDml(); PublishAfterCommit events and generic List<SObject> payloads increment Limits.getDmlStatements(). Verified on a real org with a cross-matrix test.
  • publishBehavior and eventType from platform-event object metadata are now parsed from object-meta.xml, round-tripped through .nimbus/schemas/*.json, and exposed via SchemaProvider.GetPublishBehavior. Requires a fresh nimbus sync.
  • Per-worker VariableTracker is now mutex-guarded. Timed-out test goroutines can no longer race the main goroutine reading GetExecutedLines, eliminating a class of "concurrent map iteration and map write" crashes near the end of long suites.
Fixed
  • Test.stopTest() now resets governor limits between queue-drain passes. A subscriber that re-publishes events when hitting the callout cap (the Onfido "retry-later" pattern) no longer loops forever — each pass models a separate async transaction with fresh limits, matching real Salesforce behaviour.
  • nimbus sync now invalidates the daemon's "DB already seeded" flag so the next test run re-seeds standard profiles and the mock user. Previously the daemon kept the flag true, the post-sync test suite skipped seeding, and every @testSetup that touched Profile/User failed with Attempt to de-reference a null object: field Name.
  • Site and SiteDetail seed rows from nimbus.seed.site.<Name>=<secureUrl> now land after ClearAllTables instead of before. The earlier race let TRUNCATE CASCADE wipe the sites a few ms after they were inserted.
  • Before-insert triggers that mutate Name on the trigger copy no longer leak the mutation back to the caller's local variable. The copy barrier now matches Salesforce: auto-populated Names still propagate, but trigger-renamed Names do not.
  • Child subquery handling now prefers the declared element type over the first-element type when dispatching EventBus.publish. A List<SObject> generic list stays on the regular DML counter even when filled with PublishImmediately events.
  • Identifier.Lower lazy cache is now stored via atomic.Pointer[string]. The previous plain-string assignment was not atomic (two-word header) and concurrent AST walks occasionally observed a torn value, crashing deep inside map access.
  • Attachment.Name is no longer hidden by objectsWithoutNameField. Attachment has Name (the file name) in real Salesforce; hiding it broke every test that set Name on Attachment in @testSetup.
  • stripInaccessible and SOQL user-mode access checks now treat Name as implicitly readable when the user has object-level read but no explicit FLS grant. The previous stricter rule over-stripped Name for standard users.
  • lookupRecordById now quotes table and column identifiers. Tables whose API name is a PostgreSQL reserved word (e.g. User) no longer cause SELECT * FROM user WHERE id = $1 to resolve user as CURRENT_USER and abort the transaction with "column id does not exist".
  • ALTER TABLE add-missing-column retry in Insert and BatchInsert is now wrapped in savepoints. Under parallel workers, a lock-timed-out ALTER used to leave the transaction aborted and cascade failures deep in the trigger chain.
  • Preserved static fields (externalStaticWrites) are now snapshotted to their initial values so cross-test reset restores the exact pre-test state instead of leaking mutations from previous tests.
April 12, 2026
0.1.15
Added
  • Dom.Document and Dom.XmlNode are now fully implemented with real XML parsing, serialization, attribute handling, and namespace support. Methods include load, getRootElement, createRootElement, toXmlString, and the full XmlNode API.
  • Compression.compress and Compression.decompress for Zlib blob encoding.
  • Flow.Interview.start() now actually executes the flow via the flow executor. Input variables (including Apex class instances) are preserved through the round-trip so getVariableValue() returns them correctly.
  • transient keyword support: fields and properties marked transient are now excluded from JSON.serialize and JSON.serializePretty output, matching Salesforce behaviour.
  • Assert.isInstanceOfType now checks implemented interfaces on ObjectValue, not just the inheritance chain. A class implementing System.Schedulable is now correctly recognised as an instance of it.
  • Automated Process user (autoproc@<OrgId>) is now seeded with no profile, matching Salesforce's system user.
  • OrgLimits.getMap() registered as a builtin so it resolves via the standard static-method path.
  • Dynamic SOQL via Database.query() now checks system object mocks (Organization, UserRole) before hitting the database.
Fixed
  • Profile SOQL queries are now fully mocked at the interpreter level for all query shapes: literal Name = 'X', bind variables (Name = :profileName), Name IN (...), and by-Id lookups. This eliminates stale isdeleted state from the daemon's cached database.
  • Schema.SObjectType.isAccessible(), isCreateable(), isUpdateable(), and isDeletable() now delegate to CheckObjectPermission instead of always returning true.
  • HttpResponse.getHeaderKeys() now returns keys in sorted order for deterministic output.
  • Profile seed SQL now resets isdeleted = FALSE on conflict, preventing stale soft-delete state.
  • checkRestrictedProfile uses the seeded profile ID lookup table, avoiding raw SQL queries against a potentially stale profile table.
  • DML insert now silently skips null elements in List<SObject> instead of throwing "Insert requires SObject records".
April 12, 2026
0.1.14
Added
  • Database.upsert now honours the external Id field argument for matching existing records.
  • System.Quiddity enum members are now fully enumerated. Returns RUNTEST_SYNC inside a test context.
  • ApexTrigger metadata rows are now seeded alongside ApexClass rows at startup.
  • SOQL FIELDS(ALL), FIELDS(STANDARD), and FIELDS(CUSTOM) expansion is now supported.
  • OrgLimits.getMap() now includes SingleEmail and MassEmail entries.
Fixed
  • Postfix ++ and -- on member fields accessed through a static property getter now actually increment.
  • Test.stopTest() now drains platform events published by async jobs during execution.
  • Partial-success Database.insert and Database.update now validate record Ids correctly.
  • User.Name is now a PostgreSQL generated column that survives schema autoSync.
  • Hierarchy custom settings getOrgDefaults() now populates SetupOwnerId with the org Id.
  • System exception classes are now recognised as subclasses of System.Exception during overload resolution.
  • Set and List now match System.Iterable parameters in addition to bare Iterable.
  • Default Object.toString() fallback for custom class instances.
  • BatchInsert now skips generated columns, matching the single-row Insert path.
  • Governor counters for callouts, publish-immediate DML, and email invocations now increment correctly.
April 11, 2026
0.1.13
Added
  • Regex patterns with lookbehind ((?<!...), (?<=...)) and lookahead ((?!...), (?=...)) now compile and run. Go's stdlib regexp uses RE2 and rejects lookaround; nimbus now transparently falls back to a .NET-compatible engine when RE2 refuses the pattern. String.replaceAll, String.replaceFirst, and the full System.Pattern / System.Matcher API all route through the wrapper, so data-masking rules, SSN scrubbers, and anything else using advanced regex now behaves like real Apex.
  • Cross-class dependency-injection patterns from test class static blocks now survive test isolation. When a test class's static { ... } block either directly writes another class's @TestVisible static field (MyService.client = new MockClient();) or triggers a side-effect via a helper method (MyService.useMocks();), nimbus records the resulting overrides and preserves them across the per-test-method reset. Previously the next access would lazily re-run the field initializer and silently swap the mock back to the production implementation, so any test that depended on the static-block mock would see real service behaviour and fail in surprising ways.
Fixed
  • SOQL date literals (TODAY, YESTERDAY, THIS_WEEK, THIS_MONTH, THIS_QUARTER, THIS_YEAR, and the LAST_/NEXT_ variants) are now translated as half-open ranges instead of point comparisons. A query like WHERE CreatedDate = THIS_WEEK used to generate createddate = date_trunc('week', NOW()), which only matched records created exactly at midnight Monday; it now generates a proper createddate >= start AND createddate < end range. Every comparison operator (=, !=, <, >, <=, >=) respects Salesforce's boundary semantics. Week ranges start on Sunday to match Date.toStartOfWeek() on US English locales.
  • Regex replacement backreferences ($1, $2, …) now work correctly when the capture group number is followed by literal letters or digits. A replacement like '$1XXX-XX-$4' used to expand to an empty string because Go's stdlib regexp greedily consumed 1XXX as a named group; nimbus now translates Java/Apex replacement syntax to Go's unambiguous ${N} form so SSN/credit-card formatters that embed literal text next to backref numbers produce the expected output.
  • Matcher.replaceFirst() now expands group references in the replacement string. Previously it returned the replacement text verbatim without ever applying the captured groups, so a pattern (\d+) with replacement '[$1]' on the input 'abc 42 xyz' returned 'abc [$1] xyz' instead of 'abc [42] xyz'.
  • Aggregate SOQL queries with multi-level relationship chains in SELECT or GROUP BY now translate to valid SQL. A query like SELECT LogEntry__r.Log__r.Status__c, COUNT(Id) FROM LogEntryTag__c GROUP BY LogEntry__r.Log__r.Status__c used to emit three separately-quoted identifiers ("logentry__r"."log__r"."status__c") which PostgreSQL rejected with "missing FROM-clause entry for table log__r". The aggregate translator now builds the chained JOIN alias the same way the regular translator does.
  • ORDER BY with multi-level relationship fields now resolves to the correct chained JOIN alias. ORDER BY LogEntry__r.Log__r.LogRetentionDate__c used to emit nonsense column references for anything past the first hop; the translator now walks the full dotted path exactly like the SELECT clause does.
  • JSON.deserialize(..., Database.SaveResult.class) (and the UpsertResult, DeleteResult, UndeleteResult, Error variants) now reconstructs a typed object instead of a bare Map. Overload resolution against Database.Error parameters scored -1 when tests built mock results through serialize/deserialize round-trips; the typed path makes round-trip mocks behave the way integration code expects.
  • Schema.LogEntryArchive__b-style Big Object custom objects are now loaded from project metadata. The schema scanner's allow-list filtered directories by suffix (__c, __mdt, __e) and silently dropped __b; Schema.MyBigObject__b.SObjectType.getDescribe().fields.getMap() used to return an empty map and cascade into null-deref chains. (Schema cache is keyed per project — a first run after upgrading will re-scan and pick them up.)
  • SOQL ALL ROWS is now stripped during translation instead of forwarded to PostgreSQL as a literal identifier. Queries that wanted to include soft-deleted rows used to fail with syntax error near "ALL".
  • SELECT and WHERE clauses that use the .Type discriminator on a polymorphic relationship (Owner.Type, What.Type, Who.Type, LinkedEntity.Type, RelatedTo.Type) now project NULL under an alias instead of trying to join against a non-existent type column on the target table. Full polymorphic dispatch still isn't implemented, but queries that reference .Type for logging or filtering now run instead of erroring.
  • Bind variables written as bare method calls (:getNamespacePrefix(), no receiver) now consume the trailing parentheses instead of leaving them dangling in the translated SQL. Previously the translator emitted nonsense like ... IS NULL() when the method returned null.
  • Datetime.valueOf(Long) now constructs a datetime from a Unix-millisecond epoch, matching Salesforce's documented overload. Nimbus already handled the string overload; the Long overload silently returned null, so any code that round-tripped timestamps through Long.valueOf(epochString) ended up with null datetimes.
  • SObject.getPopulatedFieldsAsMap() now excludes fields whose value is null. Apex's real behaviour returns only populated (explicitly-set) fields, so tests that iterate the map and skip based on containsKey now observe the same contract nimbus uses for serialization.
  • Custom objects with a Name field of type Text are now auto-populated with the record Id on insert when the caller leaves Name null or empty. Salesforce performs this auto-fill on text Name fields for Master-Detail/Lookup children so rec.Name is never bare null after insert; nimbus mirrors the behaviour.
  • Formula-field resolution now skips rollup summary fields. Rollups stash their metadata in the Formula slot as a summary:op:field string (historical hack), and the formula evaluator was parsing that as Apex, returning null, and silently clobbering the correct aggregate value that the rollup pipeline had just computed.
  • Schema.DescribeFieldResult.getPrecision() and getScale() now read the field's declared metadata instead of returning hardcoded 18 / 2. Number fields with precision/scale populated via (3.14).setScale(fieldDescribe.getScale()) used to round to 2 digits regardless of the target column type.
  • getLength() on any Id field now unconditionally returns 18 regardless of whether the containing SObject ships with a JSON describe. Mock-id generators that build ids as keyPrefix + zeros(length - prefix - suffix) + suffix used to produce 255-character strings for SObjects without a describe (CronTrigger, AsyncApexJob), overflowing downstream Text(18) fields.
  • Schema.X prefixed references (e.g. Schema.AuthSession, Schema.LoginHistory) now resolve to the real standard SObject even when a project has an inner class with the same short name. The previous shadow-check short-circuit picked the inner class, so Schema.AuthSession.SObjectType.getDescribe() failed with "Method AuthSession.getDescribe not found" on any codebase that ships a fixture called AuthSession.
  • Test.setMock(HttpCalloutMock.class, mock) and Test.setMock(System.HttpCalloutMock.class, mock) now land under the same registration key. Previously the qualified form was stored under system.httpcalloutmock while Http.send looked up httpcalloutmock, so fully-qualified registration silently did nothing.
  • SOQL WHERE ... IN (SELECT ...) subqueries now keep only the first projected column. The translator's JOIN FK / parent-id auto-injection produced multi-column subqueries that PostgreSQL rejected; the new keepFirstSelectColumn rewrite fixes fflib selector patterns that embed subqueries.
  • System.ParentJobResult.SUCCESS (and the other enum members) now resolve correctly when the project also defines a class named ParentJobResult. The enum path now accepts both the System.-qualified form and the bare form after a namespace strip.
  • System.Flow is now recognised as a system class, so Flow.Interview and related APIs no longer fall through the auto-null stub path. Also added a generic TypeValue method dispatch so registered builtins under keyed names (Flow.Interview.createInterview, Cache.Org.getPartition, …) are found without an inline dispatcher stanza.
  • Added the ApexEmailNotification standard schema so SOQL queries that reference it no longer fail with "relation does not exist".
April 11, 2026
0.1.12
Fixed
  • System.LoggingLevel ordinals now match Salesforce's declaration order. The values are NONE, INTERNAL, FINEST, FINER, FINE, DEBUG, INFO, WARN, ERROR with ERROR as the highest (most severe, least verbose). Any logging framework that compares entries against the user's configured threshold via entry.ordinal() >= user.ordinal() now filters correctly instead of logging everything regardless of the threshold.
  • switch on null now correctly matches only a literal when null clause. Previously nimbus tried to evaluate every when label as an identifier even when the switch value was null, so a statement like switch on partitionType { when ORGANIZATION ... when SESSION ... } would throw Unknown identifier: SESSION whenever partitionType was null (e.g. from a mock delegate calling super(null, null)).
  • Child subqueries split across multiple lines are now detected. A formatted SOQL string of the form SELECT Id, (\n SELECT Id FROM Children__r\n ) FROM Parent__c used to silently drop the subquery, leaving the relationship field unpopulated and any parent.Children__r.size() call failing with Unknown method size on SObject Unknown.
  • Null-argument overload resolution now breaks ties using the declared type of the argument. Dispatching handler.execute(this.input.triggerNewMap) against overloads execute(List<SObject>) and execute(Map<Id, SObject>) now picks the map overload when triggerNewMap is null, instead of silently routing the call to the list overload and cascading into a null dereference.
  • Inner enum types declared inside a class (e.g. MyClass.MyEnum) now support valueOf(String) in addition to values(), including the standard NoSuchElementException on unknown names.
  • Overload resolution between sibling inner classes under the same outer type now matches correctly. A subclass OuterA.Mock that extends OuterB.Base is now recognised as a valid argument for a parameter typed Base, without the broader subclass-matching widening that caused regressions in earlier attempts.
  • Local variables now correctly shadow instance fields in assignments. A method that declared String conditionsLogic with the same name as a private String conditionsLogic field was silently writing to the field — the local was always returned empty. Apex scoping rules say the local wins, and nimbus now respects that.
  • Schema.DescribeFieldResult.getSoapType() returns the actual SOAP type of the field instead of hardcoded STRING. Field-type-driven comparators (e.g. choosing string vs decimal vs datetime comparison) now work on non-string fields.
  • Schema.DescribeFieldResult.getLength(), getPrecision(), and getScale() are now dispatched against the field's actual metadata instead of returning a single hardcoded default. Long text fields no longer get silently truncated because getLength() returned 255 for everything.
  • Cache.Visibility.NAMESPACE, ALL, and SESSION are now proper enum values. Calls to partition-delegate put(...) methods that accept a Cache.Visibility parameter now match the intended overload instead of failing with "No matching overload".
  • System.OrgLimits.getMap() now returns a map of stub System.OrgLimit objects so tests that iterate over governor limits run instead of throwing Method OrgLimits.getMap not found.
  • Type.forName(String) and Type.newInstance() now prefer a real SObject type over any SFDX-generated stub class of the same name. Tests that did Type.forName('Account').newInstance() used to silently return an empty ObjectValue for the Account shadow class.
  • SObject arguments now match parameters typed Schema.X (and vice versa). Nimbus already treated Schema.Account and Account as equivalent for type references; overload resolution now also honours that equivalence, fixing dispatch for framework methods declared as foo(Schema.User u).
  • Number fields declared with precision/scale in metadata (stored as bigint in the local schema) now map to the Double display type and the Double SOAP type. Tests that populate numeric fields via reflection no longer end up with the field name as a string in the column.
  • Max/min rollup summary defaults now respect the target field's type: they default to null instead of "0", so datetime rollups no longer throw "date/time field value out of range" when no child records exist.
  • Added standard schemas for AuthSession, LoginHistory, and Network, so projects that query session/network metadata during test setup no longer hit "table does not exist".
  • System.EventBus.publish() now dispatches correctly when called on a TypeValue reference. Publishing a platform event from a helper that resolves System.EventBus dynamically used to miss the method.
  • Static property getters no longer recurse when the getter body calls back into the same class via this. The inside-getter raw-value short-circuit now distinguishes instance-field reads from StaticVars reads and correctly returns the freshly-assigned value.
April 10, 2026
0.1.11
Fixed
  • Limits.getDMLStatements() now reflects DML done through Database.insert, Database.update, Database.delete, and Database.upsert. The counter used to only track the bare-statement form (insert records;), so libraries that route every write through Database.* method calls — fflib unit-of-work patterns, builder libraries — saw getDMLStatements() return 0 after a commit and any assertion against the counter failed.
  • Eliminated a duplicate "Standard User" Profile row in the seeded DB. The runner used to insert the configurable mock user profile at 00e000000000001AAA with the default name "Standard User" and seed the standard "Standard User" profile at 00e000000000003AAA, so queries like SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1 returned two rows and threw "List has more than 1 row for assignment to SObject". The mock user now points at whichever seeded standard profile matches its configured name; only custom names create a bespoke row. UserInfo.getProfileId() looks up the mock user's profile from the DB instead of returning a stale hard-coded id.
April 10, 2026
0.1.10
Fixed
  • Higher -p worker counts no longer get slower than lower ones. Running with -p 50 on a large suite used to be slower than -p 4 because workers serialized on shared locks during result reporting and metadata lookups. The result-reporting path and the metadata provider are now structured so workers can actually run in parallel.
April 10, 2026
0.1.9
Performance
  • Another round of interpreter hot-path tuning. Test runs on large suites are noticeably faster and use less CPU, with no changes to behavior or configuration.
April 10, 2026
0.1.8
Performance
  • Large cut in the number of DB round-trips the interpreter issues during test runs. On a 2,300-test production suite, total queries drop by ~75% (≈223k → 36k) and query time drops by ~70%. Repeatedly-fetched rows — custom settings, owner/parent records referenced from cross-object formulas, UserInfo.getUserId() fallbacks — are now cached per test class and invalidated on DML against the same row. Wall-clock savings depend on your DB backend: on the embedded unix-socket postgres each round-trip is already cheap, but CI runners, Docker-on-Mac, and remote postgres setups should see measurable run-time drops.
  • Interpreter hot-path cleanup. Formula builtins are now shared at package level instead of rebuilt per evaluator, parsed formulas are cached by source text, member-access metadata is cached on the AST node, and an AST-level negative cache short-circuits identifier resolution for names that are clearly not builtins.
Fixed
  • Cross-object formulas of the shape Owner:User.FirstName & " " & Owner:User.LastName (or anything richer than a single Rel:Type.Field reference) no longer fall into a fast-path that extracted nonsense table names and fired silently-failing SELECTs against imaginary tables. These formulas now route to the normal formula evaluator and return correct values.
April 9, 2026
0.1.7
Performance
  • Interpreter is ~9% faster on large test suites. No behavior changes, no flags to flip — just upgrade and runs are shorter.
April 9, 2026
0.1.6
New
  • PostgreSQL is now embedded directly in the Nimbus binary on all platforms — macOS, Linux, and Windows. No more first-run network fetch; Nimbus works offline from the first invocation. The bundle is stripped to the minimum required (three binaries, two plugins instead of 90, only libraries postgres dynamically links against) and compressed with zstd. Per-platform bundle: 14–16 MB. Shared cache in ~/.nimbus/pg/: ~55 MB (down from 132 MB per-project).
  • Stripped plugin set is measurably faster at runtime. On large production test suites, warm runs are ~15% faster compared to the unstripped distribution because postgres scans lib/postgresql/ during startup and catalog initialization — fewer plugins, less work.
Performance
  • PostgreSQL now communicates over a per-project unix domain socket on macOS and Linux instead of TCP loopback. Skipping the kernel TCP stack on every round-trip is worth ~10% wall-clock on warm runs of large suites. Windows continues to use localhost TCP (no unix socket support).
  • Migrated the database driver from lib/pq to pgx/v5. Binary protocol, automatic prepared statement caching, and active maintenance.
  • Aggressive PostgreSQL server tuning for the test workload: UNLOGGED tables, autovacuum=off, jit=off, larger shared_buffers / work_mem, and several activity-tracking knobs disabled. Cumulative speedup of 10–20% on a 2,300-test production suite.
April 9, 2026
0.1.5
New
  • Curl-based install script — curl https://install.testnimbus.dev | sh installs the latest binary without Homebrew.
Fixed
  • SOQL queries without an explicit ORDER BY clause now default to ORDER BY id, ensuring consistent, deterministic result ordering across all test runs.
  • VSCode extension metadata corrected — publisher, display name, and category fields updated.
  • Install script archive filename no longer includes a leading v prefix.
April 9, 2026
0.1.4
Changed
  • Background daemon (nimbus daemon) is now available on the free tier. Previously gated behind Pro to enable VSCode extension for free users
  • Database pre-warming - the startup optimization that keeps the embedded database warm between runs - remains a Pro feature.
April 9, 2026
0.1.3
Fixed
  • Embedded PostgreSQL server logs now redirect to .nimbus/db/postgres.log instead of being written to stdout, eliminating log noise in terminal output.
April 9, 2026
0.1.2
Fixed
  • Share tables (e.g., AccountShare, LeadShare, OpportunityShare) are now included in schema auto-sync.
April 8, 2026
0.1.1
Fixed
  • Free tier now automatically runs with 1 worker. Previously, parallel workers were always blocked on the free tier but users had to manually pass --workers 1 on every run.
  • Missing schemas are now detected and automatically synced at startup instead of failing silently.
  • Machine ID is now correctly recovered after a reinstall, preventing license validation failures on the same machine.
April 8, 2026
0.1.0
Initial
Initial release
  • Full Apex interpreter — classes, interfaces, enums, abstract classes, inheritance, generics, method overloading, exception handling.
  • Before and after triggers with complete context: Trigger.new, Trigger.old, Trigger.newMap, Trigger.oldMap, and all boolean flags.
  • Record-triggered flows, autolaunched flows, subflows, platform event flows, decisions, loops, formulas, collection processors.
  • SOQL to SQL translation with bind variables, relationship queries, aggregate functions, subqueries, and LIMIT/OFFSET.
  • DML operations: insert, update, delete, upsert, undelete — all fire triggers and roll back per-test.
  • fflib / ApexMocks support — stub API, argument captors, and verify/when patterns.
  • Background daemon (nimbus daemon start) for warm-start performance — avoids full cold-start on every run.
  • Coverage reporting: HTML, JSON, and Cobertura XML output formats.
  • Debugger with breakpoints, step in, step over, step out, and variable inspection.
  • Dev UI (nimbus dev) — browser-based dashboard with live test results, schema explorer, and anonymous Apex execution.
  • Watch mode (nimbus test:watch) — re-runs affected tests on file save.
  • Traces & Analytics: structured execution traces with 5 verbosity levels (method calls, SOQL, DML, triggers, timing).
  • Governor limit tracking — SOQL row counts, DML statement counts, heap, and CPU measurements.
  • Mutation testing (Pro) — automatically mutates code and verifies tests catch each change.
  • Benchmarking (Pro) — compare performance across branches or over time.
  • Auto schema sync from a connected Salesforce org.
  • VS Code extension: CodeLens run/debug buttons, Test Explorer integration, coverage gutter icons, SOQL preview, governor limit annotations, and trace viewer.
  • Install via Homebrew (macOS/Linux), Scoop (Windows), and direct binary download.