lix 0.16.0

Embeddable version control for apps and AI agents.
Documentation
package lix:plugin@2.0.0;

interface host {
  type bytes = list<u8>;

  variant host-error {
    invalid-range,
    limit-exceeded(string),
    rejected(string),
  }

  record record-chunk {
    total-len: u64,
    bytes: bytes,
  }

  /// One immutable host-owned file and projection-state version.
  resource snapshot {
    file-len: func() -> u64;
    read-file: func(offset: u64, length: u32) -> result<bytes, host-error>;
    read-state: func(
      key: bytes,
      offset: u64,
      max-bytes: u32,
    ) -> result<option<record-chunk>, host-error>;
  }

  /// One bounded, self-describing page of row mutations.
  record row-page {
    payload: bytes,
    /// Large typed values are carried out-of-band from the page descriptor.
    /// Payload records reference these page-local attachments by ordinal.
    attachments: list<bytes>,
  }

  resource row-source {
    next-page: func(max-bytes: u32) -> result<option<row-page>, host-error>;
  }

  record file-edit {
    offset: u64,
    delete-len: u64,
    insert: bytes,
  }

  /// Host-owned atomic output shared by the four projection operations.
  /// The SDK exposes only the methods appropriate for each operation.
  resource transition {
    max-batch-bytes: func() -> u32;
    put-state: func(key: bytes, value: bytes) -> result<_, host-error>;
    delete-state: func(key: bytes) -> result<_, host-error>;
    /// Delete matching accepted and pending private state. Later writes survive.
    /// Empty prefixes and prefixes overlapping host-reserved state are rejected.
    delete-state-prefix: func(prefix: bytes) -> result<_, host-error>;
    emit-rows: func(page: row-page) -> result<_, host-error>;
    replace-all-rows: func() -> result<_, host-error>;
    emit-file-edit: func(edit: file-edit) -> result<_, host-error>;
    begin-file-replacement: func(total-length: u64) -> result<_, host-error>;
    write-file-replacement: func(chunk: bytes) -> result<_, host-error>;
    finish-file-replacement: func() -> result<_, host-error>;
  }

  enum merge-side {
    base,
    a,
    b,
  }

  record column-merge-meta {
    ordinal: u32,
    schema-key: string,
    /// Each primary-key component is one Schema v1 value encoded by the
    /// canonical typed-value codec, never a lossy textual identity.
    primary-key: list<bytes>,
    schema-fingerprint: list<u8>,
    file-id: option<string>,
    column: string,
    base-len: option<u64>,
    a-len: option<u64>,
    b-len: option<u64>,
    base-row-len: u64,
    a-row-len: u64,
    b-row-len: u64,
  }

  /// Lazy access to overlapping column changes and their complete row context.
  resource column-merge-source {
    len: func() -> u32;
    get: func(index: u32) -> result<column-merge-meta, host-error>;
    read-value: func(
      index: u32,
      side: merge-side,
      offset: u64,
      length: u32,
    ) -> result<option<bytes>, host-error>;
    /// The range contains canonical typed-value bytes.
    read-row: func(
      index: u32,
      side: merge-side,
      offset: u64,
      length: u32,
    ) -> result<bytes, host-error>;
    /// The range contains one canonical typed-row encoding.
  }

  /// Ordered column merge results. Omitted results are invalid; `use-lww`
  /// explicitly accepts the host's higher-ranked `b` value.
  resource column-merge-sink {
    max-batch-bytes: func() -> u32;
    use-lww: func(ordinal: u32) -> result<_, host-error>;
    begin-replace: func(
      ordinal: u32,
      total-length: option<u64>,
    ) -> result<_, host-error>;
    write-replacement: func(chunk: bytes) -> result<_, host-error>;
    finish-replace: func() -> result<_, host-error>;
  }
}

interface types {
  use host.{bytes, file-edit, row-source, snapshot};

  variant plugin-error {
    invalid-input(string),
    limit-exceeded(string),
    internal(string),
  }

  record create-context {
    high: u64,
    low: u32,
  }

  record parse-request {
    file-id: string,
    path: string,
    file: own<snapshot>,
    creates: create-context,
  }

  record parse-changes-request {
    file-id: string,
    before-path: string,
    after-path: string,
    before: own<snapshot>,
    file-edits: list<file-edit>,
    /// Cold reconciliation receives current durable typed rows to preserve
    /// semantic identities. Warm incremental execution omits them.
    rows: option<own<row-source>>,
    creates: create-context,
  }

  record serialize-request {
    file-id: string,
    path: string,
    rows: own<row-source>,
    before: option<own<snapshot>>,
  }

  record serialize-changes-request {
    file-id: string,
    path: string,
    before: own<snapshot>,
    row-changes: own<row-source>,
  }
}

interface column-merger {
  use host.{column-merge-sink, column-merge-source};
  use types.{plugin-error};

  /// Called only for columns changed differently on both concurrent sides.
  merge: func(
    input: own<column-merge-source>,
    output: borrow<column-merge-sink>,
  ) -> result<_, plugin-error>;
}

interface file-projection {
  use host.{transition};
  use types.{parse-changes-request, parse-request, plugin-error,
    serialize-changes-request, serialize-request};

  parse: func(
    input: parse-request,
    output: borrow<transition>,
  ) -> result<_, plugin-error>;

  parse-changes: func(
    input: parse-changes-request,
    output: borrow<transition>,
  ) -> result<_, plugin-error>;

  serialize: func(
    input: serialize-request,
    output: borrow<transition>,
  ) -> result<_, plugin-error>;

  serialize-changes: func(
    input: serialize-changes-request,
    output: borrow<transition>,
  ) -> result<_, plugin-error>;
}

world column-merger-plugin {
  import host;
  export column-merger;
}

world file-projection-plugin {
  import host;
  export file-projection;
}

world plugin {
  import host;
  export column-merger;
  export file-projection;
}