Skip to content

feat(rust): make readable indented Links Notation the default encoding - #38

Merged
konard merged 2 commits into
mainfrom
issue-37-0e0bcabcea22
Aug 20, 2026
Merged

feat(rust): make readable indented Links Notation the default encoding#38
konard merged 2 commits into
mainfrom
issue-37-0e0bcabcea22

Conversation

@konard

@konard konard commented Aug 20, 2026

Copy link
Copy Markdown
Member

Closes #37.

Problem

Stored documents were unreadable: every string went through base64, so a router
state file looked like

(object ((str dHlwZQ==) (str Um91dGVyU3RhdGU=)) ((str c3VidHlwZQ==) (str VG9rZW5TdG9yZQ==)))

grep, git diff and human review were all useless on it, even though nothing
in the data actually needed encoding.

Solution

encode() now writes an indented, plain-text document. One ( ) construct
carries both objects and arrays at every level including the root; key value
lines make an object, bare-value lines make an array:

(
  type "RouterState"
  server (
    host "127.0.0.1"
    port 18878
  )
  models (
    "claude-haiku"
    "claude-opus"
  )
)
  • Strings are double-quoted and written as text; numbers, true, false and
    null stay bare, so "18878" and 18878 still decode to a string and an int
    respectively.
  • A value is base64-encoded only when it genuinely cannot be written as text
    (it contains control characters), and each such value is marked individually
    as (base64 "...") — the rest of the document stays readable.
  • An empty array is () and an empty object is ( + newline + ), so empty
    containers keep their type across a round trip.
  • The previous single-line form is kept under explicit names,
    encode_compact() / encode_obfuscated(); encode_with_indent() lets the
    indentation string be configured. Nothing but the readable form is the default.
  • decode() detects which form it is given, so previously written files keep
    decoding and are rewritten in the readable form the next time they are saved.
  • links-notation is raised to 0.14, where a parenthesis opens a nested
    indentation context — required for the nested form above to read back.

Because the links-notation AST does not preserve quoting ("42" and 42 both
parse to Ref("42")) and cannot tell a one-pair object from a two-element
array, the readable form is read back by a small line-aware tokenizer in
rust/src/readable.rs that keeps both distinctions.

Reproduction

experiments/issue-37/parenthesis-indentation contains the probe used to verify
that 0.14 is genuinely required: on 0.13 the nested server link flattens to
four loose references, on 0.14 it yields two pair-links.

Before/after output of the codec itself is visible in
cargo run --example basic_usage (the example now prints both the readable and
the compact form).

Tests

rust/tests/readable_format.rs (17 tests) covers every case listed in the issue:

  • ASCII keys and values appear verbatim, with no base64 in sight
  • output spans multiple indented lines and matches the documented shape exactly
  • the output is valid Links Notation, and server parses as one link of two
    pairs (the 0.14-only behaviour)
  • nested objects/arrays round-trip; an object used as an array element keeps its
    record boundary
  • numbers and booleans stay numbers and booleans; numeric- and boolean-looking
    strings stay strings; NaN/Infinity/-Infinity round-trip
  • quotes, apostrophes, parentheses and non-ASCII text round-trip as text
  • values with control characters are marked individually and round-trip, and a
    literal base64 key is not mistaken for the marker
  • documents written in the previous base64 form (the real string quoted in the
    issue) still decode, and encode_compact output still decodes
  • hand-written documents are accepted

rust/tests/documented_examples.rs pins the snippets used in the READMEs, and
rust/src/readable.rs carries 9 unit tests for the encoder/decoder internals.

Full local run: 41 unit + 17 readable + 5 documentation + 7 doc tests passing,
with cargo fmt --check, cargo clippy --all-targets -D warnings and
scripts/check-file-size.mjs clean.

Docs

Crate docs, rust/README.md and the root README.md no longer claim that UTF-8
support works "using base64 encoding"; they describe the readable format, the
output-format table (encode / encode_with_indent / encode_compact /
encode_obfuscated) and the (base64 "...") marker. A rust/changelog.d/
fragment requests a minor bump.

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: #37
@konard konard self-assigned this Aug 20, 2026
encode() now writes an indented, plain-text document that uses one ( )
construct for both objects and arrays at every level, including the root.
Keys and values are written verbatim, strings are double-quoted, and
numbers, true/false and null stay bare so types survive a round trip.

