What is committed, and why
On this page
You commit a file only if it can't be worked out again from today's source, or if it is a promise whose diff a person must read. Migrations, renames and the lock hold history that today's source no longer has. The API record is a promise to your clients. Acceptances are people's decisions. Anything else would go stale, conflict in merges and tempt people to edit it by hand, so it is a build output instead.
Record files
Every record but the lock is a .jadpo_record file, kept next to what
it describes:
api/main.jadpo_record, the API record;database/identities.jadpo_record, the identity registry;- each
translations/sources.jadpo_record; - each installed package's
surface.jadpo_record; - the root's
ignored_suggestions.jadpo_record, the suggestions a team keeps quiet (Suggestions), andtaught_suggestions.jadpo_record, the patterns it taughtjadpo suggest.
Each is in Jadpo's readable format, not JSON. Its first line says what generated it and which command updates it, as the API record's does:
// Generated by Jadpo from the routes of API main; update it with `jadpo api update`, never by hand.record api 1
Every record but the migrations is written in the
record format: the header line, then
record <kind> <format>, then its entries. jadpo.lock keeps its name.
Files people write, such as acceptances, are not records. A migration's
folder keeps its own files (Migrations).
How jadpo check keeps records up to date
You never edit a record by hand. jadpo check works out each record
again and fails while one disagrees with the source, naming the command
that brings it up to date. A hand edit is an error naming that command.
There are three such errors:
record.out_of_date: the record disagrees with the source.record.edited: Jadpo didn't write the record that way.record.format_newer: a newer Jadpo wrote it.
The API record is api/<api>.jadpo_record, a view rebuilt from the
routes, with no version: line. The OpenAPI document's version is the
first 12 hex digits of the SHA-256 of the record the routes give today,
which is the committed record whenever jadpo check passes. The
identity registry, the translation sources, the suggestion records and
jadpo.lock are ledgers of one-line entries
(Checking a record explains views and ledgers).
jadpo records
jadpo records lists every record, what writes it and whether it is up
to date, and names the command for any record that isn't. It groups
them by the command that writes them:
$ jadpo recordsjadpo api update✗ api/main.jadpo_record out of datejadpo rename, jadpo domain or jadpo fmt --layout✓ database/identities.jadpo_record up to date✗ records out of date 1 out of date · 2 records→ jadpo api update brings api/main.jadpo_record up to date
jadpo records update brings every record up to date at once. It is
also how a project moves a record from an older name (api/<api>.api,
database/identities.json) to its name now
(Checking a record).
Migrations
Migrations are records too. jadpo migrate new writes each folder in
database/migrations/: snapshot.json, postgres.sql and, when the
migration needs a person's decision, changes.jadpo. jadpo check
fails, naming the command to run, while the entities and the latest
snapshot disagree, while the latest migration waits for a decision, and
when a record can't be read. Each folder is a migration_snapshot node
in the manifest.
Folders at the root
The root of a project holds
api/, acceptances/, packages/ and jadpo.lock beside the rest.
packages/ holds the installed packages. They are committed, so every
upgrade is an ordinary diff. Each is checked against its digest in the
lock and is read-only, but only its surface.jadpo_record is a record.
Your app's own integrations, in shared/providers/, are source, written
like any other code (Integrations).
Checking a record
A record is one of two shapes:
- A view, such as the API record or a package surface, is rebuilt from the source and compared with the committed file byte for byte.
- A ledger, such as the identity registry, the translation sources, the suggestion records or the lock, is a list of one-line entries that hold history.
A hand edit of a view is told by its shape. Without the base
commit's build, a view that differs from what its writer gives today is
record.edited when Jadpo couldn't have written it, and
record.out_of_date otherwise. Jadpo couldn't have written a comment, a
fact outside the kind's closed list, a tab, a trailing space, a layout
Jadpo doesn't write, or a header other than the one it writes. Both
errors name the same command, which writes the record again, so a
careful edit of one fact reads as out of date.
A ledger is checked against its own rewrite. Each line must read, each key must appear once, and the file must equal what Jadpo writes from its lines: sorted, with the header in the newest format.
- Lines that git's
unionmerge left out of order arerecord.edited, andjadpo records updateputs them right. - Two lines for one key, such as one thing renamed two ways or one
package locked twice, are
record.editednaming both lines, and are left for a person to settle.
Checking that each line added since the base is one its command could write needs the base commit's build too.
jadpo records update brings every record up to date at once. It
writes each view from the source, rewrites each ledger, and moves a
record under its old name. It is the command an error's next: line
names for a ledger, because a ledger's own commands (jadpo rename,
jadpo translations update, jadpo suggest ignore) need arguments.
Old names are moved, not read forever. api/<api>.api and
database/identities.json are record.out_of_date ("the record's old
name"). jadpo api update and jadpo records update write the new file
and remove the old one. Until then the old registry's lines are still
read, so no key changes before the move.
Older formats are read and rewritten. ignored_suggestions and
taught_suggestions are at format 2, with the shared header. Their
format 1, with its own header line, is still read, and
jadpo records update rewrites it.
Records don't stop a program running. record.out_of_date and
record.edited are promises that jadpo check and CI hold you to, so
jadpo test and jadpo dev run past them. record.format_newer stops
them, because this Jadpo can't read what the project holds.
jadpo records shows each record's state as a symbol and a word
under the command that writes it: ✓ up to date, ✗ out of date,
✗ edited by hand, ✗ missing, ✗ old name, ✗ newer format, or
– not checked while the source has errors. Like jadpo check, it fails
while any record needs its command.
What some entries hold:
- The API record. An
api <name>entry carries oneauthentication <method>line per method, sorted, andidempotency required. The entry is left out when it has neither. A route'sgeneratedandcallerfacts come beforeinput. Enum variants are sorted like fields, asvariant <name>, and a variant's fields sit one indent deeper. Types sort by born name without their domain, then by name now. A failure's entry is itsstatus, then itsmessageas a Jadpo string, then its client fields. - The lock. A
gitsource records the repository, the tag and the tag's commit:package AcmeCouriers.v1 version 1.4.0 git github.com/acme/acme-couriers tag v1.4.0 commit <40 hex> digest sha256:<64 hex>. Aregistrysource isversion <v> registry digest <d>, and apathsource isversion <v> path <folder> digest <d>. Alinkis a live folder, so it has no version or digest:package AcmePricing.v2 link ../acme-pricing. Versions are written like1.4.0, with an optional pre-release. A commit is 40 or 64 lower-case hex digits. Lines sort by name, then by major version as a number. - A project that pins the Jadpo release it runs with has that pin as the
lock's first line,
jadpo version 0.1.2. The release has no pre-release, and it must be on the release lineapp.jadponames, such as 0.1. A pin on another release line isrecord.out_of_date(Which jadpo runs).
The record format
Every record is written in one small line format, an outline in Jadpo's own words. A change touches only the lines of the fact that changed, and git's hunk header names the entry the change is in.
The header. Line 1 is // Generated by Jadpo from <what it describes>; update it with <command>, never by hand. Line 2 is record <kind> <format>, where the kind is one of api, events, surface,
identities, translations, ignored_suggestions,
taught_suggestions and lock. Any other // line is a hand edit.
Nothing anywhere in a record changes on its own: it holds no timestamp,
compiler version, count, total, digest of the whole file or alignment.
Entries. A heading starts in column 0, with a word from the kind's
closed list (POST /todos, operation bookings.create). Its members
follow, indented four spaces, and a member's own fields, such as an enum
variant's, are indented eight. Nothing goes deeper. A member is one of
three things:
- a description,
/// text; - a field,
name: value, written as in source; - a fact, which starts with a lower-case word from the kind's closed
list:
status 201,fails todo_not_found,file types/booking.jadpo.
A list repeats its word, one item per line. A flag is a fact that is
present or absent (changes, idempotent, accepted), never
changes: true. A line's kind is known from its first characters, so a
tool can read a record without a Jadpo parser.
Views and ledgers. A view's entries are outlines. A ledger's entry
is its heading alone, one self-contained line, so git's union merge
can combine what two branches added. jadpo check then catches what that
merge gets wrong (Checking a record).
Values are Jadpo, written on one line as the formatter writes them in
source: names, dotted names, strings, numbers and type expressions
(Text { min_length: 1, max_length: 200 }). Paths and URLs are never
quoted, and a file path is relative to the record's folder. A digest is
sha256: and 64 lower-case hex digits. A hash is 12 hex digits, since it
isn't a security check.
Layout. UTF-8, LF line endings, a final newline, no trailing spaces,
no tabs, one space between tokens and no alignment, so a long new name
never re-indents its neighbours. One blank line follows the header and
separates entries that have members; one-line entries have no blank
lines between them. Strings use Jadpo's escapes and only those: \",
\\, \n, \r, \t. A description is the declaration's first
paragraph joined onto one /// line, so rewrapping the comment in source
changes nothing. Records are compared with CRLF read as LF.
Order is by identity. Everything is sorted; only a function's
parameters keep the order they are written in. Fields sort by name,
since field order is no part of a JSON contract. Repeated facts (fails,
host) sort by value. Where the identity registry has a line for a
thing, the thing sorts by its born name, so a rename changes lines in
place instead of moving a block.
| Record | Sections, in order | Within a section |
|---|---|---|
api |
api, routes, named types, failures |
Routes by path, then method; types by born name; failures by code |
surface |
package, provider, provides, operations, actions, polls, events, functions, messages, types, failures, recipes |
By born name; operations by resource.operation |
identities |
One | By born name, then kind |
translations |
One | By locale, then member |
lock |
One | The jadpo pin, then by package name, then major version |
Versions. Each kind has its own format number, starting at 1. Any
change to what a kind holds or how it is laid out raises it, additions
included, since a view is compared byte for byte. A view in an older
format is out of date, and its command rewrites it. A ledger holds
history, so Jadpo reads it in every format it has ever written for that
kind, and rewrites it in the newest. A record in a newer format than this
Jadpo writes is record.format_newer: Jadpo never guesses. The lock's
format 1 already holds the jadpo version pin, since no lock had shipped
before the pin was added.
The identity registry's lines are the kind, the born name, -> and the
name now. A field, variant or rule follows what it belongs to, so its
line gives only its own new name, and renaming Task again never touches
it:
entity Todo -> Taskfield Todo.priority -> importance
A translation source's line holds three facts: the hash of the default
wording it was translated from (from), the hash of its own wording when
that was recorded (wording), and accepted once a person accepted it
(Translations as messages).
Whether a translation is stale or changed is worked out from those, never
stored:
translation de_de.order_shipped from 3f9a1c0e2b7d wording 77b2e1d4c8a9 acceptedtranslation fr_fr.order_shipped from 3f9a1c0e2b7d wording a01c9e44f3b8
Records in git and in your editor
jadpo new writes these lines into a new project's .gitattributes.
linguist-generated=false keeps every record's diff expanded in a pull
request, since records are committed so that their diff is read.
merge=union is git's own merge driver for ledgers, and needs no set-up:
*.jadpo_record text eol=lf linguist-generated=falsejadpo.lock text eol=lf linguist-generated=false merge=uniondatabase/identities.jadpo_record merge=union**/translations/sources.jadpo_record merge=union
Editors get a record language: a TextMate grammar,
source.jadpo-record, in editors/vscode, and jadpo lsp serving a
record's errors, outline, file links and folding.
Not built yet: the base commit's build, which comes with jadpo approval, so every view is checked by its shape alone, and the record
language for editors (What is built).
Markdown for agents: /docs/what-is-committed-and-why.md.Built from Jadpo main at a707efb.