Patch format
A patch is one JSON file with two nested layers. Both are checked in full before anything is applied, and an envelope that does not match is refused rather than partially landed.
Write the file, then hand it to your operator. It goes into their Downloads folder, where the connector notices it within a few seconds.
What you need before writing one
Three of these come from the operator and cannot be guessed; the fourth you choose yourself.
- interaction_id and authorization
- Minted together on the Agent access panel at llambda.net. They are checked as a pair, so a correct token beside an unfilled id fails exactly like a wrong token. Minting again revokes the previous pair.
- The collection id
-
A symbolic name the operator registered, such as
tests— never a filesystem path. They can read it fromcollectionsat thellambda>prompt. - The gizmo and gadget names
-
Both name the project inside that collection, and for a scaffolded
gizmo they are the same word. Neither has to exist yet: ask for
new-gadgetin the same patch and it is created first. - A patch id you have not used
- Any unique string. Reusing one is how a corrected patch is mistaken for the patch it corrects, and the second is then ignored as already seen.
A patch that changes one line
This is the whole of an ordinary patch. It assumes a collection
registered as tests holding a gizmo hello-patch
scaffolded by new-gadget, and it changes what the
--hello route prints:
{
"schema": "LLAMBDA_DOWNLOAD_PATCH_V1",
"interaction_id": "<from the Agent access panel>",
"patch_id": "hello-greeting-1",
"authorization": "<from the same panel>",
"target": {
"collection": "tests",
"gizmo": "hello-patch",
"gadget": "hello-patch",
"agent": "edd"
},
"patch": {
"format": "LS_FORK_JSON_PATCH_V1",
"agent": "edd",
"title": "Change the hello greeting",
"files": [
{
"op": "upsert",
"path": "hello-patch.gizmo/hello-patch.wires/projects/hello-patch/src/hello-patch/lib/HelloPatch.hs",
"content": "module HelloPatch where\n\ngo :: String -> [String] -> IO String\ngo \"--hello\" _ = pure \"hello patches!\"\ngo route _ = pure (\"unknown route: \" ++ route)\n"
}
]
}
}
Three things in it are worth copying exactly. collection is
the registered id, not a path. path is relative to the
collection root with forward slashes, and names the module the scaffold
wrote. content is the entire replacement file — there is
no diff operation, so a patch that means to change one line still carries
every other line with it.
Drop it in the operator's Downloads folder. The connector validates it, applies and commits the edit, then compiles and installs. What the compiler said comes back in the result.
Ask the operator to run it. A result saying the patch compiled is not the same as the program doing what you meant, and this is the only step that tells you which you got:
> cd C:\projects\hello-patch.gizmo\hello-patch.wires
> onepush.bat
...
Test suite hello-patch-cog: PASS
> bin\hello-patch.exe --hello
hello patches!
The envelope
{
"schema": "LLAMBDA_DOWNLOAD_PATCH_V1",
"interaction_id": "<your interaction id>",
"patch_id": "<unique per patch>",
"authorization": "<token>",
"target": {
"collection": "<registered collection id, e.g. work>",
"gizmo": "<gizmo>",
"gadget": "<gadget>",
"agent": "<your name>"
},
"patch": {
"format": "LS_FORK_JSON_PATCH_V1",
"agent": "<your name>",
"title": "<one line>",
"files": [
{ "op": "upsert", "path": "<collection-relative>", "content": "<whole file>" },
{ "op": "move", "from": "<old path>", "path": "<new path>" },
{ "op": "delete", "path": "<collection-relative>" }
]
}
}
File operations
Every entry in files names an op. Each one is
decided entirely by the patch: nothing depends on what the file currently
holds, which is why there is no diff or search-and-replace operation. The
same patch is replayed later when lanes are amalgamated, and an operation
that read the existing text would produce something different on replay
and conflict on a file nobody edited.
- upsert
-
pathandcontent. Writes the file, creating parent directories. The content is the whole file, not a diff and not a fragment, and its bytes are written exactly as sent — so send a file in one line-ending form, never part CRLF and part LF. - delete
-
path. Removes one file. An already-absent path is not an error: the operation states the end result, and a replay over a tree that has since dropped the file is ordinary. - copy
-
fromandpath. Copies bytes from one path to another without sending the content. The source must exist when the patch lands. - move
-
fromandpath. Copies then removes the source. This is how a module is renamed: one entry, no content over the wire, and the rename stays legible as a rename in the history. - delete-tree
-
path. Removes a directory and everything under it. This is how a gizmo is abandoned — listing its files individually requires knowing the whole tree, and quietly leaves behind anything the author did not know about.
Operations
Files are not the whole of what a patch can ask for. Scaffolding a new
gizmo means running the generators, which no arrangement of file writes
expresses. An operations array, beside patch in
the envelope, asks the connector to do it:
"target": {
"collection": "<registered collection id>",
"gizmo": "widget",
"gadget": "widget",
"agent": "<your name>"
},
"operations": [
{ "op": "new-gadget", "name": "widget" }
],
"patch": { ... files that edit widget.gizmo ... }
- new-collection
-
name. Establishes a new collection of that name beside the one the envelope targets, using the toolchain the connector ships, and registers it so later patches can target it by the same name. The directory must not already exist — that is what keeps this safe: the operation only ever works in a folder it just created, so nothing already on the machine can be read or written. A name that collides with anything is refused, and a failure part-way removes what it made rather than leaving a half-built tree behind. - new-gadget
-
name. Scaffolds a buildable gizmo of that name in the collection named bytarget. The name is symbolic — letters, digits, dash and underscore — and becomes both the gizmo directory and its module. Succeeds without changing anything if the gizmo is already there.
This is not a shell. The connector runs a fixed set of its own
commands, and every argument is a symbolic name rather than a path: a
gizmo name is letters, digits, dash and underscore, and the collection is
the one already named in target. A patch cannot name a
directory, cannot name a program, and cannot reach a collection its
authorization did not admit it to.
Operations run before the files land, so one patch can create a gizmo and populate it. They are also idempotent: asking for a gizmo that already exists succeeds and changes nothing, which is what lets a patch be resent after a failure further along.
Starting a new gizmo
Name the new gizmo and gadget in target — neither has
to exist. Ask for new-gadget, and the scaffold it writes is a
gizmo that compiles, installs and passes its tests before you have edited
anything:
<name>.gizmo/
<name>.gadgets authority: the gadget and its junction
<name>.wires/
<name>.wire name, projects folder, bin folder
onepush.bat build, install, test
projects/
wire.project which packages this gadget holds
<name>/src/<name>/
package.nut dependencies, modules, go-is entry point
package.garrage the build-stage view of the same
package.cog the test suite
package.bolt what the executable links
buttons.plan the lifecycle phases
lib/<Module>.hs your code
Then upsert your own module over
lib/<Module>.hs and add whatever else the project needs
in the same patch. Everything else in that tree is generated on the first
onepush and regenerated on every one after, so leave the
.cabal, the entry point and the test cog alone.
Landing builds it for you. Once the files are applied and committed,
the gadget is compiled and installed, and what the compiler said comes
back in the result — so a patch that creates a gizmo tells you in
one round whether it compiles. The build is install-only, so run
onepush.bat yourself when you want the tests too.
Rules that cost a round when broken
- collection is a name, not a path
-
It is the id the operator registered a collection under —
work, say — and the connector resolves it to a directory on their machine. A filesystem path here is refused, and so is an id nobody has registered. Ask your operator which id to use: they can read it fromcollectionsat thellambda>prompt. Note that one machine usually holds several collections side by side, so the folder containing them is not itself a collection. - interaction_id is not the token
- Both are checked, and they are checked together. A correct token with an unfilled placeholder id fails exactly like a wrong token, so resealing the token never helps. Fill in the id.
- Every file entry needs op
-
Either
upsertordelete. Nothing else is accepted, and an entry without one is rejected before the rest of the envelope is read. - Paths are relative to the collection root
-
Not to the gadget. A file in the paint gadget starts
paint.gizmo/paint.wires/. Forward slashes only: a path containing a backslash or a colon is refused, as is one that is absolute or that contains a.or..segment. - content is the whole file
-
Not a diff, and not a fragment.
deleteentries carry no content. - patch_id must be new each time
- Reusing one is how a corrected patch gets mistaken for the patch it corrects, and the second one is then ignored as already seen.
- No extra top-level fields
- Unknown fields are refused by name. The refusal reads as though the whole patch is wrong, when usually one field is misspelled or one layer is missing its wrapper.
- semantic is optional
- It names the kind of edit, for a person and for Elliot's templates. The lander does not read it, so leaving it out is fine and including it is accepted.
Limits
- At most 200 file entries in one patch.
- Each path at most 512 characters.
- The authorization token between 32 and 256 characters.
- Collection, gizmo, gadget, agent, interaction id and patch id are symbolic names: letters, digits, dash and underscore.
Adding a Haskell module
The build is descriptor-driven. A module that is not declared will
compile on your machine and fail in the collection, so a patch adding one
must also patch package.nut — the module under
exposes:, and any new package under depends:.
Do not patch the generated .cabal file: it is written by
wires cast from package.nut, and a hand-edited
one is skipped by the generator from then on.
What comes back
- land_ok: false
- It never landed. The patch did not apply, the target was wrong, or the envelope was malformed. Nothing was built, so there are no compiler diagnostics to read.
- land_ok: true, build_ok: false
-
It landed and would not compile. Read
diagnostics; they are the compiler's own words, capped at the first ten useful lines. - ok: true
-
Landed, compiled and installed. The build that runs on landing is
install-only, so this does not say the test cog passed — run
onepushfor that.