libvctrl_core 3.2.0

Reference implementations of the libvctrl contracts (in-memory store, SHA-512 hasher, binary codec)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
# libvctrl_core

Reference implementations of the `libvctrl` contracts: a deterministic binary codec, a
SHA-512 content-addressing hasher, fluent object builders, and in-memory storage backends.
`libvctrl_core` is the layer that turns the abstract `libvctrl_handler` traits and immutable
types into working, production-ready components.

- **Crate:** `libvctrl_core` 3.2.0 (library, `std`-only)
- **Language:** Rust, edition 2024 — MSRV **1.96.0**
- **License:** MIT
- **Repository:** https://github.com/mroczect/libvctrl
- **Documentation:** https://docs.rs/libvctrl_core

> Most users should depend on the **`libvctrl` facade** rather than this crate directly.
> The facade re-exports everything in `libvctrl_core` plus the contracts and the crypto
> primitives under a single, ergonomic namespace. Reach for `libvctrl_core` directly only
> when you need the codec, builders, or in-memory stores without pulling in the facade's
> `crypto` namespace alias.

---

## Overview

`libvctrl_handler` defines _what_ a version control object model looks like — the traits
(`Encoder`, `Decoder`, `Hasher`, `ObjectStore`, `RefStore`) and the immutable data types
(`Blob`, `Tree`, `TreeEntry`, `Commit`, `CommitMeta`, `Tag`, `Hash`, `UserID`).
`libvctrl_core` defines _how_ those contracts are realised:

- A **binary codec** that serialises objects into a deterministic, versioned byte stream
  and parses untrusted byte streams back into validated objects.
- A **SHA-512 hasher** that bridges the raw `libvctrl_sha512` digest engine to the handler
  `Hash` type used for content addressing.
- **Fluent builders** for constructing validated `Blob`, `Tree`, `TreeEntry`, `Commit`,
  and `Tag` objects.
- **In-memory stores** implementing `ObjectStore` and `RefStore` over `HashMap`.

The crate is `std`-only: it relies on `std::io`, `std::sync::Arc`, `HashMap`, `String`, and
`Vec`. There is no `no_std` build path. The crate exposes **no crate-level feature flags**;
all feature configuration (`sha384`, `opt_size`) happens at the facade or
`libvctrl_sha512` level.

---

## Architecture

`libvctrl_core` sits in the middle of a strictly layered, one-way dependency graph. The
contract layer (`libvctrl_handler`) depends only on the standard library. The reference
layer (`libvctrl_core`) depends on the contracts and on the raw crypto engine
(`libvctrl_sha512`). Above it sit the facade and the higher-level command crates.

```mermaid
flowchart TD
    subgraph Apps["Application layer"]
        FACADE["libvctrl<br/>facade (re-exports)"]
        PL["libvctrl_plumbing"]
        PO["libvctrl_porcelain"]
    end

    subgraph Core["Reference implementation layer — libvctrl_core"]
        CODEC["codec<br/>BinaryEncoder / BinaryDecoder"]
        HASH["hash<br/>Sha512Hasher (adapter)"]
        OBJ["object<br/>builders"]
        STORE["store<br/>MemoryStore / MemoryRefStore"]
    end

    subgraph Foundation["Foundation"]
        HANDLER["libvctrl_handler<br/>contracts & types"]
        SHA["libvctrl_sha512<br/>raw crypto engine"]
    end

    FACADE --> Core
    PL --> Core
    PO --> Core
    CODEC --> HANDLER
    HASH --> HANDLER
    HASH --> SHA
    OBJ --> HANDLER
    STORE --> HANDLER
```

Internally, each module implements a specific handler trait against the contract types:

