Timestamp versioning GitHub issue

The date-pinning model and the %puck.era surface that sets cutoffs in user code.

vibecode
{"vibecode": {
    "doc": "timestamp_versioning",
    "role": "canonical reference for Caspian's date-pinned versioning — the rationale (one date governs the whole library tree, reproducibility comes free), the %puck.era surface (block form, era handle, confine), the lookup resolution rules, the out-of-range alarm, the relationship to the blockchain, and what this model replaces",
    "audience": "Caspian programmers writing user-role code that needs to constrain library version selection by date, plus implementers needing to understand the resolution machinery and alarm semantics",
    "key_concepts": ["date_pinning_primary_versioning_axis",
        "puck_era_surface_block_or_handle", "confine_installs_era_as_active_puck",
        "resolution_walks_fetchers_returns_latest_in_range",
        "out_of_range_is_security_alarm_not_dependency_error",
        "blockchain_anchors_dates_when_present",
        "replaces_manifests_lockfiles_constraint_solvers"]
}}

%puck.era constrains library lookups to artifacts within a specified timestamp range. Where per-URL timestamp narrowing (also on this page) narrows one specific library by date, and the per-call kwargs narrow one call, an era narrows the whole library tree.

This page covers the era surface plus the surrounding date-pinning model: why date-based versioning is the primary axis, how the resolver picks a version given an active range, what happens when no candidate fits, and how the blockchain anchors dates when it's in play.


At a glance GitHub issue

The three common patterns for narrowing library version selection by date.

Block form — narrow every lookup inside a block:

caspian
%puck.era(min: '2023-08-12', max: '2023-09-01') do
    $gup = %puck['https://foo.bar/gup']      # narrowed to the era
end

Per-call kwarg — narrow one specific lookup:

caspian
$gup = %puck['https://foo.bar/gup', ts_max: '2023-09-01']

Per-URL timestamp — narrow one library, live-global:

caspian
%puck.config('foo.bar/gup').timestamp = '2023-08-12'
$gup = %puck['https://foo.bar/gup']           # uses the configured timestamp

Surfaces intersect: per-call narrows within whatever the block form / per-URL config already established. The lookup picks the latest artifact whose effective_date falls within every active range. If no candidate fits, puck.uno/error/out_of_range raises.

Read on for the full model, the resolver semantics, and the relationship to the blockchain.


Why date-pinning GitHub issue

The model is built around a simple observation: if your tests passed on 3 May, the library tree as it existed on 3 May is the tree your code is known to work with. Pinning to that date in production is the most direct expression of that fact.

Three concrete benefits:

  1. One number replaces a tree. Instead of declaring versions for every direct dependency and hoping the transitive closure resolves consistently, a single date governs the whole graph. No constraint solver, no resolution algorithm, no "transitive version conflict" diagnostics. The cutoff propagates through the call stack, so a library twenty calls deep gets the same date as the top-level program.

  2. Reproducibility comes free. The cutoff is the lockfile. As long as the date is recorded with the deployment, the library tree can be reproduced exactly.

  3. Testing simplifies. Run tests on date X. Set the production cutoff to X. Ship. Anything published after X is not in your runtime tree, by construction.


The %puck.era surface GitHub issue

Three forms, same underlying mechanism: a block form for inline narrowing, an era object you can hold and reuse, and a confine block that installs an era as the active %puck for transparent narrowing.

Block form — inline narrowing GitHub issue

The simplest case: narrow every %puck lookup inside a block.

%puck.era(min: '2023-08-12', max: '2023-09-01') do
    $gup    = %puck['https://foo.bar/gup']      # narrowed to the era
    $other  = %puck['https://baz.io/widget']    # also narrowed
end

Inside the block, every lookup through %puck is filtered to artifacts whose effective_date (falling back to posted) falls within [min, max]. After the block exits, the previous era state is restored.

Both min and max are accepted; either or both can be omitted, leaving that end unbounded:

