Skip to content

Release notes

Eugene Lazutkin edited this page Jul 13, 2026 · 4 revisions

Long-form release notes for deep6. The README carries cliff-notes (one or two lines per release); this page is the canonical history.

1.4.0 — 2026-07-13

Two behavioral defaults changed, both for the better; the public surface is otherwise additive.

Fixed shared-reference equality. Since 1.1.0, equal() rejected structurally-equivalent values when one side reused an object reference the other side spelled out — equal([T, T], [{v: 1}, {v: 1}]) with a shared T returned false. The seen-set introduced with cycle support enforced graph isomorphism instead of value equality, and no test covered one-sided sharing for six years. The pattern that surfaced it: stream libraries hoisting structural tokens as module-level singletons while test expectations use fresh literals.

circular is now a semantics enum. The option accepts 'bisimulation', 'closure', and 'graph' in addition to booleans; true picks the cheapest algorithm (currently 'bisimulation'). The three modes form a strictness ladder for what counts as "the value" once sharing and cycles exist: bisimulation compares only unfolded values (a 1-cycle equals a 2-cycle); 'closure' additionally requires cycles to close at the same depths on both sides — matching Node's util.isDeepStrictEqual semantics, except deep6 is stricter when only one side closes a loop at a shared node; 'graph' compares pointer structure exactly (the pre-1.4 behavior, retained as an opt-in — it is what structuredClone preserves). Sharing is forgiven in every mode except 'graph'. Full mode documentation: unify().

EnvMap is the default environment. unify() now creates an EnvMap when no environment is passed, delivering the 2.7–6.3× end-to-end speedup measured on solver workloads in May. Env remains exported and fully supported — pass new Env() explicitly to keep the proto-chain implementation. unify()'s declared types widened to the EnvLike contract (now re-exported from deep6/unify.js), and internal binding storage is officially implementation detail — use the method API (get, isBound, isAlias, getAllValues).

Performance. The new default is measurably faster: the bisimulation pair-memo needs half the bookkeeping of the old index-pairing, making typical acyclic deep-equality ~10% faster than 1.3.1 on top of the EnvMap gains; 'closure' carries a ~25% premium over the old scheme, and 'graph' costs exactly what 1.3.1 did.

Housekeeping. Fleet-standards sweep: license file normalized to verbatim SPDX BSD-3-Clause (GitHub now detects it), wiki gained a sidebar and a search index, CI permissions tightened, retired AI-rule mirror files removed. AGENTS.md/ARCHITECTURE.md no longer ship in the npm tarball (the consumer-facing llms.txt/llms-full.txt and the use-deep6 skill still do); engines.node dropped (no technical floor — deep6 runs on baseline ES2022). Dev dependencies at latest, including TypeScript 7. Test suite grew 675 → 705, green on Node, Bun, and Deno; docs corrected env.get(v) → env.get(v.name) in three places.

1.3.1 — 2026-05-09

Bug fix for an EnvMap-with-traversal regression that shipped in 1.3.0.

EnvMap now works with assemble(), clone(), and deref(). The three traverse helpers narrowed their env argument with env instanceof Env, silently treating any non-Env EnvLike (notably EnvMap) as the options argument and dropping it on the floor. Surface symptom: assemble(X, envMap) returned the unbound Variable instead of its bound value, even when X.isBound(envMap) === true. Bindings produced by unify(a, b, new EnvMap()) could not be extracted via the traverse layer. Replaced the instanceof Env check at all three sites with the same isEnvLike duck-type predicate unify() already uses (typeof e.isBound == 'function'); Env, EnvMap, and any user-defined EnvLike implementation now flow through transparently. The options-bag dispatch (assemble(source, {circular: true})) is unchanged. Microbench layer (unify → env.get) was unaffected; only the result-extraction path was broken.

1.3.0 — 2026-05-08

A multi-implementation pass on the unification environment. The on-disk public surface is additive (one new class, one new TS interface, one options-bag field), and one undocumented direct-mutation pattern (env.openObjects = true) has become a silent no-op. None of the documented APIs (equal, match, clone, unify(a, b, options)) change behaviour.

EnvMap — Map-stack sibling of Env. New class in src/env-map.js. Implements the same method API as Env (push/pop/revert/bindVar/bindVal/isBound/isAlias/get/ getAllValues plus public depth and options). Uses two parallel Map-stacks (one entry per generation depth, lazily allocated) so identity-hashed lookup avoids the dict-mode deopt that Env's parallel Object.create() chains pay once enough symbol-named bindings accumulate. On the synthetic bench/bench-env-aggregate.js workloads — designed to mirror the solver-style usage that yopl drives — EnvMap measures 4.7–8.3× faster than the proto-chain Env: map_shallow 6.51×, map_deep 5.86×, map_bindheavy 5.05×, map_bindandget 4.69×, map_alias 8.3×. Env remains the default created by unify() when no env is passed (no end-to-end speedup confirmation yet for the average consumer); pass new EnvMap() as the third argument to unify() to opt in. Backed by 60 new parity tests in tests/test-env-map.js mirroring the existing test-env.js shape, plus four unify()-with-EnvMap integration tests.

