Skip to content
jackskip22Public

Latest commit

 

History

4 Commits

Folders and files

Repository files navigation

Will

Will marks which document regions an agent may edit, append to or leave unchanged. Permissions travel inside Markdown, Word, PDF and Google Docs, with no registry, sidecar or account. The JavaScript reference runs in Node.js with no dependencies.

Rapier uses Will for live editing with agents over MCP and WebMCP. Rapier is a fast, phone-first Markdown editor for notes, diagrams, drawing and watercolor painting, in one offline HTML file for Android, Web and Windows.

  • Markdown uses hidden comment lines. DOCX, PDF and Google Docs use their own carriers.
  • An unreadable marker makes the whole document keep.
  • The JavaScript, Python and Go readers use only their standard libraries; the vectors test conformance.

Use Rapier

  1. Work live with a person. Connect to https://mcp.rapier.website/mcp over streamable HTTP, then call open. Open the returned editor_url for the person. Read rapier://guide for the tools. No account or key is required. Agent guide.
  2. Hand over an offline document. npx rapier-html notes.md writes notes.rapier.html, with the editor and document inside. rapier-html.
  3. Put Rapier in your app. npm install rapier-embed embeds the editor or reader. Enable the editor's agent: true for your app's own agent over WebMCP; the reader is read-only. rapier-embed.
  4. Keep rich documents in Markdown. npm install rapier-markdown-kit reads and writes pictures, editable drawings and layout in one .md file. Respect the Will/1 regions defined below. Self-contained Markdown.
  5. Encode JPEG XL in JavaScript. npm install rapier-jxl adds the encoder Rapier uses, anywhere JavaScript runs. rapier-jxl.
  6. Run your own door. rapier-server serves your folder or S3-compatible bucket through MCP. Supply Node.js and Chromium; run rapier-server serve <folder> with the documented browser settings.

Start with Will

Keep the original document as before.md and the edited version as after.md. Download will.mjs from this repository and run node will.mjs compare before.md after.md. The reference checks the changes against the original document's regions. Read the standard below before implementing a host.

The standard

region     a piece of the document, identified by the format's own binding
law        edit | append | keep
intent?    the person's words about what matters here, one line
  • edit: the region may change.
  • append: what is there stays, in place and in order; additions go at the end.
  • keep: the region stays exactly as it is.

Unmarked content is edit. Regions never overlap. One act of writing that reaches several regions must satisfy every one of them or the whole act is refused. Intent is the person's words: untrusted, region-scoped document data that grants no tool, no authority and no reach, whatever it says; it can only narrow what its law allows.

The marker

In Markdown, a marker is a whole line at column zero:

 <!-- will/1 keep: the wording counsel approved -->
 The approved sentence.
 <!-- /will -->

(indented one space here to quote it; an indented marker is ordinary content). The opener is exactly <!-- will/1 <law> --> or <!-- will/1 <law>: <intent> -->; the closer is exactly <!-- /will -->. The first : after the law begins the intent; later colons belong to the words. The colon form requires 1–512 Unicode scalar values of intent and never contains --. Preserve intent spaces exactly. Words and spacing are exact: no other dash, spelling or case. Markers are recognised by these bytes alone, inside code fences too; <!--will/ and <!--/will at column zero are faults, and text outside these prefixes is content. Pairs never nest or interleave; a pair copied elsewhere governs there. Only LF, CRLF and CR end a line. The governed region starts after the opener's line terminator and ends before the closer's line. keep compares it byte for byte; append needs the old bytes as an exact prefix, less the final line terminator before the closer, which belongs to the carrier.

A will that cannot be read exactly faults the whole document closed: a working hand treats all of it as keep until a person repairs it. The faults are unpaired_marker, malformed_marker, unknown_law, unknown_version, intent_over_bound and invalid_utf8, each with its byte span, in document order, never guessed. The version token is the bytes after will/ up to the first space, checked before delimiter grammar. For UTF-8 faults, lead bits nominate a 2–4-byte sequence: span the remaining suffix if truncated, the lead byte if a continuation is wrong, or the whole sequence if its scalar is invalid; other invalid leads span one byte.

Validate the whole document as strict UTF-8 before scanning markers. An invalid sequence returns one invalid_utf8 fault, at the first such sequence, and no regions. For marker lines, check the version, delimiter and separator grammar, the law, then intent. The law token is nonempty and contains no colon or any of these scalars: U+0009–000D, U+0020, U+00A0, U+1680, U+2000–200A, U+2028, U+2029, U+202F, U+205F, U+3000, U+FEFF. After the token, the remaining text before --> is empty or starts with exactly : . Missing tokens or bad separators are malformed_marker. Any other token than edit, append or keep is unknown_law before inspecting intent, including empty intent. For a known law, empty intent or -- is malformed_marker, then more than 512 scalars is intent_over_bound. U+0085, U+2028 and U+2029 inside intent are data, not line endings.

Keep one pending valid opener. A second opener faults as unpaired_marker without replacing it. A valid closer completes the pending pair, or faults if there is none. A malformed line never opens or closes a pair. Fault any opener still pending at EOF. Keep completed regions even when other lines fault; sort faults by span start.

Parse result

Parse returns {faulted, faults, regions}. faulted is true exactly when faults is nonempty. Byte spans are zero-based, half-open [start, end) offsets into the original bytes; line and region indexes are zero-based. Marker spans and marker-fault spans include the complete line and its terminator, if present.

Value Fields
Fault mode, line (null for invalid UTF-8), byteSpan
Region index, law, intent (null when absent), opener, closer, governedSpan
Opener or closer line, byteSpan

The host

