Jasmine GitHub issue

vibecode
{"vibecode": {
    "doc": "jasmine",
    "role": "spec for the Jasmine file format and storage layer — entries, required and conventional fields, the directory store, reaping, concurrency, purge. The Caspian API for writing entries lives in a separate doc.",
    "key_concepts": ["jsonl_derived", "log_entries",
        "corruption_tolerance", "directory_store",
        "reaping", "purge", "http_server_integration"]
}}

This document defines the Jasmine file format and its directory-store storage layer. The Caspian-side API for emitting entries — %chain.log[key] = value, function-call scope, nested frames, automatic exception and warning capture — lives in caspian.md.


Overview GitHub issue

Jasmine is a logging format derived from JSONL (JSON Lines) with a few tweaks specific to the Puck ecoverse.

Terminology GitHub issue

Primary use case: HTTP server logging. Jasmine was originally motivated by per-request logging for HTTP servers — capturing request/response data, errors, and other events from served sites. The design isn't HTTP-specific, though — Jasmine is suitable for any logging scenario where its tweaks (corruption tolerance, ecoverse-specific conventions) are useful. Application logs, audit trails, event streams, and similar log-shaped data fit naturally.

The JSONL baseline:

Jasmine adopts that baseline and adds format-specific tweaks for ecoverse needs. The tweaks are detailed below.


Differences from JSONL GitHub issue

Malformed lines are silently ignored GitHub issue

If a line cannot be parsed as JSON, Jasmine readers skip it without error. Strict JSONL implementations vary — some halt on a bad line, some raise — but Jasmine standardizes on quiet tolerance: bad lines are just skipped, and the next valid line is processed normally.

The motivation is practical:

The trade-off is silent data loss: corrupted lines just vanish. For a logging format that's acceptable — logs are append-only and we'd rather lose a few lines than the whole file. Producers that need strict guarantees about every line getting through should include their own integrity mechanisms (checksums in the JSON, sequence numbers, etc.).

Specification GitHub issue

Required fields GitHub issue

Every Jasmine entry must contain two fields:

The minimal valid entry contains exactly these two fields:

json
{
    "uuid": "[some uuid]",
    "timestamp": "[some timestamp]"
}

