Skip to content

Resolution & Caching ​

This page explains how Tova resolves dependency versions, fetches packages, and manages the global cache.

Version Resolution ​

Tova uses minimum version selection (MVS), the same strategy used by Go modules. This is simpler and more predictable than the SAT-solver approach used by npm/yarn.

How MVS works ​

Given a set of version constraints, MVS picks the minimum version that satisfies all of them. No ambiguity, no heuristics.

Example: If package A requires ^1.2.0 and package B requires ^1.3.0 of the same dependency:

  • The constraints are: >=1.2.0 <2.0.0 AND >=1.3.0 <2.0.0
  • Available versions: 1.0.0, 1.1.0, 1.2.0, 1.3.0, 1.4.0, 2.0.0
  • MVS selects: 1.3.0 (the minimum version satisfying both)

Why MVS? ​

PropertyMVS (Tova/Go)SAT solver (npm/yarn)
DeterministicAlways the same resultCan vary with solver heuristics
ReproducibleSame input = same outputLock file required for reproducibility
PredictableMinimum = fewest surprisesMay pick newest, introducing untested code
PerformanceLinear scanNP-hard in general case
DebuggableEasy to reason aboutConflict resolution can be opaque

One version per module ​

Tova does not allow multiple versions of the same module in a dependency tree. If two dependencies require incompatible versions (e.g., ^1.0.0 and ^2.0.0), Tova reports a clear error instead of silently duplicating the module:

error: version conflict for github.com/alice/tova-http

  github.com/carol/web-framework requires ^1.0.0
  github.com/dave/api-toolkit requires ^2.0.0

  These constraints cannot be satisfied simultaneously.
  Tip: Check if either dependency has a newer version that resolves this.

Resolution Flow ​

When you run tova install, the resolver follows this flow:

1. Read [dependencies] from tova.toml
         │
2. For each dependency:
   ├── Check tova.lock for pinned version
   │   └── If found: use pinned version
   └── If not found:
       ├── git ls-remote --tags → list available versions
       └── Select minimum version satisfying all constraints
         │
3. For each resolved module:
   ├── Check ~/.tova/pkg/ cache
   │   └── If cached: skip fetch
   └── If not cached:
       └── git clone --depth 1 --branch vX.Y.Z → cache
         │
4. Read each module's tova.toml
   └── Queue transitive [dependencies] for resolution (back to step 2)
         │
5. Collect all [npm] sections from dependency tree
         │
6. Write tova.lock with resolved versions + SHAs
         │
7. Generate package.json → bun install

Global Cache ​

All fetched packages are stored in a global cache, shared across all projects on the machine.

Cache location ​

Default: ~/.tova/pkg/

Override with the TOVA_CACHE_DIR environment variable.

Cache structure ​

~/.tova/pkg/
├── github.com/
│   ├── alice/
│   │   └── tova-http/
│   │       ├── v1.2.0/          # Full source code (no .git)
│   │       │   ├── tova.toml
│   │       │   ├── src/
│   │       │   │   └── lib.tova
│   │       │   └── ...
│   │       └── v1.3.0/
│   └── bob/
│       └── tova-jwt/
│           └── v1.0.0/
├── gitlab.com/
│   └── ...
└── .cache/                      # Compiled .js output
    └── github.com/
        └── alice/
            └── tova-http/
                └── v1.3.0/
                    └── lib.js

How fetching works ​

When a package version is not cached:

  1. git clone --depth 1 --branch v{version} {url} into a temp directory
  2. Remove the .git directory (saves disk space)
  3. Move the source to the final cache path (~/.tova/pkg/host/owner/repo/vX.Y.Z/)

Shallow clones (--depth 1) minimize download size -- only the tagged commit is fetched, not the full history.

Cache validation ​

A cached version is considered valid if:

  • The version directory exists (e.g., ~/.tova/pkg/github.com/alice/tova-http/v1.3.0/)
  • A tova.toml file exists in that directory

Compilation cache ​

When the compiler processes a Tova module import, it compiles the .tova files to .js and stores the output in .cache/. Subsequent compilations reuse the cached output, avoiding redundant compilation of dependencies.

Transitive Dependencies ​

Tova resolves transitive dependencies automatically. If your project depends on A, and A depends on B, then B is fetched and cached too.

npm dependency merging ​

Transitive npm dependencies are collected from the entire Tova dependency tree and merged into the consumer's package.json:

my-app
├── depends on: github.com/alice/http (npm: cookie-parser ^1.4.0)
└── depends on: github.com/bob/jwt (npm: jsonwebtoken ^9.0.0)
    └── depends on: github.com/carol/logger (npm: winston ^3.0.0)

The generated package.json will include cookie-parser, jsonwebtoken, and winston -- all from the transitive tree.

When the same npm package appears in multiple dependencies with different constraints, the highest constraint wins:

alice/http declares: zod ^3.0.0
bob/jwt declares:   zod ^3.2.0
→ Generated package.json: zod ^3.2.0

Offline Behavior ​

When a lock file (tova.lock) exists, tova install uses the pinned versions. If those versions are already in the global cache, no network calls are made for Tova modules -- only bun install runs for npm dependencies.

If a required version is not cached, the resolver fetches it via git clone. If the network is unavailable:

error: failed to fetch github.com/alice/tova-http

  git clone failed: Could not resolve host: github.com

  Cached versions available: v1.2.0, v1.2.1

For CI/CD pipelines, pre-populate the cache (~/.tova/pkg/) in your build image so all versions are available locally.

Circular Dependency Detection ​

Tova does not allow circular module dependencies. If A depends on B and B depends on A, the resolver reports an error:

error: circular dependency detected

  github.com/alice/http → github.com/bob/middleware → github.com/alice/http

  Tova does not allow circular module dependencies.

Circular dependencies between .tova files within the same package are fine (they're handled by the multi-file merge system). Only cross-module circular dependencies are prohibited.

Released under the MIT License.