%puck.era(min: '2023-08-12') do ... end                       # only lower bound
%puck.era(max: '2023-09-01') do ... end                       # only upper bound
%puck.era(min: '2023-08-12', max: '2023-09-01') do ... end    # both

Inner blocks can only narrow. A nested era must lie within the enclosing window — widening is rejected with puck.uno/error/era/widen.

Block-scoped means the era does not cross role boundaries — see roles.md. Each role starts with whatever era the engine installed at the boundary (typically none, or a deployment-wide upper).

Era object — explicit handle GitHub issue

Get an era object, configure it, use it directly for lookups:

$era = %puck.era                        # new era handle, both ends unbounded
$era.min = '2023-08-12'
$era.max = '2023-09-01'

$gup = $era['foo.bar/gup']              # lookup through the era — narrowed

$era behaves like a derived %puck: you can call $era[url] exactly like %puck[url], but every lookup carries the era's bounds.

Like all the config surfaces in this directory, the era handle uses the dual-path pattern: assign-the-whole-thing or tweak-a-property.

$era = %puck.era                                              # construct
$era = %puck.era(min: '2023-08-12', max: '2023-09-01')        # construct + set both
$era.min = '2023-08-12'                                       # tweak one bound
$era.max.cmp = '<'                                            # exclusive upper

.min / .max are autovivified the same way as elsewhere; default state is unbounded; .cmp defaults are inclusive (>= for .min, <= for .max); validity rules per bound match the canonical bound-operator reference.

Holding an era as a variable is useful when you want to thread the same constraint through many lookups, or pass it as a parameter to a function that does its own lookups.

confine — install the era as the active %puck GitHub issue

When you have an era and want code inside a block to use %puck as if it were that era — without rewriting every call site — use confine:

$era = %puck.era
$era.min = '2023-08-12'
$era.max = '2023-09-01'

$era.confine do
    $gup = %puck['https://foo.bar/gup']     # %puck behaves as $era inside this block
    do_more_work()                  # any %puck lookup inside also narrowed
end
# After the block: %puck is back to whatever it was before.

confine is the bridge between the era-object model and the block-scoped model: build the era however you want (one place), then any code that takes %puck for granted picks up the constraints transparently inside the block. Useful for narrowing third-party code paths without modifying them.


Per-URL timestamp narrowing — %puck.config(url).timestamp GitHub issue

Where %puck.era narrows every lookup, %puck.config(url).timestamp narrows just one specific library by date. Useful when you want to pin one library to a specific window without affecting anything else — testing a deployment with one library held back, isolating a known-good revision of one specific dependency, etc.

$config = %puck.config('foo.bar/gup')
$config.timestamp = '2023-08-12'                                  # pin to one calendar day
$config.timestamp = {min: '2023-08-12', max: '2023-09-01'}        # range
$config.timestamp.max = '2023-09-01'                              # only upper bound; min unbounded
$config.timestamp.min.cmp = '>'                                   # strictly after

The %puck.config(url) handle is live global state for that URL: setting a property takes effect immediately, two handles into the same URL share state, and the $config variable is purely convenience. Full semantics of the handle live in semver.md § The config object is a live handle, not a snapshot — same handle, same lifecycle, just configuring a different axis.

.min / .max autovivify; bound operators use the same .cmp system as everywhere else in the area. The canonical reference for the bound-operator system lives in semver.md § Bound operators (cmp); the per-URL timestamp axis reuses it without re-spec'ing it. Defaults are inclusive (>= for .min, <= for .max); the same validity rules per bound apply.

A bare timestamp value (e.g. '2023-08-12') is interpreted as a pinmin = max = '2023-08-12', both inclusive. To express a window, use the range form (hash or property tweaks).


Resolution rules GitHub issue

