rapier-markdown-kit 1.1.90 → 1.1.92

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/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # rapier-markdown-kit
2
2
 
3
- Read, write and style Self-contained Markdown in your own code: one `.md` file that carries its pictures, layout
4
- and colour.
3
+ Read, render and write Self-contained Markdown in browsers and Node: one `.md` file with pictures, editable SVG
4
+ drawings, layout and colour. Build portable documents for people and agents. Will/1 regions mark what an agent
5
+ may edit, append to or preserve. MIT, no dependencies.
5
6
 
6
7
  - Everything past CommonMark and GFM is an HTML comment other readers ignore or an ordinary reference
7
8
  definition, so every Markdown app still reads the file.
@@ -15,6 +16,10 @@ To show documents in your app without writing a renderer, frame the read-only re
15
16
  `npm install rapier-embed` (`build: 'reader'`). To hand a person the whole editor around their document as one
16
17
  offline file, run `npx rapier-html notes.md`.
17
18
 
19
+ In the editor, an agent edits live beside the person or while they are away. Rapier shows its presence and changes;
20
+ the person taps a change to see what was there before and undoes anything. `comparison.present` lets the agent
21
+ deliberately show a diff. The kit supplies the portable source and history those tools use.
22
+
18
23
  ```sh
19
24
  npm install rapier-markdown-kit
20
25
  ```
@@ -27,6 +32,18 @@ import {parseWill} from 'rapier-markdown-kit/will'; // Will/1 markers: kee
27
32
  import {parseAssets} from 'rapier-markdown-kit/assets'; // the picture appendix
28
33
  ```
29
34
 
35
+ ## Choose a Rapier workflow
36
+
37
+ Rapier is a phone-first Markdown editor for notes, diagrams, drawing and watercolor painting. It runs offline
38
+ as one HTML file on Android, Web and Windows.
39
+
40
+ 1. **Work together live.** Connect to `https://mcp.rapier.website/mcp`, then call `rapier.open` to share one document with the person.
41
+ 2. **Deliver an offline editor.** Run `npx rapier-html 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)).
42
+ 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.
43
+ 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.
44
+ 5. **Encode JPEG XL.** Install [rapier-jxl](https://github.com/jackskip22/rapier-jxl) to encode pixels, photographs and existing JPEGs in JavaScript.
45
+ 6. **Host the door.** Run [rapier-server](https://github.com/jackskip22/rapier/tree/main/server) over your own folder or S3-compatible bucket.
46
+
30
47
  ## The shared renderer
31
48
 
32
49
  `rapier-markdown-kit/render` exports `createRenderer`, `createMarkdownRenderer`,
@@ -42,7 +59,7 @@ import {createRenderer} from 'rapier-markdown-kit/render';
42
59
  // host is your bound DOM/codec/Markdown/image/style environment. Its named ports are
43
60
  // declared at the top of createRenderer; preparation returns a detached semantic root.
44
61
  const writer = createRenderer(host);
45
- const {html, filename} = await writer.render(source, {filename: 'proposal.md'});
62
+ const {html, filename} = await writer.render(source, {filename: 'report.md'});
46
63
  ```
47
64
 
48
65
  `createMarkdownRenderer(host).render(source, metadata)` returns the semantic HTML.
@@ -55,9 +72,10 @@ DOM objects between document realms.
55
72
  This **DOM-hosted library** requires a DOM and sanitizer, the Rapier grammar,
56
73
  portable image preparation, style access and the named codec/layout ports it uses.
57
74
  Those dependencies keep their own licences: the MIT grant here does not relicense a