EnvLike — named method contract. TS-only interface EnvLike in src/env-shared.d.ts. Both Env and EnvMap now declare implements EnvLike. Promotes the previously-accidental structural compatibility between the two implementations to a checked nominal contract. Re-exported from both env.d.ts and env-map.d.ts for discoverability. The runtime change in src/unify.js widens the "third argument is an env" check from instanceof Env to a duck-type predicate typeof e.isBound == 'function', so EnvMap and any user-defined EnvLike implementation work transparently — and unify.js doesn't have to import EnvMap to know about it. The custom-impl skeleton is documented in the new wiki/env-EnvLike.md.

env.options bag. Behavioural flags (openObjects, openArrays, openMaps, openSets, circular, symbols, signedZero, loose, ignoreFunctions) now live on env.options rather than directly on the Env instance. The Env constructor initializes this.options = {}; unify(a, b, env, options) does env.options = options ? {...env.options, ...options} : env.options, so the existing chaining behaviour (options accumulated across calls with the same env) is preserved. All eleven internal reads in unify.js plus three this.e.symbols reads in Command callbacks were rewritten to env.options.<flag>. Avoids the hidden-class churn of Object.assign(env, options) writing arbitrary fields onto the env instance and gives custom unifiers a single, well-defined place to read flags. Silent break for direct-flag mutators — the env.openObjects = true pattern (never documented; used internally only by yopl) is now a no-op. Coordinate with any consumer that mutated env fields directly: flip to env.options.openObjects = true. yopl is the only known consumer; its update lands in lockstep with this release.

src/env.js split into env-shared.js + env.js. Pure file reorganization, no behaviour change. src/env-shared.{js,d.ts} (new) holds the implementation-agnostic primitives — Unifier, isUnifier, Variable, isVariable, variable, _, any, keyDepth, collectSymbols, ensure. src/env.{js,d.ts} keeps the proto-chain Env class plus re-exports the full shared set, so existing import {Variable, Unifier, _, any} from 'deep6/env.js' continues working unchanged. Variable.{isBound,isAlias,get,unify} and replaceVars.js were refactored along the way to route through the Env method API (env.isBound, env.get, env.isAlias) rather than poke at private env.values / env.variables directly. Microbench cost across five Variable.unify paths: 2–9.3% (within JIT noise band; the 9.3% peak is the bound-var path with two env.get calls). Acceptable trade for sealing the leak that was the prerequisite for adding EnvMap.