```mermaid
flowchart LR
    subgraph Handler["libvctrl_handler contracts"]
        ENC[Encoder trait]
        DEC[Decoder trait]
        HAS[Hasher trait]
        OS[ObjectStore trait]
        RS[RefStore trait]
        TYPES[Blob / Tree / Commit / Tag / Hash / UserID]
    end

    subgraph Core["libvctrl_core modules"]
        CODEC[codec<br/>BinaryEncoder + BinaryDecoder]
        HASHMOD[hash<br/>Sha512Hasher]
        OBJMOD[object<br/>builders]
        STOREMOD[store<br/>MemoryStore + MemoryRefStore]
    end

    CODEC -.implements.-> ENC
    CODEC -.implements.-> DEC
    HASHMOD -.implements.-> HAS
    STOREMOD -.implements.-> OS
    STOREMOD -.implements.-> RS
    CODEC -.consumes.-> TYPES
    HASHMOD -.produces.-> TYPES
    OBJMOD -.constructs.-> TYPES
```

### Codec round-trip

A typical interaction encodes an object, hashes the encoded bytes to obtain a content
address, stores the bytes, and later decodes them back. The decoder is the trust boundary:
it treats every input as untrusted and validates structure, UTF-8, and system limits before
constructing a handler type.

```mermaid
sequenceDiagram
    participant App as Application
    participant E as BinaryEncoder
    participant H as Sha512Hasher
    participant S as MemoryStore
    participant D as BinaryDecoder

    App->>E: encode_*(&object, &mut writer)
    E-->>App: Deterministic, versioned bytes
    App->>H: hash(&mut bytes.as_slice())
    H->>H: Stream SHA-512 in fixed chunks
    H-->>App: 64-byte Hash (content address)
    App->>S: put(&hash, &bytes)
    App->>S: get(&hash)
    S-->>App: Stored bytes
    App->>D: decode_*(reader)
    D->>D: read_bounded (hard size cap)
    D->>D: check_version (byte == 3)
    D->>D: require_byte / require_slice
    D->>D: Validate UTF-8 + re-check limits
    D-->>App: Validated immutable object
```

---

## Core Features

- **Deterministic serialization.** The same object always produces the same bytes:
  fixed version byte, little-endian integers, strict field order, no platform-specific
  layouts. Determinism is required for content addressing to be stable.
- **Defense-in-depth decoding.** `BinaryDecoder` bounds the input before parsing, checks
  every offset before slicing, validates all strings as UTF-8, and re-checks system limits
  after numeric conversion. No slice indexing occurs without a preceding bounds check.
- **DoS-resistant reads.** `read_bounded` uses a 4 KiB chunk buffer and refuses to allocate
  beyond a conservative per-object maximum, preventing allocation-based denial of service.
- **Versioned format.** Every encoded object begins with a version byte (`3`). The decoder
  rejects any input whose first byte does not match, allowing the format to evolve safely.
- **Thin hasher adapter.** `Sha512Hasher` is a zero-sized, stateless, thread-safe adapter
  that bridges the raw `libvctrl_sha512` engine to the handler `Hash` type, producing
  64-byte addresses matching `HASH_LENGTH`.
- **Fluent builders.** Construct validated `Blob`, `Tree`, `TreeEntry`, `Commit`, and `Tag`
  objects through ergonomic builder APIs.
- **In-memory stores.** `MemoryStore` and `MemoryRefStore` provide ready-to-use
  `ObjectStore` and `RefStore` implementations backed by `HashMap`.
- **Strict safety.** `#![forbid(unsafe_code)]` and a comprehensive set of denied Clippy and
  rustc documentation lints are inherited from the workspace.

---

## Technology Stack

- **Language:** Rust (edition 2024, MSRV 1.96.0)
- **Dependencies:**
  - `libvctrl_handler` 5.2.0 — contracts, types, constants, validation (path dependency)
  - `libvctrl_sha512` 3.2.0 — raw SHA-512 / HMAC / HKDF engine, **with default features**
    (SHA-384 enabled)