A value is base64-encoded only when it genuinely cannot be written as
text (it contains control characters), and each such value is marked
individually as (base64 "...") while everything around it stays readable.

The previous single-line base64 output is kept under the explicit names
encode_compact()/encode_obfuscated(), and decode() accepts both forms, so
existing files keep working and migrate on the next write.

Reading the readable form back needs links-notation 0.14 semantics, where
a parenthesis opens a nested indentation context, so the dependency is
raised to 0.14.

Closes #37
@konard konard changed the title [WIP] encode() base64-encodes every string on one line, so .lino files are not human-readable — the readable formatter already exists but is not the default feat(rust): make readable indented Links Notation the default encoding Aug 20, 2026
@konard
konard marked this pull request as ready for review August 20, 2026 05:42
@konard

konard commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

Working session summary

Done — PR #38 is ready for review with all CI checks green: #38

What landed (commit 29373e3, all six issue requirements):

  1. rust/Cargo.toml/Cargo.locklinks-notation 0.13 → 0.14 (parentheses open a nested indentation context). Verified as a genuine prerequisite by experiments/issue-37/parenthesis-indentation: on 0.13 the nested server link flattens to four loose refs, on 0.14 it yields two pair-links.
  2. rust/src/readable.rs (new) — indented plain-text encoder/decoder. One ( ) construct for objects and arrays at every level including the root; strings double-quoted, numbers/true/false/null bare; () = empty array, (\n) = empty object.
  3. encode_compact() / encode_obfuscated() keep the old single-line base64 form; encode_with_indent() configures the indent. encode() is readable, always.
  4. Base64 is used only for values containing control characters, marked individually as (base64 "…") — a literal base64 key is always quoted on write and the marker requires an unquoted ref on read, so the two never collide.
  5. decode() dispatches on the first line (is_compact_notation) and accepts both forms, so stored files keep decoding and migrate on next write.
  6. Crate docs, rust/README.md and the root README.md no longer claim UTF-8 works "using base64 encoding"; they document the format, the output-format table and the marker. rust/changelog.d/ fragment requests a minor bump (no manual version edit — CI forbids it).

Verification: 41 unit + 17 readable-format + 5 documentation + 7 doc tests, plus fmt/clippy/file-size clean locally; CI passed on ubuntu, macOS and Windows. The read-back path is a dedicated line-aware tokenizer rather than the links-notation AST, because that AST discards quoting ("42" and 42 both become Ref("42")) and cannot distinguish a one-pair object from a two-element array — both distinctions are covered by tests.


This summary was automatically extracted from the AI working session output.

@konard

konard commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $5.344790

📊 Context and tokens usage:

Claude Opus 5: (2 sub-sessions)

  1. 117.0K / 1M (12%) input tokens, 39.3K / 128K (31%) output tokens
  2. 60.6K / 1M (6%) input tokens, 12.9K / 128K (10%) output tokens

Total: (5.9K new + 142.2K cache writes + 4.7M cache reads) input tokens, 61.6K output tokens, $5.344790 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Thinking level: medium (~15999 tokens)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (2366KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
konard merged commit ea6d05f into main Aug 20, 2026
11 checks passed
@konard

konard commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

🎉 Auto-merged

This pull request has been automatically merged by hive-mind.

  • All CI checks have passed

Auto-merged by hive-mind with --auto-merge flag

konard added a commit that referenced this pull request Aug 20, 2026
PR #38 changed only the Rust codec and no other language's CI ran, because
each workflow filters itself by paths:. This adds .github/workflows/parity.yml
(no paths: filter) and scripts/check-language-parity.mjs, which fails a pull
request when one language's src/ changed without the others. An intentional
single-language change opts out with [skip-parity] in the PR title or body.
Covered by scripts/check-language-parity.test.mjs (7 cases).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

encode() base64-encodes every string on one line, so .lino files are not human-readable — the readable formatter already exists but is not the default

1 participant