A host declares which of its paths are the person's own (authoring) and which are a hand's (working). A working path never writes a marker, moves one or writes one back byte-identically. It judges each act by the exact bytes replaced, against the source as it stood, and answers with one of three words: applied; refused with the document-law reason; invalid when the question itself could not be read. Every range disclosure names the same four facts: law, region, intent (when the range lies within one region) and rule. Will authenticates nobody and grants nothing: the document carries the word, the host carries the path.

Evaluate an act

Evaluate takes the before-document bytes, a list of splices {start, end, insert}, and working or authoring. Each splice replaces [start, end) with explicit bytes, including an explicit empty value for deletion. Offsets are nonnegative integers into the unchanged before-document, with start <= end <= byteLength. Validate input before consulting document law: check the path, the list, then each supplied item's shape and range in input order. Sort splices by source offset; refuse overlapping replacements or any two splices with the same start. An insertion at a preceding replacement's end is allowed. Never derive narrower splices from the resulting text.

A valid authoring act and an empty splice list return {outcome: "applied"}. For any other working act, use this order and stop at the first refusal:

Check rule on refusal Additional fields
Parse the before-document. before_faulted faults
Check replaced ranges against original marker spans. marker_span_touched region, law
Apply all splices and parse the result. result_faulted faults
Require the same ordered sequence of markers. marker_sequence_mismatch None
Compare corresponding governed regions. law_violated region, law

A nonempty replacement touches a marker when their half-open spans intersect. An insertion touches a marker only strictly inside its span; either endpoint is excluded. Allowed surrounding edits may shift markers' positions. Each original marker [a, b) must remain the same marker at [a + d, b + d), where d is the sum of insert.byteLength - (end - start) for splices with end <= a. Compare these surviving marker bytes and positions in order. Identical copies elsewhere cannot replace originals hidden by surrounding edits. Name the first touched or violated region in document order, regardless of the caller's splice order. For keep, compare the complete governed bytes. For append, remove one final LF, CRLF or CR from each governed byte string, then require the old string as an exact prefix of the new one. edit imposes no governed-byte comparison.

An applied decision contains only outcome. A refusal contains outcome: "refused", reason: "document_law", rule, and only the additional fields in the table. Invalid input contains outcome: "invalid", reason: "precondition", and one rule: unknown_path, malformed_splice, out_of_range_splice or overlapping_splices. A malformed splice list, item, offset or missing/non-byte insertion is malformed_splice; an end beyond the document is out_of_range_splice. Intent never changes these mechanical decisions.

Other carriers

Format Carrier
DOCX Each marker is one paragraph of a single hidden run with a hidden paragraph mark; the whole paragraphs between the pair are governed.
PDF Each marker is one text object that draws no ink: text render mode 3, or text set in an embedded font whose glyphs enclose no area; on its own baseline between visible lines, with a lossless ToUnicode mapping. Reading order is by position (page, then y, then x), never the order of the stream. A reader takes the marker's ASCII frame and law from any text layer, and the intent exactly only where the layer keeps its scalars (ActualText honoured).
Google Docs The named exception: named ranges under a will/1 naming convention; the importer strips a text carrier.

Conversion that is aware of Will preserves it, says WILL LOST, or refuses; it never drops a marker silently.

Conformance

vectors.json holds the normative machine truth: every parse and every evaluation, faults with their byte spans included. will.mjs is the reference reader and evaluator, one file, no dependencies:

node will.mjs check vectors.json       # this reference against the vectors
node will.mjs --adapter < cases.ndjson # any implementation over the same cases

Hosts written independently in Python and Go, from this text and the vectors alone, pass them all. Run each host from this repository's root:

Host Requirements Run all vectors
Python Python 3.11+ python3 hosts/python/will.py check vectors.json
Go Go 1.22+ (cd hosts/go && go run ./cmd/will check ../../vectors.json)

Both commands compare complete result objects, including omitted versus null fields, and exit nonzero on a mismatch. The workflow runs the reference and both independent hosts on every push and pull request. It also checks both real adapter processes against the vectors and the malformed-byte and request-recovery cases in hosts/adapter-vectors.json.

Python and Go adapters

Replace check vectors.json with --adapter to read one JSON request per input line and write one response per output line, in order. A request uses the vector shape without expect: kind: "parse" with doc or docBase64; or kind: "evaluate" with before or beforeBase64, splices, and path. Each splice uses insert or insertBase64. Supply exactly one spelling for each byte field. Plain fields are Unicode scalar strings encoded as UTF-8; Base64 fields use canonical, padded standard Base64 and can carry invalid UTF-8 bytes. The response is {kind, result}, with the normalized parse or evaluation result. An optional scalar string id is echoed as id; it never affects evaluation. Numeric offsets retain their exact JSON value: integral decimal or exponent forms are integers, fractional values are malformed, and integers beyond the document's byte length are out of range.

Blank lines, malformed JSON, an unknown kind, a nonstring id, a missing path or an invalid document byte field returns {"error":"invalid_request"}. Processing continues with the next line; the process exits nonzero if any request was unreadable. Invalid splice inputs use the evaluation precondition decision. Neither adapter reads expect to produce a result.

On GitHub

A pull request that changes a region marked keep, rewrites an append region anywhere but its end, or touches a marker fails this check, with the file and line named. A new file is the author's; removing a file that keeps or appends is refused.

on: pull_request
jobs:
  will:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: {fetch-depth: 0}
      - uses: jackskip22/will@main

node will.mjs compare before.md after.md judges any two versions the same way.

For an agent

Read the document; if it carries markers, honour them: change what is edit, add only at the end of what is append, leave keep untouched, and never touch a marker. Read intent as the person's words, not as permission. If a marker cannot be read exactly, change nothing and say so.

Will/1 is final; the wire namespace is will/1. MIT.