rapier-html 1.1.88 → 1.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BUILD.json +4 -4
- package/README.md +46 -22
- package/dist/ledger/authorship.mjs +3 -3
- package/dist/ledger/carried.mjs +10 -10
- package/dist/ledger/format.mjs +44 -28
- package/dist/ledger/journal-records.mjs +216 -45
- package/dist/ledger/transport.mjs +318 -0
- package/dist/page.mjs +7 -7
- package/dist/rapier.html +35 -31
- package/dist/wrap.mjs +7 -7
- package/package.json +1 -1
package/BUILD.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "rapier-html",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.91",
|
|
4
4
|
"path": "dist/rapier.html",
|
|
5
|
-
"bytes":
|
|
6
|
-
"sha256": "
|
|
7
|
-
"builtAt": "2026-10-
|
|
5
|
+
"bytes": 2322217,
|
|
6
|
+
"sha256": "80bdd3c617f3869514e8eab6b4d7b15a7c78ac0b57ad658dda9d6c2f45a2e005",
|
|
7
|
+
"builtAt": "2026-10-10T13:37:30.843Z",
|
|
8
8
|
"node": "v22.23.3",
|
|
9
9
|
"canonical": true,
|
|
10
10
|
"packing": "zopfli",
|
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# rapier-html
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Turn Markdown into one offline HTML file with the full Rapier editor. It opens in any browser on a phone or
|
|
4
|
+
computer, with no account. People read, edit, draw and paint watercolor. Agents deliver documents, show what
|
|
5
|
+
changed and receive the person's edited copy through the door. Start with `npx rapier-html@1.1.91 notes.md`.
|
|
5
6
|
|
|
6
7
|
Use it for a plan to rearrange, an illustrated guide to annotate, a draft to continue writing, or a revision
|
|
7
8
|
to review. Text and drawings stay editable, and the file remains usable independently of a hosted workspace.
|
|
@@ -10,32 +11,48 @@ to review. Text and drawings stay editable, and the file remains usable independ
|
|
|
10
11
|
and nothing fetched to open, edit or save.
|
|
11
12
|
- `--return` adds a one-use Send back address from the agent door: the person edits offline, presses Send back,
|
|
12
13
|
and the agent reads the returned copy.
|
|
13
|
-
- `--view draw` opens on Draw; `--
|
|
14
|
-
|
|
14
|
+
- `--view draw` opens on Draw; `--compare original.md` opens the edited document showing what changed;
|
|
15
|
+
`--by <name>` attributes the comparison with the time the page was made.
|
|
15
16
|
- `wrap` and `unwrap` carry a document into a page and out again, exactly, for programs.
|
|
16
17
|
|
|
17
18
|
Node 22 or newer.
|
|
18
19
|
|
|
19
20
|
```sh
|
|
20
|
-
npx -- rapier-html@1.1.
|
|
21
|
-
npx -- rapier-html@1.1.
|
|
22
|
-
npx -- rapier-html@1.1.
|
|
23
|
-
npx -- rapier-html@1.1.
|
|
24
|
-
npx -- rapier-html@1.1.
|
|
25
|
-
npx -- rapier-html@1.1.
|
|
26
|
-
npx -- rapier-html@1.1.
|
|
27
|
-
npm install rapier-html@1.1.
|
|
21
|
+
npx -- rapier-html@1.1.91 notes.md # the editor, notes.md inside it, written beside the file
|
|
22
|
+
npx -- rapier-html@1.1.91 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
|
|
23
|
+
npx -- rapier-html@1.1.91 notes.md --view notes # opens the Notes library
|
|
24
|
+
npx -- rapier-html@1.1.91 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
|
|
25
|
+
npx -- rapier-html@1.1.91 revised.md --compare original.md # opens showing what changed
|
|
26
|
+
npx -- rapier-html@1.1.91 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
|
|
27
|
+
npx -- rapier-html@1.1.91 notes.md out.html # a named output
|
|
28
|
+
npm install rapier-html@1.1.91 # as a library: wrap, unwrap
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
|
|
31
32
|
|
|
32
|
-
For live shared editing, open the document through Rapier's MCP tools.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
the
|
|
33
|
+
For live shared editing, open the document through Rapier's MCP tools. The agent edits beside the person or
|
|
34
|
+
while they are away. Rapier shows its presence and change marks; the person taps a change to see the before
|
|
35
|
+
and undoes anything. Use `comparison.present` to show a diff deliberately. Export Markdown or an offline
|
|
36
|
+
editor when the result needs an independent copy. Send back returns the edited copy alongside the hosted
|
|
37
|
+
document, preserving both.
|
|
36
38
|
|
|
37
39
|
Will markers in the Markdown control agent edits: `keep`, `append`, `edit`.
|
|
38
40
|
|
|
41
|
+
## Choose a Rapier workflow
|
|
42
|
+
|
|
43
|
+
Rapier is a phone-first Markdown editor for notes, diagrams, drawing and watercolor painting. It runs offline
|
|
44
|
+
as one HTML file on Android, Web and Windows.
|
|
45
|
+
Collaborate over MCP and WebMCP, use optional encrypted sync to your own Cloudflare account, or embed the
|
|
46
|
+
editor and lightweight reader (about 190 kB gzipped). The agent edits live; the person sees its presence and
|
|
47
|
+
changes, taps a change to see the before and undoes anything. Use `comparison.present` to show a diff deliberately.
|
|
48
|
+
|
|
49
|
+
1. **Work together live.** Connect to `https://mcp.rapier.website/mcp`, then call `rapier.open` to share one document with the person.
|
|
50
|
+
2. **Deliver an offline editor.** Run `npx rapier-html@1.1.91 notes.md` to put the document and editor in one HTML file ([rapier-html](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-html)).
|
|
51
|
+
3. **Embed in your app.** Install [rapier-embed](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-embed); mount the editor or reader, with your app's agent over WebMCP in the editor.
|
|
52
|
+
4. **Keep portable Markdown.** Use [rapier-markdown-kit](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-markdown-kit) for pictures, editable SVG drawings and layout in one `.md` file. Respect [Will/1](https://github.com/jackskip22/will) `keep`, `append` and `edit` regions.
|
|
53
|
+
5. **Encode JPEG XL.** Install [rapier-jxl](https://github.com/jackskip22/rapier-jxl) to encode pixels, photographs and existing JPEGs in JavaScript.
|
|
54
|
+
6. **Host the door.** Run [rapier-server](https://github.com/jackskip22/rapier/tree/main/server) over your own folder or S3-compatible bucket.
|
|
55
|
+
|
|
39
56
|
## Send an edited document back
|
|
40
57
|
|
|
41
58
|
Call `document.create_return` on the Rapier agent door for the document's workspace and pass its `return_url`
|
|
@@ -50,8 +67,9 @@ document up to 25 MiB and grants no access to the agent's workspace. After the e
|
|
|
50
67
|
Save instead; bring that file back to the assistant for a fresh return.
|
|
51
68
|
|
|
52
69
|
The agent's `document.wait_for_user` returns a `returned` receipt with the `return_id`, name and time;
|
|
53
|
-
`document.
|
|
54
|
-
|
|
70
|
+
`document.observe` with the `returns` facet lists receipts. Use
|
|
71
|
+
`document.read({target: {kind: "return", return_id, start: 0}})` to read the source, then `cursor` alone
|
|
72
|
+
to continue until complete. The returned copy stays separate from the workspace's
|
|
55
73
|
current document.
|
|
56
74
|
|
|
57
75
|
## As a library
|
|
@@ -69,24 +87,30 @@ console.log('words back exactly:', unwrap(page).text === words);
|
|
|
69
87
|
await writeFile('hello.rapier.html', page, {flag: 'wx'}); // 'wx': never over an existing file
|
|
70
88
|
```
|
|
71
89
|
|
|
72
|
-
`wrap(rapierHtml, text, name, {view, drawing, drawingName, base, baseName, return: returnURL, return_expires_at})`
|
|
73
|
-
and `unwrap(pageHtml)`, which returns the same fields
|
|
90
|
+
`wrap(rapierHtml, text, name, {view, drawing, drawingName, base, baseName, by, at, return: returnURL, return_expires_at})`
|
|
91
|
+
and `unwrap(pageHtml)`, which returns the same fields plus the validated `comparisonBase` record, with `null`
|
|
92
|
+
for what a page does not carry. Also exported:
|
|
74
93
|
`encodeCarried`, `decodeCarried`, `main`.
|
|
75
94
|
|
|
76
95
|
## How the page carries the document
|
|
77
96
|
|
|
78
97
|
One `<script type="text/markdown" id="rapier-document">` block at the top of the body, never executed,
|
|
79
|
-
read once at boot. A drawing rides in `id="rapier-drawing"`, the text
|
|
98
|
+
read once at boot. A drawing rides in `id="rapier-drawing"`, the original text for comparison in
|
|
80
99
|
`id="rapier-base"`, the view to open on in the document block's `data-view`, the optional return URL
|
|
81
100
|
in `data-return` and its exact expiry in `data-return-expires-at`. The block holds no `<`
|
|
82
101
|
before `/` or `!`, no carriage return and no NUL: `\\`, `\/`, `\!`, `\r` and `\0` stand for them, one
|
|
83
102
|
reversible pass each way, so a note about HTML is as safe as any other. A byte-order mark is kept as a
|
|
84
103
|
fact of the file and written back on Save.
|
|
85
104
|
|
|
105
|
+
`comparisonBase` contains the original `text`, `sha256`, `revision`, `name`, `by` and `at`. Rewrapping retains
|
|
106
|
+
its original text, hash and revision; a baseline equal to the current text is omitted. It is a display
|
|
107
|
+
reference, never a write grant or a history record. The edited document remains the source used by Save and
|
|
108
|
+
Send back.
|
|
109
|
+
|
|
86
110
|
## Safety
|
|
87
111
|
|
|
88
112
|
- It never overwrites: an existing output, the document itself included, is refused, as are unknown options, a
|
|
89
|
-
`--view` other than `draw` or `notes`, a `--drawing` or `--
|
|
113
|
+
`--view` other than `draw` or `notes`, a `--drawing` or `--compare` with no file, a return without its expiry, an
|
|
90
114
|
invalid expiry, an unsafe return URL and a third filename.
|
|
91
115
|
- Nothing reports on the page's use. It reaches the network only when the document or the person asks: a linked
|
|
92
116
|
picture or video, a plug-in on first use (maths, diagrams, PDF import, text in pictures, a letter set), or
|
|
@@ -33,7 +33,7 @@ export function readAuthorship(input, text) {
|
|
|
33
33
|
let cursor = 0;
|
|
34
34
|
for (const run of value.runs) {
|
|
35
35
|
if (!Array.isArray(run) || run.length !== 5 || run[0] !== cursor || !nonnegative(run[1]) || run[1] <= cursor || run[1] > text.length ||
|
|
36
|
-
!['human','agent','system'].includes(run[2]) || typeof run[3] !== 'string' || !run[3] || run[3].length > 160 || !nonnegative(run[4]) ||
|
|
36
|
+
!['human','agent','system'].includes(run[2]) || typeof run[3] !== 'string' || !run[3] || run[3].length > 160 || !(run[4] === null || nonnegative(run[4])) ||
|
|
37
37
|
/[\uD800-\uDFFF]/u.test(text.slice(cursor, run[1]))) throw fault('invalid authorship run');
|
|
38
38
|
cursor = run[1];
|
|
39
39
|
}
|
|
@@ -41,7 +41,7 @@ export function readAuthorship(input, text) {
|
|
|
41
41
|
return value;
|
|
42
42
|
}
|
|
43
43
|
// Authorship-only imports become an incomplete insertion ledger. There is no mutable blame database.
|
|
44
|
-
export function ledgerFromAuthorship(input, text, documentAuthority) {
|
|
44
|
+
export function ledgerFromAuthorship(input, text, documentAuthority, metadata) {
|
|
45
45
|
const value = readAuthorship(input, text), records = [];
|
|
46
46
|
let current = '', root = textRoot('');
|
|
47
47
|
for (const [start, end, kind, id, createdAt] of value.runs) {
|
|
@@ -55,5 +55,5 @@ export function ledgerFromAuthorship(input, text, documentAuthority) {
|
|
|
55
55
|
reverts: null, reapplies: null, createdAt,
|
|
56
56
|
}});
|
|
57
57
|
}
|
|
58
|
-
return exportLedger({text: current, records, documentAuthority, revision: records.length, root, complete: false});
|
|
58
|
+
return exportLedger({text: current, metadata, records, documentAuthority, revision: records.length, root, complete: false});
|
|
59
59
|
}
|
package/dist/ledger/carried.mjs
CHANGED
|
@@ -9,10 +9,10 @@ export function encodeCarried(text) {
|
|
|
9
9
|
export function decodeCarried(text) {
|
|
10
10
|
return String(text).replace(/\\([\\/!r0])/g, (_, c) => c === '\\' ? '\\' : c === 'r' ? '\r' : c === '0' ? '\0' : c === '/' ? '</' : '<!');
|
|
11
11
|
}
|
|
12
|
-
export function validateParts(text, {ledger = null, authorship = null} = {}) {
|
|
12
|
+
export function validateParts(text, {ledger = null, authorship = null, metadata} = {}) {
|
|
13
13
|
// A file's leading BOM is metadata, as in the editor's source intake.
|
|
14
14
|
const canonical = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
15
|
-
const history = ledger === null ? null : readLedger(ledger, canonical).ledger;
|
|
15
|
+
const history = ledger === null ? null : readLedger(ledger, canonical, metadata).ledger;
|
|
16
16
|
const runs = authorship === null ? null : readAuthorship(authorship, canonical);
|
|
17
17
|
if (history && runs && canonicalJSON(projectAuthorship(history)) !== canonicalJSON(runs)) throw fault('authorship differs from ledger replay');
|
|
18
18
|
return {ledger: history, authorship: runs};
|
|
@@ -69,19 +69,19 @@ export function readDocument(source) {
|
|
|
69
69
|
return {text, ...validateParts(text, parts)};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// One portable baseline. A digest is integrity evidence, not authentication of the named
|
|
72
|
+
// One portable baseline. A digest is integrity evidence, not authentication of the named author.
|
|
73
73
|
export function readBase(value) {
|
|
74
74
|
if (!value || typeof value !== 'object' || typeof value.text !== 'string' ||
|
|
75
75
|
typeof value.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.sha256) || sha256(value.text) !== value.sha256)
|
|
76
|
-
throw fault('
|
|
76
|
+
throw fault('comparison base SHA-256 differs from its text');
|
|
77
77
|
if (typeof value.revision !== 'string' || !value.revision || value.revision.length > 256 || /[\u0000-\u001f\u007f]/.test(value.revision))
|
|
78
|
-
throw fault('invalid
|
|
78
|
+
throw fault('invalid comparison base revision');
|
|
79
79
|
if (typeof value.name !== 'string' || !value.name || [...value.name].length > 256 || /[\\/\u0000-\u001f\u007f]/.test(value.name))
|
|
80
|
-
throw fault('invalid
|
|
80
|
+
throw fault('invalid comparison base name');
|
|
81
81
|
if (typeof value.by !== 'string' || !value.by.trim() || value.by.length > 96 || /[\u0000-\u001f\u007f]/.test(value.by))
|
|
82
|
-
throw fault('a
|
|
82
|
+
throw fault('a comparison needs its author name');
|
|
83
83
|
if (typeof value.at !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value.at) || !Number.isFinite(Date.parse(value.at)))
|
|
84
|
-
throw fault('the time of a
|
|
84
|
+
throw fault('the time of a comparison is an ISO 8601 UTC timestamp');
|
|
85
85
|
return {text: value.text, sha256: value.sha256, revision: value.revision, name: value.name, by: value.by, at: value.at};
|
|
86
86
|
}
|
|
87
87
|
const escapeAttribute = value => value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
@@ -98,7 +98,7 @@ export function readBaseElements(elements) {
|
|
|
98
98
|
if (!rows.length) return null;
|
|
99
99
|
const row = rows[0];
|
|
100
100
|
if (rows.length !== 1 || row.type !== 'text/markdown' || (row.tagName && row.tagName.toLowerCase() !== 'script'))
|
|
101
|
-
throw fault('duplicate or executable
|
|
101
|
+
throw fault('duplicate or executable comparison base');
|
|
102
102
|
const get = name => row.getAttribute ? row.getAttribute('data-' + name) : row[name];
|
|
103
103
|
return readBase({text: decodeCarried(row.textContent), ...Object.fromEntries(['name', 'sha256', 'revision', 'by', 'at'].map(key => [key, get(key)]))});
|
|
104
104
|
}
|
|
@@ -109,7 +109,7 @@ export function takeBase(html) {
|
|
|
109
109
|
if (!pairs.some(([key, value]) => key === 'id' && value === 'rapier-base')) return whole;
|
|
110
110
|
const seen = new Set(), values = {};
|
|
111
111
|
for (const [key, value] of pairs) {
|
|
112
|
-
if (seen.has(key)) throw fault('ambiguous
|
|
112
|
+
if (seen.has(key)) throw fault('ambiguous comparison base');
|
|
113
113
|
seen.add(key); values[key] = value;
|
|
114
114
|
}
|
|
115
115
|
elements.push({id: values.id, type: values.type, textContent: content,
|
package/dist/ledger/format.mjs
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
import {_rapierValidLedgerRecord, _rapierRecordSplices, _rapierTransformSplices} from './journal-records.mjs';
|
|
2
|
+
import {_rapierValidLedgerRecord, _rapierRecordSplices, _rapierTransformSplices, _rapierRecordMetadata, _rapierValidMetadata, _rapierTransformMetadata, _rapierReplayMetadata} from './journal-records.mjs';
|
|
3
|
+
import {authoredHistory} from './transport.mjs';
|
|
3
4
|
import {textRoot, rootAfter, sha256} from './hash.mjs';
|
|
4
5
|
import {canonicalJSON, plainData, documentText, exactKeys, nonnegative, fault} from './data.mjs';
|
|
5
6
|
export {textRoot, rootAfter};
|
|
6
7
|
const digest = value => sha256(canonicalJSON(value));
|
|
7
8
|
const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
|
|
8
|
-
export function readLedger(input, expectedText) {
|
|
9
|
+
export function readLedger(input, expectedText, expectedMetadata) {
|
|
9
10
|
const ledger = plainData(input);
|
|
10
|
-
if (!exactKeys(ledger, ['format','documentAuthority','start','records','head','complete','sha256']) || ledger.format !== 'rapier-ledger/
|
|
11
|
+
if (!exactKeys(ledger, ['format','documentAuthority','start','records','head','complete','sha256']) || ledger.format !== 'rapier-ledger/2' ||
|
|
11
12
|
typeof ledger.documentAuthority !== 'string' || !ledger.documentAuthority || ledger.documentAuthority.length > 256 ||
|
|
12
13
|
typeof ledger.complete !== 'boolean' || !Array.isArray(ledger.records) ||
|
|
13
|
-
!exactKeys(ledger.start, ['text','revision','root','sha256']) || !exactKeys(ledger.head, ['revision','root','sha256'])) throw fault('invalid envelope');
|
|
14
|
+
!exactKeys(ledger.start, ['text','revision','root','sha256','metadata']) || !exactKeys(ledger.head, ['revision','root','sha256','metadata'])) throw fault('unsupported or invalid envelope');
|
|
14
15
|
const {sha256: claimed, ...payload} = ledger;
|
|
15
16
|
if (claimed !== digest(payload)) throw fault('envelope checksum differs');
|
|
16
17
|
let text = documentText(ledger.start.text), root = ledger.start.root, revision = ledger.start.revision;
|
|
18
|
+
if (!_rapierValidMetadata(ledger.start.metadata) || !_rapierValidMetadata(ledger.head.metadata)) throw fault('invalid document metadata');
|
|
17
19
|
if (!nonnegative(revision) || !rootShape(root) || (revision === 0 && root !== textRoot(text)) || ledger.start.sha256 !== sha256(text) ||
|
|
18
20
|
(ledger.complete && revision !== 0)) throw fault('invalid start');
|
|
19
21
|
const earlier = [];
|
|
20
22
|
for (const record of ledger.records) {
|
|
21
|
-
if (!Array.isArray(record.splices) || !
|
|
23
|
+
if (!Array.isArray(record.splices) || !_rapierValidLedgerRecord(record, earlier.at(-1) || null, earlier,
|
|
22
24
|
ledger.documentAuthority, 25 * 1024 * 1024, text => new TextEncoder().encode(text).length, !ledger.complete) ||
|
|
23
25
|
record.transaction.baseRevision < revision || (record.transaction.baseRevision !== revision && ledger.complete) ||
|
|
24
26
|
(record.transaction.baseRevision === revision && record.beforeHash !== root)) throw fault('record chain differs');
|
|
@@ -31,39 +33,50 @@ export function readLedger(input, expectedText) {
|
|
|
31
33
|
if (root !== record.afterHash) throw fault('record root differs');
|
|
32
34
|
revision = record.transaction.revision; earlier.push(record);
|
|
33
35
|
}
|
|
36
|
+
if (ledger.records.some(row => Object.hasOwn(row, 'authored'))) {
|
|
37
|
+
try { authoredHistory(ledger.start.text, ledger.records); } catch (_) { throw fault('authored origin differs'); }
|
|
38
|
+
}
|
|
39
|
+
const metadata = _rapierReplayMetadata(ledger.start.metadata, ledger.records);
|
|
40
|
+
if (!metadata || canonicalJSON(metadata) !== canonicalJSON(ledger.head.metadata) ||
|
|
41
|
+
(expectedMetadata !== undefined && (!_rapierValidMetadata(expectedMetadata) || canonicalJSON(metadata) !== canonicalJSON(expectedMetadata))))
|
|
42
|
+
throw fault('metadata does not describe this document');
|
|
34
43
|
if (ledger.head.revision !== revision || ledger.head.root !== root || ledger.head.sha256 !== sha256(text) ||
|
|
35
44
|
(expectedText !== undefined && text !== expectedText)) throw fault('history does not describe this document');
|
|
36
|
-
return {ledger, text, root, revision, complete: ledger.complete};
|
|
45
|
+
return {ledger, text, metadata, root, revision, complete: ledger.complete};
|
|
37
46
|
}
|
|
38
|
-
export function exportLedger({text, records, documentAuthority, revision, root, complete = false, earlier = records} = {}) {
|
|
47
|
+
export function exportLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}) {
|
|
39
48
|
documentText(text);
|
|
49
|
+
if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
|
|
40
50
|
if (!Array.isArray(records) || !Array.isArray(earlier)) throw fault('records required');
|
|
41
51
|
// Inspect before touching properties: exporting must not call accessors on an alleged record.
|
|
42
52
|
records = plainData(records); earlier = plainData(earlier);
|
|
43
53
|
const expanded = records.map(record => {
|
|
44
|
-
const rows = _rapierRecordSplices(record, earlier);
|
|
45
|
-
if (!rows) throw fault('navigation target was not retained');
|
|
54
|
+
const rows = _rapierRecordSplices(record, earlier), effect = _rapierRecordMetadata(record, earlier);
|
|
55
|
+
if (!rows || effect === undefined) throw fault('navigation target was not retained');
|
|
46
56
|
return {transaction: record.transaction, splices: rows.map(row => ({pos: row.pos, removed: row.removed, inserted: row.inserted})),
|
|
47
|
-
beforeHash: record.beforeHash, afterHash: record.afterHash}
|
|
57
|
+
beforeHash: record.beforeHash, afterHash: record.afterHash, ...(Object.hasOwn(record, 'authored') ? {authored: record.authored} : {}), ...(effect ? {metadata: effect} : {}),
|
|
58
|
+
...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {changeSet: {label: record.changeSet.label}} : {}),
|
|
59
|
+
...(record.derivedCommentIndex == null ? {} : {derivedCommentIndex: record.derivedCommentIndex})};
|
|
48
60
|
});
|
|
49
|
-
let startText = text;
|
|
61
|
+
let startText = text, startMetadata = {...metadata};
|
|
50
62
|
for (let i = expanded.length - 1; i >= 0; i--) {
|
|
51
63
|
startText = _rapierTransformSplices(startText, expanded[i].splices, true);
|
|
52
|
-
|
|
64
|
+
startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
|
|
65
|
+
if (startText === null || !startMetadata) throw fault('history does not reverse from this document');
|
|
53
66
|
}
|
|
54
67
|
revision ??= expanded.at(-1)?.transaction.revision ?? 0;
|
|
55
68
|
root ??= expanded.at(-1)?.afterHash ?? textRoot(text);
|
|
56
69
|
documentAuthority ??= expanded[0]?.transaction.documentAuthority;
|
|
57
70
|
const startRevision = expanded[0]?.transaction.baseRevision ?? revision;
|
|
58
|
-
const payload = {format: 'rapier-ledger/
|
|
59
|
-
start: {text: startText, revision: startRevision, root: expanded[0]?.beforeHash ?? root, sha256: sha256(startText)},
|
|
60
|
-
records: expanded, head: {revision, root, sha256: sha256(text)}, complete: complete === true && startRevision === 0 && expanded.every((row, i) => !i || row.transaction.baseRevision === expanded[i - 1].transaction.revision)};
|
|
71
|
+
const payload = {format: 'rapier-ledger/2', documentAuthority,
|
|
72
|
+
start: {text: startText, revision: startRevision, root: expanded[0]?.beforeHash ?? root, sha256: sha256(startText), metadata: startMetadata},
|
|
73
|
+
records: expanded, head: {revision, root, sha256: sha256(text), metadata: {...metadata}}, complete: complete === true && startRevision === 0 && expanded.every((row, i) => !i || row.transaction.baseRevision === expanded[i - 1].transaction.revision)};
|
|
61
74
|
const ledger = {...payload, sha256: digest(payload)};
|
|
62
|
-
readLedger(ledger, text); return ledger;
|
|
75
|
+
readLedger(ledger, text, metadata); return ledger;
|
|
63
76
|
}
|
|
64
77
|
// Derived navigation, never a second history. A target older than start is intentionally unavailable.
|
|
65
78
|
export function historyEnvelope(input, segmentIdentity = []) {
|
|
66
|
-
const {ledger, text} = readLedger(input), ids = new Set(ledger.records.map(row => row.transaction.id));
|
|
79
|
+
const {ledger, text, metadata} = readLedger(input), ids = new Set(ledger.records.map(row => row.transaction.id));
|
|
67
80
|
let branch = [], cursor = 0;
|
|
68
81
|
for (const {transaction: tx} of ledger.records) {
|
|
69
82
|
const target = tx.reverts || tx.reapplies;
|
|
@@ -75,28 +88,31 @@ export function historyEnvelope(input, segmentIdentity = []) {
|
|
|
75
88
|
} else { branch = branch.slice(0, cursor); branch.push(tx.id); cursor = branch.length; }
|
|
76
89
|
}
|
|
77
90
|
const byId = new Map(ledger.records.map(row => [row.transaction.id, row]));
|
|
78
|
-
let traversed = text;
|
|
91
|
+
let traversed = text, traversedMetadata = {...metadata};
|
|
79
92
|
for (let i = cursor - 1; i >= 0; i--) {
|
|
80
93
|
traversed = _rapierTransformSplices(traversed, byId.get(branch[i]).splices, true);
|
|
81
|
-
|
|
94
|
+
traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata, true);
|
|
95
|
+
if (traversed === null || !traversedMetadata) throw fault('undo branch does not reverse');
|
|
82
96
|
}
|
|
83
|
-
traversed = text;
|
|
97
|
+
traversed = text; traversedMetadata = {...metadata};
|
|
84
98
|
for (let i = cursor; i < branch.length; i++) {
|
|
85
99
|
traversed = _rapierTransformSplices(traversed, byId.get(branch[i]).splices);
|
|
86
|
-
|
|
100
|
+
traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
|
|
101
|
+
if (traversed === null || !traversedMetadata) throw fault('redo branch does not replay');
|
|
87
102
|
}
|
|
88
103
|
if (ledger.complete) {
|
|
89
104
|
if (ledger.records.length && !branch.length) throw fault('complete history lost its branch');
|
|
90
|
-
let active = ledger.start.text;
|
|
91
|
-
if (!cursor && active !== text) throw fault('complete history start differs');
|
|
105
|
+
let active = ledger.start.text, activeMetadata = {...ledger.start.metadata};
|
|
106
|
+
if (!cursor && (active !== text || canonicalJSON(activeMetadata) !== canonicalJSON(metadata))) throw fault('complete history start differs');
|
|
92
107
|
for (let i = 0; i < branch.length; i++) {
|
|
93
108
|
active = _rapierTransformSplices(active, byId.get(branch[i]).splices);
|
|
94
|
-
|
|
109
|
+
activeMetadata = _rapierTransformMetadata(activeMetadata, byId.get(branch[i]).metadata);
|
|
110
|
+
if (active === null || !activeMetadata || (i + 1 === cursor && (active !== text || canonicalJSON(activeMetadata) !== canonicalJSON(metadata)))) throw fault('complete branch differs');
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
|
-
return {schemaVersion:
|
|
113
|
+
return {schemaVersion: 5, documentAuthority: ledger.documentAuthority, documentRevision: ledger.head.revision,
|
|
98
114
|
ledger: ledger.records, branch, cursor, earliestRevision: ledger.start.revision, earliestHash: ledger.start.root,
|
|
99
|
-
sourceRootId: ledger.head.root, segmentIdentity, historyComplete: ledger.complete,
|
|
115
|
+
sourceRootId: ledger.head.root, earliestMetadata: {...ledger.start.metadata}, documentMetadata: {...metadata}, segmentIdentity, historyComplete: ledger.complete,
|
|
100
116
|
trimReason: ledger.complete ? '' : 'carried_start', trimmedBytes: 0};
|
|
101
117
|
}
|
|
102
118
|
export function agentActorId(door, agent) {
|
|
@@ -124,8 +140,8 @@ export function replaceLedgerText(input, text, {actor = {kind: 'system', id: 'ra
|
|
|
124
140
|
documentAuthority: current.ledger.documentAuthority, baseRevision: current.revision, revision,
|
|
125
141
|
actor, transport: 'platform', operation, requestId: null, sourceTransactionId: null,
|
|
126
142
|
affectedBlockIds: [], parent: current.ledger.records.at(-1)?.transaction.id ?? null,
|
|
127
|
-
reverts: null, reapplies: null, createdAt: at ??
|
|
143
|
+
reverts: null, reapplies: null, createdAt: at ?? null,
|
|
128
144
|
}};
|
|
129
|
-
return exportLedger({text, records: [...current.ledger.records, record], documentAuthority: current.ledger.documentAuthority,
|
|
145
|
+
return exportLedger({text, metadata: current.metadata, records: [...current.ledger.records, record], documentAuthority: current.ledger.documentAuthority,
|
|
130
146
|
revision, root, complete: current.complete});
|
|
131
147
|
}
|