For a lookup %puck['https://foo.com/bar'] under an active timespan [L, U]:

  1. The puck walks its fetchers (per puck.md § Lookup Mechanism), each consulting its faucets (cache first, then remote source typically). Each fetcher reports the latest version of foo.com/bar within [L, U] that it has.
  2. The puck returns the latest result across all fetchers' responses. Finding the latest requires consulting all fetchers, not short-circuiting on first hit.
  3. In a future release, the puck will check the signature of the library against a key library. That feature is not in initial development.
  4. If no fetcher returns a match, the out-of-range alarm is raised.

Each (URL, version, date) triple is its own cached artifact. Different programs running through the same engine under different cutoffs will each get the appropriate version for their cutoff; no program's lookup affects any other's.

The "canonical date" of a library is whatever the provider has authoritatively recorded. For a blockchain-backed provider, this is the posted timestamp on the chain (or the effective_date, if explicitly set; see Relationship to the blockchain below). For a plain HTTPS provider, this is whatever the provider asserts — the date is no stronger than the trust placed in the provider.

The same machinery applies when other constraint surfaces are active: %puck.config(url) constraints intersect with the era; per-call kwargs intersect further. In all cases the resolver picks the latest candidate satisfying every active constraint.


Out-of-range alarm GitHub issue

When a library lookup returns nothing dated within the active [L, U] timespan, the engine raises puck.uno/error/out_of_range. This is an alarm under the role model (see roles.md — Exceptions and Alarms): always fatal, no unwinding, no finally blocks, no catch handlers from Caspian code. The engine takes over directly.

This is not a "missing dependency" error. If the program was tested under cutoff X and is now running under the same cutoff, every library it calls should resolve. An out-of-range exception means one of the following has happened:

In each case, the integrity of the deployment is in question. The exception is therefore treated with the same severity as any other security exception, not as ordinary control flow.

The same alarm fires when per-URL timestamp narrowing or per-URL semver constraints intersect with the era to produce an empty candidate set, and when per-call kwargs rule out every candidate the broader surfaces would have allowed. The integrity argument applies identically in those cases.

Forensic payload GitHub issue

The alarm carries a structured payload describing exactly what happened:

This is the information a security responder or audit log needs to investigate.


What this replaces GitHub issue

The model intentionally starts without:

The design starts from a position of not needing these mechanisms. Adding any of them later is possible — but each one increases the burden on every script (versions to track, manifests to maintain, conflicts to resolve), so the bar for introducing them should be high.


Relationship to the blockchain GitHub issue

The Puck blockchain provides a cryptographically anchored posted timestamp for every library version. When a chain is available, the cutoff is genuinely tamper-evident — a library's date cannot be forged.

When the cutoff is enforced against non-chain providers, dates are only as trustworthy as the providers themselves. The model still works — the engine still picks the latest version on or before the cutoff — but the integrity guarantee is weaker.

The date-pinning model itself does not require a blockchain. It is the simpler primitive; the chain is one implementation of trustworthy dates.


Composition with other narrowing surfaces GitHub issue

%puck.era is one of several constraint surfaces. At any lookup, all active constraints intersect:

Surface Axis Scope Set via
%puck.era (this page) timestamp Puck-wide, block-scoped or handle %puck.era(min:, max:) do ... end / %puck.era handle
Per-URL timestamp (this page) timestamp One URL, live-global %puck.config(url).timestamp = ...
Per-URL semver semver One URL, live-global %puck.config(url).semver = ...
Per-call kwargs both One call %puck['url', ts_min: '...', semver_min: '...']

At lookup time the resolver checks every active constraint. If their intersection is empty for a given URL, the lookup raises puck.uno/error/out_of_range. Each narrowing surface can only constrain further; none can expand what an outer surface already permits.

Bounds use the same operator system everywhere — .cmp per bound with >=/> / <=/< validity rules (call-site kwargs always inclusive) — so intersecting them is purely a numeric exercise on the resolver side.


See also GitHub issue


Open questions GitHub issue


© 2026 Puck.uno