Env perf optimizations. Three layered changes to src/env.js — ensure() fastpath through the symbol-keyed proto-chain (Idea #1+#2+ #3) — measured 37–47% improvement on the targeted bench/bench-ensure.js synthetic workloads (shallow/deep/bindheavy). The bench/bench-env-aggregate.js yopl-shape workload sees a smaller gain because the dominant cost there is V8-internal map mode tracking, not the JS-side hot path. A speculative rewrite of collectSymbols into a generator with a Set was reverted after benchmarking showed 12–22% regression vs. the original Array.from + .forEach form.

Documentation refresh. Wiki: five pages updated (Home.md, env.md, env-Env.md, env-Variable.md) plus two new pages (env-EnvMap.md, env-EnvLike.md). The wiki submodule advances 742c895 → 8f861f6 → <new> over the migration plus this release. AI docs: AGENTS.md, ARCHITECTURE.md, CODEBASE.md, llms.txt, llms-full.txt all reflect the new module layout, the Env/EnvMap/EnvLike trio, and the env.options bag. The .windsurfrules / .cursorrules / .clinerules mirror files were re-synced from the canonical AGENTS.md (had drifted from a 65-line snapshot to the current 256 lines).

Tests. 60 new EnvMap parity tests in tests/test-env-map.js bring the suite to 675 across Node, Bun, and Deno. The added benches (bench/bench-env-aggregate.js, bench/bench-yopl-namekind.js, bench/bench-variable-unify.js) ship in the repo but are not in the npm tarball.

1.2.2 — 2026-05-07

A bugfix release. Every behavioral change brings the implementation in line with what the docs already promised — there are no new features.

Cycle-detection corner cases under circular: true (the default for equal() and match()). unify() and walk() were over-eagerly seen- tracking values that don't recurse into children — atomic registered types (Date, RegExp, URL, typed arrays, Promise) and custom Unifier instances. The same instance reused at multiple positions on one side hit the seen-set on the second visit and was wrongly rejected. Examples now fixed: equal({a: d, b: d}, {a: new Date(t), b: new Date(t)}) returned false, now true; match(['1', '2'], [m, m]) where m is a reused matchString returned false, now true. Cycle tracking now applies only to types whose handlers actually recurse via children (plain objects, arrays, Map, and the Wrap instances produced by open() / soft()). Genuine cycles through plain objects, arrays, and Maps still detect correctly.

Array-vs-object collision in equal(). The fast path that skips the type registry for plain prototypes fell through to unifyObjects without checking that both sides were the same kind, so equal([1, 2, 3], {0: 1, 1: 2, 2: 3}) and equal([], {}) returned true. Both now correctly return false.

Wrap.unify fall-through type-parity. The outer parity check covered arrays only; Map/Set were checked per-branch but the plain-object fall-through had no parity check at all. Empty pattern wrapped via open({}) would trivially match any object whose own enumerable keys are empty — Date, RegExp, Promise, URL, ArrayBuffer, DataView. Now the outer check enforces array/Map/Set parity, and the fall-through gates on prototype kind (both plain or both same non-plain prototype), preserving typed-array prefix-match via open().

clone() and assemble() / deref() with unbound variables and no env. Calling clone({a: variable('x')}) without an env crashed in processVariable dereferencing null.values. The variable is now passed through unchanged when no env is supplied.

match() opens arrays by default. defaultMatchOptions was missing openArrays: true despite documenting openObjects / openMaps / openSets defaults symmetrically. match([1, 2, 3], [1, 2]) now returns true (was false), matching the symmetric "open all containers" semantic the wiki promised.

Ref is now a named export of deep6/unifiers/ref.js. The Ref class was always documented in the wiki and accessible via ref.Ref, but the runtime didn't expose it as a named export. import {Ref} from 'deep6/unifiers/ref.js' now works.

Strict boolean returns from matchString.unify(...) and matchInstanceOf.unify(...). These methods were declared boolean in their .d.ts sidecars but the JS implementations returned the raw RegExp.exec result (a RegExpExecArray | null) or the result of a short-circuited boolean expression on falsy val (could be 0, '', null). Returns are now coerced to strict boolean so the .d.ts contract is honest — no behavioral change for truthy/falsy callers.

TypeScript typing fixes. Phantom registry / filters top-level exports in unify.d.ts (they were always unify.registry / unify.filters attached to the function, never named exports) moved to the function intersection. assemble() and deref() are now generic in the source type — <T>(source: T, …) => T — preserving structural type for callers. open() and soft() return a phantom-branded Wrapped<T> so the type system distinguishes wrapped values from the original (preventing the silent-no-op trap of equal(a, open(a))). Unifier.unify is widened from boolean to unknown (the dispatcher uses a truthy check, not strict equality), with built-in subclasses narrowing back to boolean.

Engine, CI, and tooling. engines.node: ">=22" declared (Node 20 EOL'd 2026-04-30). CI matrix bumped to [22, 24, 26]. Wiki submodule URL switched from SSH to HTTPS. Each src/**/*.js now carries a // @ts-self-types="./<file>.d.ts" directive on line 1 so IDE/LSP hover defers to the .d.ts sidecar. .github/dependabot.yml upgraded to the fleet-canonical form (grouped per ecosystem, versioning-strategy: 'increase-if-necessary' on npm).

Tests. tests/test-edge-cases.js added — 38 regression tests covering each fix above. Total now 611 tests across all runtimes (Node, Bun, Deno).

Internal docs. dev-docs/env-design-alternatives.md added — a critique of the prototypal-inheritance generational design in src/env.js with two alternative sketches (rollback DSU + journaled flat Maps; linked-list frames with per-frame Map) and a tradeoff table. Recommendation is do not refactor without a measured workload reason; the existing design trades V8 hidden-class friendliness for O(1) push and trivial revert.

Earlier releases

Reproduced as-is from the README cliff-notes — no detail beyond what shipped originally.

  • 1.2.1 — AI-friendliness: added shippable use-deep6 skill, bugs / homepage / llms / llmsFull fields in package.json, ship AGENTS.md and ARCHITECTURE.md to npm.
  • 1.2.0 — added URL support, bug fixes (RegExp flag comparison, Map assembly / deref, env.ensure()), performance optimizations, improved TypeScript typings, expanded tests (~573).
  • 1.1.4 — updated dev deps.
  • 1.1.3 — updated dev deps.
  • 1.1.2 — updated dev deps.
  • 1.1.1 — reformulated any as a well-known symbol.
  • 1.1.0 — separated from yopl, extensive refactoring.
  • 1.0.1 — added the exports statement.
  • 1.0.0 — the first 1.0 release.

Clone this wiki locally