Aleph symbol

THE INFINITE ARCHIVE

formless and empty

I saw all the mirrors on the Earth, and none of them reflected me.

Fingerprint: 6aefe41 — Drift: 1d · 7.60h · +1.0h · 708 wc

Refactoring Seraph

Making the revision system own its data.

Created: 2026-01-12
Domain: Development
Division: Design
Modified: 2026-01-28
RSS: RSS

Overview

I want to split Seraph from Ordinal, and make it its own module.

Right now the revision system is smeared across revisions.py, commit_flow.py, and a handful of places in the renderer and context builder. Multiple parts of the system know a little bit about how revisions work, and they all make assumptions about each other. Revision data gets created in one place, interpreted in another, and finished somewhere else.

The database path is derived from content_dir and hardcoded to content/.revisions/revisions.db. That puts build state inside the content tree and makes it look like authored material.

The sqlite connection uses row_factory = sqlite3.Row, and those rows get passed straight out of the revisions code. Callers then poke at them using string keys like row["worked_hours"], row["meta_json"], and row["timestamp"]. At that point the schema is no longer private. Every caller has to know column names, which fields can be missing, and how nulls behave. That knowledge is duplicated all over the codebase.

What I actually want is very simple. The revision system produces two kinds of data: commit records and article cache records. Each of those needs a defined structure. The same fields should exist every time, with the same names and the same kinds of values. Type coercion, defaults, and parsing should happen exactly once when the data is loaded. If the representation is dicts, the keys need to be consistent. If it's dataclasses, they still need a clean way to turn into dicts for templates and JSON output.

Anything that depends on commit history, previous values, or stored metadata belongs in the revisions code. commit_flow should not be inspecting raw rows to figure out previous totals. The renderer should not be parsing meta_json or knowing how revision fields are stored. Nothing downstream should be deriving values from raw storage fields. They should just receive data that is ready to use.

More to come...

2026-01-13

"I want to split Seraph from Ordinal, and make it its own module."

By 'split' I mean split it at the code level first: its own package, its own API, no imports from Ordinal. It can still live inside the Ordinal repo until it's stable enough to extract.

I think this can be done in two phases, where Seraph lives inside Ordinal but can be lifted out later, and then we can turn it into an external dependency.

Since right now Ordinal is basically a single Python app with modules under ordinal/src/*.py. I think the easiest lift is to create a real package directory under ordinal/src/seraph and start moving code there so Ordinal can keep running.

File Structure

ordinal/
  src/
    ...
    seraph/
      __init__.py
      api.py
      models.py
      db.py
      queries.py
      compute.py
      paths.py

Then we migrate src/revisions.py into that package:

src/revisions.py either disappears/becomes thin compatibility shim that imports from seraph.api, and we re-export old function names for a time...

src/commit_flow.py, src/context_builder.py, src/html_renderer.py, src/taxonomy.py stop importing src.revisions and start importing seraph.api and seraph.models

I think this will lead to a clear home for Seraph with more private internals and a small public interface without having to change how we run main.py today.

I think its important that Seraph does not import anything from src.base_utils or other Ordinal modules. If it needs config like content_dir, we pass it in from Ordinal so we keep dependency direction cleaner.

Let's see how this ends up going.

seraph/api.py

This is the only thing applications should import. It should remain small and act as a stable interface.

seraph/models.py

Dataclasses or TypedDicts for things Seraph returns such as CommitRecord, ArticleCacheRecord, maybe WorkStats

seraph/db.py

Connection creation, schema init, transaction helpers. Sqlite related code.

seraph/queries.py

SQL and row mapping. This is where sqlite3.Row gets converted into dataclasses/dicts. Nothing else sees rows.

seraph/compute.py

Derived values: drift, worked deltas, parsing meta JSON into dict, coercions.

seraph/paths.py

DB path resolution rules. But the API should allow the caller to pass an explicit path, so Ordinal can decide.

seraph/migrate.py

Any schema migrations / seeding logic.

2026-01-14

So far there is a branch with a small Seraph package under ordinal/src/seraph/.

I removed the service object. The API is now plain functions that take an explicit db_path. The behavior is more obvious and makes Seraph easier to move out later.

get_latest_commit(db_path, slug) opens the database, runs one query, converts the row to a CommitRecord, and returns a plain object.

CommitRecord is a frozen dataclass with a fixed schema. Meta JSON is parsed once at construction. Callers get a dict, not raw JSON. Nothing outside Seraph touches sqlite3.Row.

context_builder now consumes CommitRecord objects. No more, json.loads on call sites or row["meta_json"].

Attribute access is replacing string keys now.

66578fb8e9da437aa6f8ac29a24ff3a3

Connections

Outbound 2
Inbound 0

Outbound

Revisions

MODIFIED 6aefe41 2026-01-26 03:57:03
Spell check
MODIFIED 09b2a8a 2026-01-14 10:25:28
3.7
MODIFIED e082400 2026-01-14 03:29:34
added content laying out refactoring phases
MODIFIED aaea7f3 2026-01-13 07:28:20
fix typos, better explanation in the intro
MODIFIED aaa7cc5 2026-01-13 07:24:39
Added article