- **Dev-dependencies:** `proptest` 1.11.0
- **Lint policy:** workspace-inherited, `#![forbid(unsafe_code)]`, denied missing-docs,
  rust-2018-idioms, and a broad set of Clippy lints (including pedantic and nursery
  groups). See the repository for the authoritative lint configuration.
- **Feature flags:** none at the crate level.

> Note on the MSRV field: this crate's `Cargo.toml` does not yet declare
> `rust-version = "1.96"`. The workspace standard is Rust 1.96.0, and this README
> documents that as the MSRV. Adding `rust-version = "1.96"` to
> `libvctrl_core/Cargo.toml` is recommended in a future maintenance pass.

---

## Project Structure

```text
libvctrl_core/
├── Cargo.toml
└── src/
    ├── lib.rs
    ├── codec/
    │   ├── mod.rs
    │   ├── binary_encoder.rs
    │   └── binary_decoder.rs
    ├── hash/
    │   ├── mod.rs
    │   └── sha512.rs
    ├── object/
    │   ├── mod.rs
    │   ├── blob.rs
    │   ├── commit.rs
    │   ├── tag.rs
    │   └── tree.rs
    └── store/
        ├── mod.rs
        ├── memory.rs
        └── ref_store.rs
```

---

## Getting Started

### Prerequisites

- Rust toolchain **1.96.0** or newer (edition 2024 is required)
- Cargo

No system libraries or external services are required.

### Installation

For most users, depend on the facade instead:

```toml
[dependencies]
libvctrl = "2.2"
```

To depend on `libvctrl_core` directly (codec/builders/stores only, without the facade's
`crypto` namespace alias):

```toml
[dependencies]
libvctrl_core = "3.2"
```

Or via Cargo:

```bash
cargo add libvctrl_core
```

This will pull `libvctrl_handler` and `libvctrl_sha512` as transitive dependencies.

### Configuration

`libvctrl_core` exposes **no crate-level feature flags**. Feature configuration
(`sha384`, `opt_size`) is controlled at the `libvctrl` facade or `libvctrl_sha512` level.
Because this crate depends on `libvctrl_sha512` **with default features**, SHA-384 support
is enabled transitively. If you need to control the crypto feature set, use the facade,
which wires `libvctrl_sha512` with `default-features = false` and re-enables only what it
needs.

---

## Usage

### Encode, hash, store, and decode a Blob

```rust
use std::io::Cursor;
use libvctrl_handler::{Blob, Decoder, Encoder, Hasher, ObjectStore};
use libvctrl_core::codec::{BinaryEncoder, BinaryDecoder};
use libvctrl_core::hash::Sha512Hasher;
use libvctrl_core::store::MemoryStore;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Create a validated blob.
    let blob = Blob::new(b"hello world".to_vec())?;

    // 2. Encode into a deterministic, versioned byte stream.
    let mut encoded = Vec::new();
    BinaryEncoder.encode_blob(&blob, &mut encoded)?;

    // 3. Hash the encoded bytes to obtain a 64-byte content address.
    let hash = Sha512Hasher.hash(&mut encoded.as_slice())?;

    // 4. Store the encoded object in memory.
    let mut store = MemoryStore::new();
    store.put(&hash, &encoded)?;

    // 5. Retrieve and decode back into a validated object.
    let reader = store.get(&hash)?;
    let decoded = BinaryDecoder.decode_blob(reader)?;
    assert_eq!(decoded, blob);
    Ok(())
}
```

### Encode and decode a Tree

```rust
use std::io::Cursor;
use libvctrl_handler::{Encoder, Decoder, EntryKind, Hash, Tree, TreeEntry};
use libvctrl_core::codec::{BinaryEncoder, BinaryDecoder};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let hash = Hash::from_bytes(&[0u8; 64])?;
    let entry = TreeEntry::new("a.txt".to_owned(), EntryKind::Blob, hash)?;
    let tree = Tree::new(vec![entry])?;

    let mut encoded = Vec::new();
    BinaryEncoder.encode_tree(&tree, &mut encoded)?;

    let decoded = BinaryDecoder.decode_tree(Cursor::new(encoded.as_slice()))?;
    assert_eq!(decoded, tree);
    Ok(())
}
```