58
- separately supplied host or vendor. The repository's `editor/render-host.js` binds the
59
- editor; `server/` provides a complete local host with a pinned Chromium process. Its
60
- Word exporter and filesystem service are AGPL, not part of this MIT package.
75
+ separately supplied host or vendor. The [editor host binding](https://github.com/jackskip22/rapier/blob/main/src/editor/render-host.js)
76
+ lists the renderer's dependencies. [rapier-server](https://github.com/jackskip22/rapier/blob/main/server/README.md)
77
+ provides a complete local host with a pinned Chromium process. Its Word exporter and filesystem service are AGPL,
78
+ not part of this MIT package.
61
79
 
62
80
  Serialization tests preserve source bytes through HTML export and recovery. Server integration
63
81
  tests also check the exported HTTP response through a browser DOM.
@@ -272,8 +290,9 @@ tolerance.
272
290
 
273
291
  ## Licence
274
292
 
275
- MIT, the full text in `LICENSE`; every module and the stylesheet keep their own MIT line. Pretext's licence, notice and
276
- pinned source inventory are in `dist/agent/vendor/pretext/`. The Rapier editor is AGPL-3.0-only and
293
+ MIT, the full text in `LICENSE`; Rapier-owned modules and the stylesheet keep their MIT line. Pretext's licence, notice and
294
+ pinned source inventory are in `dist/agent/vendor/pretext/`. The history projection's jsdiff dependency is BSD-3-Clause;
295
+ `dist/agent/diff.mjs` carries its complete upstream copyright, conditions and disclaimer. The Rapier editor is AGPL-3.0-only and
277
296
  none of it is in this package. The kit carries Rapier's release number, written from the one
278
297
  value the editor's release reads.
279
298
 
@@ -294,7 +313,7 @@ import {exportLedger, readLedger, authorship, merge} from 'rapier-markdown-kit/r
294
313
  import {writeDocument, readDocument} from 'rapier-markdown-kit/ledger/carried';
295
314
 
296
315
  const ledger = exportLedger({
297
- text: 'Hello', records: [], documentAuthority: 'example-document', complete: true,
316
+ text: 'Hello', metadata: {filename: 'Hello.md', docKind: 'markdown'}, records: [], documentAuthority: 'example-document', complete: true,
298
317
  });
299
318
  const saved = writeDocument('Hello', {ledger});
300
319
  const reopened = readDocument(saved);
@@ -308,23 +327,40 @@ console.assert(together.text === 'Hello');
308
327
 
309
328
  ## The format
310
329
 
311
- `rapier-ledger/1` is a plain JSON object:
330
+ `rapier-ledger/2` is a plain JSON object:
312
331
 
313
332
  ```
314
- {format, documentAuthority, start: {text, revision, root, sha256}, records,
315
- head: {revision, root, sha256}, complete, sha256}
316
- records[]: {transaction, splices: [{pos, removed, inserted}], beforeHash, afterHash}
333
+ {format, documentAuthority, start: {text, revision, root, sha256, metadata}, records,
334
+ head: {revision, root, sha256, metadata}, complete, sha256}
335
+ records[]: {transaction, splices: [{pos, removed, inserted}], beforeHash, afterHash, metadata?, authored?}
317
336
  transaction: {id, documentAuthority, baseRevision, revision, actor: {kind, id},
318
337
  transport, operation, requestId, sourceTransactionId, affectedBlockIds,
319
338
  parent, reverts, reapplies, createdAt}
320
339
  ```
321
340
 
322
- `format` is `rapier-ledger/1`. `start.text` is the exact text before the first
341
+ `format` is `rapier-ledger/2`. `start.text` is the exact text before the first
323
342
  retained record. Every outgoing record carries its own splices, including Undo and
324
343
  Redo. Offsets and ranges count UTF-16 code units; no boundary may cut a surrogate
325
344
  pair. Splices execute in their recorded order. UTF-8 checksums preserve CRLF, tabs,
326
345
  NUL and non-ASCII text. The canonical text limit is 25 MiB; each record has at most
327
- 64 splices. The existing record validator remains authoritative for field bounds.
346
+ 64 splices. Metadata is {filename, docKind}; a record may carry filename or docKind pairs {before, after}. Metadata-only acts keep equal source roots and an empty splice list. Inverses reference retained act IDs, so later metadata choices survive selective Undo. File handles and save permissions are never portable. The existing record validator remains authoritative for field bounds.
347
+
348
+ An optional `authored: {basis, source, splices}` preserves an act's original placement
349
+ when concurrent transport changes its physical `record.splices`. `basis` is the sorted,
350
+ minimal frontier of canonical act IDs observed at authoring; its causal closure is
351
+ reconstructed from these same records, including metadata-only acts and inverse
352
+ dependencies. `source` is the SHA-256 of the exact UTF-8 source before the original
353
+ sequential `splices`. An ordinary untransported record omits this field; its retained
354
+ prefix supplies the same evidence. Transport preserves the evidence and the original
355
+ act ID, author, time, turn, label, metadata choices and inverse targets. It neither
356
+ copies the growing prefix into each record nor creates another journal.
357
+
358
+ Use `readLedger` or `exportLedger` to validate retained history and `merge` or
359
+ `transposeAuthored` to transport it. Validation proves the causal closure, source
360
+ hash and exact original-to-physical placement; field shape or equal final text alone
361
+ is insufficient. Do not attach new authored evidence to an arbitrary physical copy.
362
+ Selective Undo uses this evidence to preserve concurrent insertions at their original
363
+ gaps, even when a replacement split into several physical splices.
328
364
 
329
365
  `start.sha256` and `head.sha256` hash UTF-8 text. The envelope `sha256` hashes
330
366
  canonical JSON of every other envelope field: recursively sorted object keys,
@@ -340,16 +376,17 @@ actual document. `historyEnvelope` additionally proves the editor's Undo/Redo br
340
376
  a fresh hash of the whole current text. SHA-256 of the exact checkpoint text is
341
377
  separate. A nonzero start names a retained checkpoint; it does not
342
378
  claim to reconstruct discarded events. `complete: false` also permits a stated
343
- revision gap from the editor's pruning of cancelling navigation pairs; each
344
- remaining splice must still replay exactly. It is never a claim about missing edits.
345
-
346
- `actor.kind` is `human`, `agent` or `system`. A named agent's `actor.id` is
347
- `<door>/<host-given name>` (name at most 96 characters, combined id at most 160).
348
- Without a name, the door stands alone. The name is a **host-supplied claim**, not a
349
- verified identity. Timestamps are supplied record times, not independently attested.
379
+ revision gap in a partial ledger; each retained splice must still replay exactly. An incomplete ledger never
380
+ claims to reconstruct events it does not contain.
381
+
382
+ `actor.kind` is `human`, `agent` or `system`. Canonical doors mint an opaque provenance
383
+ `actor.id` independent of the private principal and retain a supplied display name
384
+ separately as `actor.name` (host names at most 96 characters). The private principal-to-author
385
+ mapping stays in the existing private owner; neither raw nor hashed access credentials
386
+ belong in portable authorship. The name is a **host-supplied claim**, not a verified identity. Timestamps are supplied record times, not independently attested.
350
387
  Checksums detect inconsistency/corruption, **not forgery**: anyone able to rewrite a
351
388
  file can rewrite its checksums and labels. Importing a label never grants a live
352
- principal's read handles, review authority or undo permissions.
389
+ principal's read handles or authenticated document access.
353
390
 
354
391
  ## Optional carried parts
355
392
 
@@ -359,7 +396,7 @@ beside the document's existing carried source:
359
396
  | Element | Type and contents | Choice |
360
397
  | --- | --- | --- |
361
398
  | `#rapier-authorship` | `application/json`, `rapier-authorship/1` | Who wrote what |
362
- | `#rapier-ledger` | `application/json`, `rapier-ledger/1` | The whole history |
399
+ | `#rapier-ledger` | `application/json`, `rapier-ledger/2` | The whole history |
363
400
 
364
401
  Each body uses the same reversible `encodeCarried`/`decodeCarried` escape as the
365
402
  page's source. `writeParts`, `readParts` and `takeParts` implement it. Duplicate,
@@ -392,10 +429,10 @@ leading BOM as file metadata. Other Markdown readers ignore the comment. Plain
392
429
  saves without a choice are unchanged byte for byte. For executable code, use a web
393
430
  page to carry metadata rather than appending a Markdown comment to program source.
394
431
 
395
- The existing proposal page's `#rapier-base` is independent: `text/markdown`, with
396
- `data-revision`, `data-sha256`, `data-by` and `data-at`, carries the proposal's
397
- baseline. It is **not** reconstructed from an optional ledger, and this module does
398
- not implement the separate proposal-tool/CLI workflow.
432
+ The comparison page's `#rapier-base` is independent: `text/markdown`, with
433
+ `data-name`, `data-revision`, `data-sha256`, `data-by` and `data-at`, carries an exact
434
+ display reference. It is not reconstructed from the optional ledger and never
435
+ replaces current source. `rapier-html --compare original.md revised.md` packages both texts.
399
436
 
400
437
  ## Merging copies without a server
401
438
 
@@ -414,22 +451,19 @@ unchanged. `review.required` names an unresolved result; a UI must not auto-acce
414
451
  it. Rapier offers carried-copy imports from Compare through its existing review,
415
452
  checks the live document again at commit, and installs the proven ledger before
416
453
  publishing its receipt or opening the durable-save barrier. A rejected or stale
417
- proposal mutates nothing. A failed install rolls the source and undo window back.
454
+ copy import mutates nothing. A failed install rolls the source and undo window back.
418
455
  Copies without a proven common checkpoint are refused by this API.
419
456
 
420
457
  ## Host integration and retention
421
458
 
422
459
  The embed helper accepts `agentName`; the checked `rapier-connect` message carries
423
460
  `agent: {name}`. WebMCP uses the registration owner's supplied client name. Modern
424
- MCP uses the request's client metadata, outside tool arguments; a legacy stateless
425
- request without a supplied name stays unnamed. MCP editor snapshots include the
461
+ MCP uses the request's client metadata, outside tool arguments; a request without a supplied name stays unnamed. MCP editor snapshots include the
426
462
  server's retained journal only in editor metadata, not in model-facing tool results.
427
463
  The browser proves a replay suffix before preserving remote writers under local
428
464
  revision links; missing remote events never become an invented complete history.
429
465
 
430
- Rapier retains its existing 500-record / 4-MiB in-memory window. A carried file can
431
- outlive that window; reopening may trim it again. The module preserves an explicitly
432
- incomplete start. Full history includes removed text and can be much larger than
433
- the current document; it is opt-in for each outgoing file, never a persistent
434
- preference. Authorship is also opt-in. Neither option implies signatures, identity
435
- verification, infinite retention, a relay server or automatic synchronization.
466
+ Rapier's live history supports before previews and Undo for individual acts or turns, preserving later edits.
467
+ A carried ledger preserves the history included in that file and explicitly identifies an incomplete start.
468
+ Full history includes removed text and can be much larger than the current document; choose it separately for
469
+ each outgoing file. Authorship is also optional. Neither part verifies identities or synchronizes copies.
@@ -0,0 +1,52 @@
1
+ /*! jsdiff 9.0.0 — derivative bundle. Upstream licence, reproduced complete:
2
+ https://github.com/kpdecker/jsdiff/blob/v9.0.0/LICENSE
3
+
4
+ BSD 3-Clause License
5
+
6
+ Copyright (c) 2009-2015, Kevin Decker <kpdecker@gmail.com>
7
+ All rights reserved.
8
+
9
+ Redistribution and use in source and binary forms, with or without
10
+ modification, are permitted provided that the following conditions are met:
11
+
12
+ 1. Redistributions of source code must retain the above copyright notice, this
13
+ list of conditions and the following disclaimer.
14
+
15
+ 2. Redistributions in binary form must reproduce the above copyright notice,
16
+ this list of conditions and the following disclaimer in the documentation
17
+ and/or other materials provided with the distribution.
18
+
19
+ 3. Neither the name of the copyright holder nor the names of its
20
+ contributors may be used to endorse or promote products derived from
21
+ this software without specific prior written permission.
22
+
23
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
+ */
34
+ /* Rapier note on this derivative bundle: the wall clock and the scheduler are injected.
35
+ `diffWithOptionsObj` reads `options.now()` instead of `Date.now()` and `options.schedule(fn,
36
+ ms)` instead of `setTimeout(fn, ms)`. Callers that want a real-time `timeout` or the async
37
+ callback path pass `{ now: Date.now, schedule: setTimeout }` (the Compare Worker in
38
+ editor/engine.js); callers that want a deterministic result bound only by `maxEditLength` pass
39
+ neither (the kernel), and the diff then depends on nothing but its two input strings. This keeps
40
+ one diff factory for the whole app and keeps agent/kernel.mjs inside the decision-purity gate
41
+ (tools/purity-gate.mjs) without any per-file exemption. */
42
+ export function createDiff(){var O=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var Q=Object.prototype.hasOwnProperty;var X=(i,e)=>{for(var n in e)O(i,n,{get:e[n],enumerable:!0})},Z=(i,e,n,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let l of B(e))!Q.call(i,l)&&l!==n&&O(i,l,{get:()=>e[l],enumerable:!(t=Y(e,l))||t.enumerable});return i};var K=i=>Z(O({},"__esModule",{value:!0}),i);var oe={};X(oe,{diffChars:()=>diffChars,diffLines:()=>y,diffWordsWithSpace:()=>z,structuredPatch:()=>U});var C=class{diff(e,n,t={}){let l;typeof t=="function"?(l=t,t={}):"callback"in t&&(l=t.callback);let o=this.castInput(e,t),r=this.castInput(n,t),u=this.removeEmpty(this.tokenize(o,t)),a=this.removeEmpty(this.tokenize(r,t));return this.diffWithOptionsObj(u,a,t,l)}diffWithOptionsObj(e,n,t,l){var o;let r=s=>{if(s=this.postProcess(s,t),l){t.schedule(function(){l(s)},0);return}else return s},u=n.length,a=e.length,f=1,c=u+a;t.maxEditLength!=null&&(c=Math.min(c,t.maxEditLength));let w=(o=t.timeout)!==null&&o!==void 0?o:1/0,p=(t.now?t.now():0)+w,m=[{oldPos:-1,lastComponent:void 0}],x=this.extractCommon(m[0],n,e,0,t);if(m[0].oldPos+1>=a&&x+1>=u)return r(this.buildValues(m[0].lastComponent,n,e));let v=-1/0,P=1/0,E=()=>{for(let s=Math.max(v,-f);s<=Math.min(P,f);s+=2){let d,h=m[s-1],g=m[s+1];h&&(m[s-1]=void 0);let F=!1;if(g){let q=g.oldPos-s;F=g&&0<=q&&q<u}let D=h&&h.oldPos+1<a;if(!F&&!D){m[s]=void 0;continue}if(!D||F&&h.oldPos<g.oldPos?d=this.addToPath(g,!0,!1,0,t):d=this.addToPath(h,!1,!0,1,t),x=this.extractCommon(d,n,e,s,t),d.oldPos+1>=a&&x+1>=u)return r(this.buildValues(d.lastComponent,n,e))||!0;m[s]=d,d.oldPos+1>=a&&(P=Math.min(P,s-1)),x+1>=u&&(v=Math.max(v,s+1))}f++};if(l)(function s(){t.schedule(function(){if(f>c||(t.now?t.now():0)>p)return l(void 0);E()||s()},0)})();else for(;f<=c&&(t.now?t.now():0)<=p;){let s=E();if(s)return s}}addToPath(e,n,t,l,o){let r=e.lastComponent;return r&&!o.oneChangePerToken&&r.added===n&&r.removed===t?{oldPos:e.oldPos+l,lastComponent:{count:r.count+1,added:n,removed:t,previousComponent:r.previousComponent}}:{oldPos:e.oldPos+l,lastComponent:{count:1,added:n,removed:t,previousComponent:r}}}extractCommon(e,n,t,l,o){let r=n.length,u=t.length,a=e.oldPos,f=a-l,c=0;for(;f+1<r&&a+1<u&&this.equals(t[a+1],n[f+1],o);)f++,a++,c++,o.oneChangePerToken&&(e.lastComponent={count:1,previousComponent:e.lastComponent,added:!1,removed:!1});return c&&!o.oneChangePerToken&&(e.lastComponent={count:c,previousComponent:e.lastComponent,added:!1,removed:!1}),e.oldPos=a,f}equals(e,n,t){return t.comparator?t.comparator(e,n):e===n||!!t.ignoreCase&&e.toLowerCase()===n.toLowerCase()}removeEmpty(e){let n=[];for(let t=0;t<e.length;t++)e[t]&&n.push(e[t]);return n}castInput(e,n){return e}tokenize(e,n){return Array.from(e)}join(e){return e.join("")}postProcess(e,n){return e}buildValues(e,n,t){let l=[],o;for(;e;)l.push(e),o=e.previousComponent,delete e.previousComponent,e=o;l.reverse();let r=l.length,u=0,a=0,f=0;for(;u<r;u++){let c=l[u];if(c.removed)c.value=this.join(t.slice(f,f+c.count)),f+=c.count;else{c.value=this.join(n.slice(a,a+c.count));a+=c.count,c.added||(f+=c.count)}}return l}};var I="a-zA-Z0-9_\\u{AD}\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}";var R=class extends C{tokenize(e){let n=new RegExp(`(\\r?\\n)|[${I}]+|[^\\S\\n\\r]+|[^${I}]`,"ug");return e.match(n)||[]}},J=new R;function z(i,e,n){return J.diff(i,e,n)}var $=class extends C{constructor(){super(...arguments),this.tokenize=ie}equals(e,n,t){return t.ignoreWhitespace?((!t.newlineIsToken||!e.includes(`
43
+ `))&&(e=e.trim()),(!t.newlineIsToken||!n.includes(`
44
+ `))&&(n=n.trim())):t.ignoreNewlineAtEof&&!t.newlineIsToken&&(e.endsWith(`
45
+ `)&&(e=e.slice(0,-1)),n.endsWith(`
46
+ `)&&(n=n.slice(0,-1))),super.equals(e,n,t)}},V=new $;function y(i,e,n){return V.diff(i,e,n)}function ie(i,e){e.stripTrailingCr&&(i=i.replace(/\r\n/g,`
47
+ `));let n=[],t=i.split(/(\n|\r\n)/);t[t.length-1]||t.pop();for(let l=0;l<t.length;l++){let o=t[l];l%2&&!e.newlineIsToken?n[n.length-1]+=o:n.push(o)}return n}function U(i,e,n,t,l,o,r){let u;r?typeof r=="function"?u={callback:r}:u=r:u={},typeof u.context>"u"&&(u.context=4);let a=u.context;if(u.newlineIsToken)throw new Error("newlineIsToken may not be used with patch-generation functions, only with diffing functions");if(u.callback){let{callback:c}=u;y(n,t,Object.assign(Object.assign({},u),{callback:w=>{let p=f(w);c(p)}}))}else return f(y(n,t,u));function f(c){if(!c)return;c.push({value:"",lines:[]});function w(s){return s.map(function(d){return" "+d})}let p=[],m=0,x=0,v=[],P=1,E=1;for(let s=0;s<c.length;s++){let d=c[s],h=d.lines||le(d.value);if(d.lines=h,d.added||d.removed){if(!m){let g=c[s-1];m=P,x=E,g&&(v=a>0?w(g.lines.slice(-a)):[],m-=v.length,x-=v.length)}for(let g of h)v.push((d.added?"+":"-")+g);d.added?E+=h.length:P+=h.length}else{if(m)if(h.length<=a*2&&s<c.length-2)for(let g of w(h))v.push(g);else{let g=Math.min(h.length,a);for(let D of w(h.slice(0,g)))v.push(D);let F={oldStart:m,oldLines:P-m+g,newStart:x,newLines:E-x+g,lines:v};p.push(F),m=0,x=0,v=[]}P+=h.length,E+=h.length}}for(let s of p)for(let d=0;d<s.lines.length;d++)s.lines[d].endsWith(`
48
+ `)?s.lines[d]=s.lines[d].slice(0,-1):(s.lines.splice(d+1,0,"\"),d++);return{oldFileName:i,newFileName:e,oldHeader:l,newHeader:o,hunks:p}}}function le(i){let e=i.endsWith(`
49
+ `),n=i.split(`
50
+ `).map(t=>t+`
51
+ `);return e?n.pop():n.push(n.pop().slice(0,-1)),n}function diffChars(before,after,options){return new C().diff(before,after,options)}return K(oe);}
52
+ export const {diffChars, diffLines, diffWordsWithSpace, structuredPatch} = createDiff();
@@ -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
  }
@@ -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 proposer.
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('proposal base SHA-256 differs from its text');
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 proposal base revision');
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 proposal base name');
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 proposal needs its proposer name');
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 proposal is an ISO 8601 UTC timestamp');
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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -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 proposal base');
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 proposal base');
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,
@@ -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/1' ||
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) || !record.splices.length || !_rapierValidLedgerRecord(record, earlier.at(-1) || null, earlier,
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
- if (startText === null) throw fault('history does not reverse from this document');
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/1', documentAuthority,
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
- if (traversed === null) throw fault('undo branch does not reverse');
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
- if (traversed === null) throw fault('redo branch does not replay');
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
- if (active === null || (i + 1 === cursor && active !== text)) throw fault('complete branch differs');
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: 4, documentAuthority: ledger.documentAuthority, documentRevision: ledger.head.revision,
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 ?? current.ledger.records.at(-1)?.transaction.createdAt ?? 0,
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
  }