(Examples in this doc are pretty-printed across multiple lines for readability. In practice every Jasmine entry occupies a single line in the file — that's how JSONL works.)

Beyond these two fields, an entry can have any number of additional fields specific to the application using Jasmine (event type, request path, error message, structured data, etc.).

Format conventions:

Conventional fields GitHub issue

Beyond the required uuid and timestamp, Jasmine reserves certain top-level field names for specific kinds of structured content. These are conventions — not required, but if a producer uses them, they should follow the documented shape so consumers can rely on it.

success (outcome flag) GitHub issue

For operations that have a meaningful pass/fail outcome (a request handled, a job run, a transaction completed), the entry carries a top-level success field. The value is truthy or falsey — strict true/false is the norm, but any JSON value works since consumers read it as a boolean check.

Not all entries have a success field. Some kinds of logged events don't really have a pass/fail outcome (a startup notice, a periodic heartbeat, a structural marker). When there's no meaningful success/failure to record, leave the field off.

When present, success is conventionally the first field in the entry. It's the highest-signal piece of information for scanning a log — putting it first means grep, jq, and human eyes all see it before anything else.

The idiom: start false, flip to true on success. Set "success": false when the entry is created, then update it to true only if the operation completes successfully. The benefit is crash-safety — if the operation aborts (crash, kill, uncaught exception, anything that prevents the explicit flip), the entry remains false, which is the correct outcome. No special handling needed for failure cases; failure is the default.

json
{
    "success": true,
    "uuid": "[some uuid]",
    "timestamp": "[some timestamp]"
}

web (request/response data) GitHub issue

When Jasmine is used to log HTTP traffic — its primary use case — each entry carries a top-level web field. The web field contains exactly two sub-fields: request and response.

json
{
    "success": true,
    "uuid": "[some uuid]",
    "timestamp": "[some timestamp]",
    "web": {
        "request": {
            "method": "GET",
            "path": "/some/path",
            "host": "example.com",
            "...": "..."
        },
        "response": {
            "status": 200,
            "duration_ms": 47,
            "bytes": 12480,
            "...": "..."
        }
    }
}

The two sub-fields are paired by structure — every web entry has both. request holds details of what came in; response holds details of what went out.

The detailed shape of request and response (which fields are standard, which are optional, privacy considerations for things like IP/user-agent, etc.) is its own spec — to be filled in as the HTTP server's logging needs become more concrete.

Writing entries from Caspian GitHub issue

The Caspian-side API is one line:

%chain.log['user_id'] = $user.id

The function call is the entry boundary; nested calls produce nested frames; the framework collects the outermost entry. The full Caspian API — function-call scope, nested frames, automatic exception and warning capture, role-boundary behavior — is documented in caspian.md. This document concerns the file format and the storage layer only.

Logger failure cascade GitHub issue

Failure to log never stops the process. A logging system that crashes the program when it can't record is worse than one that quietly drops the entry — and quietly dropping the entry is also unacceptable. Jasmine takes the middle path: the process keeps running, and the failure raises a warning through Caspian's warning system. That's what warnings are for.

If Jasmine itself fails to record (downstream service down, disk full, store rejected the entry, etc.), the original event is not silently swallowed. Jasmine raises a warning whose payload carries both the failure reason and the original event that couldn't be recorded, so whatever is observing warnings has everything it needs to recover the entry.

Jasmine does not write to stderr or any other destination directly. The warning system decides where warnings go — stderr, a log of its own, a separate channel, dropped if no observer is configured, whatever the operator has wired up. That routing is not Jasmine's concern.

This prevents the classic "the bug that broke logging was the bug we needed the logs to find" scenario. The process keeps running through every layer of this cascade — Jasmine never raises the failure as an exception to the calling code.


Stores GitHub issue

Jasmine separates what to log (entries, the format) from where they live (the store). Stores are pluggable; one Jasmine producer can be configured with multiple stores, and each entry fans out to all of them.

A store is anything that holds Jasmine entries — handles writing, and where applicable reading. The architecture is built around an abstract store class with concrete subclasses for each destination kind:

Only the directory store ships in v1. The others remain pluggable extension points; community or future work can fill them in.

Constructing a Jasmine log GitHub issue

There is one class — puck.uno/jasmine — for all Jasmine logs. The constructor takes keyword arguments that configure which store(s) the log uses. Each keyword names the kind of store:

$log = %puck['https://puck.uno/jasmine'].new(dir: '/path/to/directory')

The example above produces a single log object with one store — a directory store pointed at the given path. Other store kinds use different keyword names (db:, webhook:, etc.) once their implementations land. For v1, only dir: is supported.

The shortcut form (one keyword arg, sensible defaults for everything else) is what most users will use. The underlying setup is more elaborate (file rotation, naming, etc. — details below); defaults cover the common case.

A log with no stores raises a warning when an entry is created on it. Constructing a Jasmine log with no store keyword arguments is syntactically allowed (the object exists, you have a reference), but the moment something tries to create an entry through it, a warning fires — there's no destination for the entry to land in, so the entry would be dropped silently. The warning surfaces the common misconfiguration of forgetting to wire up a store. Silently dropping entries would be worse than nagging.

Suppressing the warning: no_writers_ok. A log object exposes a no_writers_ok property (also settable via constructor keyword) that quiets the no-stores warning. When no_writers_ok is truthy, creating an entry on an empty log is silent — entries are still dropped, but the framework trusts the developer made that choice deliberately:

$log = %puck['https://puck.uno/jasmine'].new(no_writers_ok: true)
# or
$log.no_writers_ok = true

This is an example of a "Don't worry nanny" feature: the framework's default behavior is to warn about a likely mistake, but the developer can flip a flag to indicate "I know, this is intentional, hush." Part of Mikobase's no-nanny-code philosophy — the nanny is on by default to catch real bugs, but it doesn't override developer choice when the developer explicitly opts in.

Directory store: file layout GitHub issue

The directory store organizes entries by calendar date. Each day's entries go into a file named with the date:

2022-04-01.jasmine
2022-04-02.jasmine
2022-04-03.jasmine

When an entry is to be written, the directory store appends it to the file for the current date. If the file doesn't yet exist, it is created automatically. No manual setup required; the directory just needs to be writable.

At midnight (local-date rollover), entries start going to the new day's file automatically. No explicit rotation logic; the rollover falls out of the naming scheme.

Finer-grained naming (per-hour, per-size, per-process, etc.) is not in v1. The daily-rollover scheme is intentionally simple — it covers the common case and produces readable, navigable directories. If real demand emerges for more granular naming, we can layer it on without breaking the daily default.

Time zone is intentionally left unspecified for the date in the filename. The filename's date is for coarse grouping; precise time information lives on each entry's timestamp field, which carries a full timestamp including time zone. The filename's date would be redundant information at best. If a real need emerges to pin filename-date semantics (e.g., to enforce UTC across a fleet), we can address it then.

Reaping GitHub issue

The directory store supports a reaping pattern: a routine that walks the file looking for unreaped entries, yields each to a closure, then marks the entry as reaped. Reaping marks an entry by replacing its leading { with #. The line becomes unparseable JSON, but otherwise intact:

Before:  {"timestamp": "...", "uuid": "...", "web": {...}}
After:   #"timestamp": "...", "uuid": "...", "web": {...}}

On the next pass — and to any other Jasmine reader — the reaped line is silently skipped via the malformed-line tolerance rule. The line itself signals its reaped state; no separate flag, no external tracking. The reaped line remains readable for forensic inspection (you can still grep or eyeball it), it's just no longer processed as an entry.

Idempotent: running reap twice doesn't double-process anything. Single-byte change preserves the file's overall structure (no length shifts, no offset corruption).

Concurrency: the format's emergent gift GitHub issue

Because reaping only modifies single bytes in the middle of the file (and only on lines that aren't being touched by writers), the reaper does not need an exclusive lock on the log file. Other processes can be appending entries at the same time without coordinating with the reaper. Concretely:

This is "lock-free reads, brief append locks, no read/write contention" — a really nice concurrency property that falls out of the format design rather than being engineered separately.

Notes on edge cases:

Reaper coordination GitHub issue

To prevent multiple reapers from processing the same entries, the directory store uses a sentinel lock file named reap.lock inside the log directory:

2022-04-01.jasmine
2022-04-02.jasmine
reap.lock
write.lock

Every reaper must acquire an exclusive lock on reap.lock before reaping. The lock serializes reapers across processes (and across machines, where the filesystem supports advisory locking). The file's contents don't matter — it exists purely as a lock target. The store creates it automatically if it doesn't exist.

Default behavior: non-blocking. If a reaper can't acquire the lock (because another reaper is already running), it doesn't wait — it just moves on with whatever else it was doing, skipping reaping for this cycle. The reasoning:

Why a separate file rather than locking the log file itself:

The non-blocking default can be overridden — a reaper can be configured to block on the lock if a use case actually needs strict serialization (e.g., the next reap cycle won't run for a long time). Exact API for this TBD.

Purge GitHub issue

The reaper marks entries as processed but does not delete files. Over time the directory accumulates files whose entries are entirely reaped — they parse to nothing, take up disk space, and clutter the listing. A separate purge routine handles cleanup.

Purge is conceptually distinct from reaping:

The two routines have different responsibilities and run on different schedules. A typical setup might run reaping frequently (every few minutes) and purge much less often (once a day, or once a week). They don't need to coordinate with each other directly.

Locking GitHub issue

Purge acquires write.lock per file, not for the whole purge run. For each candidate file:

  1. Acquire exclusive lock on write.lock.
  2. Scan the file — does any line parse as a Jasmine entry?
  3. If no: delete the file. If yes: leave it alone.
  4. Release the lock.
  5. Move to the next candidate.

The reason for per-file granularity: if a purge cycle is working through a large backlog (hundreds or thousands of stale files), we don't want it to tie up writers for the entire sweep. Per-file acquire/release keeps any individual writer's wait short — at most the duration of one file scan, which is fast.

Why write.lock rather than a separate purge lock:

By default purge blocks when acquiring write.lock for each file (unlike the reaper's non-blocking default on reap.lock). Purge runs rarely and needs to complete its work; missing a purge cycle isn't useful. The wait is brief in practice because each lock acquisition only spans a single file's scan + possible unlink.

What counts as "empty" GitHub issue

A file is eligible for purge when no line in it can be parsed as a Jasmine entry — every line is either malformed, a reaped entry (leading #), or some other non-JSON content. The check is the same one any Jasmine reader does; if iterating the file yields zero entries, the file is empty from Jasmine's point of view and can be removed.

This definition handles the common edge cases naturally:

Configurable: should today's file be deletable? Both purge modes expose an option for whether the current-date file is eligible when empty. The defaults differ:

Either default can be flipped by configuration if a user has a reason to want the opposite behavior. The current-date file becomes eligible regardless once the date rolls over.


Open Questions GitHub issue

The Jasmine spec is intentionally kept light at this stage. The big structural decisions are settled (JSONL baseline, malformed-line tolerance, required uuid/timestamp, conventional web field, ambient %chain.log, nested call frames, role-boundary security via nesting). Further format development is deferred — the rest will be refined through real use rather than upfront design. As Robinson and other consumers actually exercise Jasmine, the format will evolve in response to concrete needs that surface.

Specific items deferred:

Each will get addressed when there's a concrete need driving it.

© 2026 Puck.uno