Repository navigation
Release notes
Long-form release notes for deep6. The README carries cliff-notes (one or two lines per release); this page is the canonical history.
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.
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.
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.
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.
Reproduced as-is from the README cliff-notes — no detail beyond what shipped originally.
- 1.2.1 — AI-friendliness: added shippable
use-deep6skill,bugs/homepage/llms/llmsFullfields inpackage.json, shipAGENTS.mdandARCHITECTURE.mdto npm. - 1.2.0 — added
URLsupport, bug fixes (RegExpflag 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
anyas 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.
Core functions
Environments and variables
Unification
Traverse
Unifiers
Utilities