### Encode and decode a Commit

```rust
use std::io::Cursor;
use libvctrl_handler::{Commit, Decoder, Encoder, Hash, UserID};
use libvctrl_core::codec::{BinaryEncoder, BinaryDecoder};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tree = Hash::from_bytes(&[1u8; 64])?;
    let author = UserID::new("Alice".to_owned(), "alice@example.com".to_owned())?;
    let committer = UserID::new("Bob".to_owned(), "bob@example.com".to_owned())?;
    let commit = Commit::new(tree, vec![], author, committer, "Initial commit".to_owned())?;

    let mut encoded = Vec::new();
    BinaryEncoder.encode_commit(&commit, &mut encoded)?;

    let decoded = BinaryDecoder.decode_commit(Cursor::new(encoded.as_slice()))?;
    assert_eq!(decoded, commit);
    Ok(())
}
```

### Hash a streaming input

```rust
use libvctrl_core::hash::Sha512Hasher;
use libvctrl_handler::Hasher;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let hash = Sha512Hasher.hash(b"hello world".as_ref())?;
    assert_eq!(hash.as_bytes().len(), 64);
    Ok(())
}
```

### Building objects with fluent builders

The `object` module provides ergonomic builders that wrap the handler constructors with
validation. The builder surface mirrors the facade re-exports
(`BlobBuilder`, `CommitBuilder`, `TagBuilder`, `TreeBuilder`, `TreeEntryBuilder`):

```rust
// Conceptual — see docs.rs/libvctrl_core for the exact builder API.
// use libvctrl_core::object::{BlobBuilder, CommitBuilder, TreeBuilder, TreeEntryBuilder};
// use libvctrl_handler::{EntryKind, Hash, UserID};
//
// let blob = BlobBuilder::new().data(b"content".to_vec()).build()?;
// let entry = TreeEntryBuilder::new("file.txt".to_owned(), EntryKind::Blob, hash).build()?;
// let tree  = TreeBuilder::new().entry(entry).build()?;
// let user  = UserID::new("Alice".to_owned(), "alice@example.com".to_owned())?;
// let commit = CommitBuilder::new()
//     .tree(tree_hash)
//     .author(user.clone())
//     .committer(user)
//     .message("Initial commit")
//     .build()?;
```

---

## API Reference / Core Modules

Full API documentation is published at <https://docs.rs/libvctrl_core>. The summary below
describes each module and, for the codec, the canonical binary format.

### `codec` — Binary codec

Implements the `Encoder` and `Decoder` traits. Two zero-sized types:

- **`BinaryEncoder`** — writes objects to any `std::io::Write` sink. Stateless; streams
  fields directly without allocating the entire payload upfront. All length conversions are
  checked with `try_from` so impossible lengths are reported as
  `VctrlError::SerializationError` rather than causing silent truncation.
- **`BinaryDecoder`** — reads objects from any `std::io::Read` source. The trust boundary
  for untrusted input.

The current format version is `VERSION = 3` (`pub const VERSION: u8 = 3`). The decoder
rejects any input whose first byte does not match `EXPECTED_VERSION = 3`.

#### Decoder validation pipeline

The decoder follows a defense-in-depth strategy. No slice indexing is performed without a
preceding bounds check.

```mermaid
flowchart TD
    R[Untrusted Read stream] --> RB[read_bounded<br/>4 KiB chunks, hard size cap]
    RB -->|exceeds cap| ERR1[VctrlError::CorruptedData]
    RB --> CV[check_version<br/>first byte == 3]
    CV -->|wrong/missing| ERR2[VctrlError::CorruptedData]
    CV -->|ok| PARSE[Parse with require_byte / require_slice<br/>overflow-safe slicing]
    PARSE -->|truncated / unknown kind / trailing bytes| ERR3[VctrlError::CorruptedData]
    PARSE --> UTF[Validate all strings as UTF-8]
    UTF -->|invalid| ERR4[VctrlError::CorruptedData]
    UTF --> LIM[Re-check system limits<br/>MAX_BLOB_SIZE / MAX_TREE_ENTRIES / MAX_MESSAGE_LENGTH]
    LIM -->|exceeds limit| ERR5[VctrlError::CorruptedData / SerializationError]
    LIM --> CONSTRUCT[Construct handler type<br/>Blob / Tree / Commit / Tag]
    CONSTRUCT --> OBJ[Immutable validated object]
```

#### Binary format layouts

All integers are little-endian. Strings are `u8` length-prefixed. Larger payloads use
`u32` or `u64` length prefixes. Every object begins with a one-byte version field (`3`).

**Blob**

| Offset | Size       | Field               |
| ------ | ---------- | ------------------- |
| 0      | 1          | Version byte        |
| 1      | 8          | `data_len` (u64 LE) |
| 9      | `data_len` | Raw blob data       |

**Tree**

| Offset | Size   | Field                        |
| ------ | ------ | ---------------------------- |
| 0      | 1      | Version byte                 |
| 1      | 4      | `entry_count` (u32 LE)       |
| 5      | varies | Repeated entries (see below) |

Each tree entry:

| Field       | Size       |
| ----------- | ---------- |
| `name_len`  | 1 (u8)     |
| `name`      | `name_len` |
| `kind_byte` | 1 (u8)     |
| `hash`      | 64         |

`EntryKind` discriminants:

| Byte | Kind       |
| ---- | ---------- |
| 0    | Blob       |
| 1    | Executable |
| 2    | Symlink    |
| 3    | Tree       |
| 4    | Submodule  |

**Commit**

| Field                  | Size        |
| ---------------------- | ----------- |
| Version                | 1           |
| Tree hash              | 64          |
| Parent count           | 2 (u16 LE)  |
| Parent hashes          | 64 * count  |
| Author name length     | 1           |
| Author name            | length      |
| Author email length    | 1           |
| Author email           | length      |
| Committer name length  | 1           |
| Committer name         | length      |
| Committer email length | 1           |
| Committer email        | length      |
| Message length         | 4 (u32 LE)  |
| Message                | length      |
| Timestamp              | 8 (i64 LE)  |
| Timezone offset        | 2 (i16 LE)  |
| Encoding length        | 1           |
| Encoding               | length or 0 |

**Tag**

| Field               | Size           |
| ------------------- | -------------- |
| Version             | 1              |
| Name length         | 1              |
| Name                | length         |
| Target hash         | 64             |
| Tagger presence     | 1              |
| Tagger name length  | 1 (if present) |
| Tagger name         | length         |
| Tagger email length | 1 (if present) |
| Tagger email        | length         |
| Message length      | 4 (u32 LE)     |
| Message             | length         |
| Timestamp           | 8 (i64 LE)     |
| Timezone offset     | 2 (i16 LE)     |
| Encoding length     | 1              |
| Encoding            | length or 0    |

#### Error mapping

The codec surfaces three `VctrlError` variants:

- `VctrlError::CorruptedData` — version mismatch, missing/truncated length prefix,
  length mismatch, unknown entry-kind byte, trailing bytes, invalid UTF-8, or input
  exceeding the bounded size cap. The dominant failure mode on the decode path.
- `VctrlError::SerializationError` — length overflow that cannot be represented in the
  prefix integer (e.g. a name longer than `u8::MAX`, a message longer than `u32::MAX`),
  or a message exceeding `MAX_MESSAGE_LENGTH`. The dominant failure mode on the encode
  path.
- `VctrlError::IoError` — underlying `Read`/`Write` failure, wrapped via
  `std::sync::Arc` so the error remains cloneable.

### `hash` — Content addressing

- **`Sha512Hasher`** — a zero-sized, stateless, thread-safe type implementing the `Hasher`
  trait. It is a **thin adapter** that bridges the raw `libvctrl_sha512` digest engine to
  the handler `Hash` type. The `hash` method reads from any `std::io::Read` in fixed-size
  chunks, feeds each chunk into the SHA-512 engine, and finalises into a 64-byte `Hash`
  whose length always matches `HASH_LENGTH`.

### `object` — Fluent builders

Builders for `Blob`, `Commit`, `Tag`, `Tree`, and `TreeEntry`. Each builder wraps the
corresponding handler constructor with validation and a fluent API:

- `BlobBuilder`
- `CommitBuilder`
- `TagBuilder`
- `TreeBuilder`
- `TreeEntryBuilder`

See <https://docs.rs/libvctrl_core> for the exact builder method surface. The builders are
also re-exported at the root of the `libvctrl` facade.

### `store` — In-memory storage

Two `HashMap`-backed implementations:

- **`MemoryStore`** — implements `ObjectStore`. Stores encoded object bytes keyed by `Hash`.
- **`MemoryRefStore`** — implements `RefStore`. Stores named references (e.g. branch and
  tag names) pointing at `Hash` values.

Both are intended for testing, prototyping, and ephemeral in-process VCS sessions. For
persistent or remote backends, implement `ObjectStore` and `RefStore` directly.

---

## Testing

Run the crate's test suite with Cargo:

```bash
cargo test
```

Property-based tests use `proptest` (a dev-dependency). To run the entire workspace test
suite from the repository root:

```bash
cargo test --workspace
```

To verify the strict lint policy is satisfied:

```bash
cargo clippy --workspace --all-targets -- -D warnings
cargo doc --workspace --no-deps
```

---

## Contributing

Contributions are welcome. The crate enforces `#![forbid(unsafe_code)]` and inherits a
strict, denied Clippy and documentation-lint policy from the workspace; all public items
must be documented.

For contribution guidelines, code style, and the full lint configuration, see the
repository's `CONTRIBUTING.md` and the workspace root `README.md`:

- Repository: https://github.com/mroczect/libvctrl

When contributing to `libvctrl_core`, keep the contract/implementation boundary clean: new
behaviour belongs here, new contracts belong in `libvctrl_handler`, and new user-facing
commands belong in `libvctrl_plumbing` or `libvctrl_porcelain`.

---

## Ecosystem

`libvctrl_core` is one layer of a larger workspace. The related crates are listed below;
each has its own documentation.

| Crate                | Role                                                     | Documentation                      |
| -------------------- | -------------------------------------------------------- | ---------------------------------- |
| `libvctrl`           | Facade: re-exports contracts, reference impl, and crypto | https://docs.rs/libvctrl           |
| `libvctrl_handler`   | Contract layer: traits, types, limits, validation        | https://docs.rs/libvctrl_handler   |
| `libvctrl_sha512`    | Zero-dependency SHA-512 / HMAC / HKDF primitives         | https://docs.rs/libvctrl_sha512    |
| `libvctrl_plumbing`  | Command-level VCS operations built on `libvctrl_core`    | https://docs.rs/libvctrl_plumbing  |
| `libvctrl_porcelain` | High-level, user-facing VCS operations                   | https://docs.rs/libvctrl_porcelain |

The dependency flow is strictly one-way: `handler` is the foundation, `core` implements the
contracts, the facade re-exports both, and `plumbing`/`porcelain` build on `core`.

```mermaid
flowchart LR
    H[libvctrl_handler<br/>contracts] --> C[libvctrl_core<br/>reference impl]
    C --> PL[libvctrl_plumbing]
    C --> PO[libvctrl_porcelain]
    H --> F[libvctrl<br/>facade]
    C --> F
```

---

## License

Licensed under the MIT License. See the repository for the full license text.