# API contracts

The root package is portable MoonBit, tested on Wasm GC and JavaScript. `/host` and `/cli` explicitly support JavaScript only (Node.js 22.16+).

| API | Contract |
| --- | --- |
| `create_options(name)` | Hybrid, 256 KiB pieces, multifile, public, no endpoints by default. |
| `recommend_piece_length(files, mode=Hybrid, target_pieces=4096)` | Return a power of two in 16 KiB..16 MiB. Aim for the target; for v2/hybrid the effective target is at least the nonempty file count. The target is soft at 16 MiB; the 400,000-piece limit is hard. Empty/oversized payloads and invalid targets raise. |
| `validate_sources(files)` | Validate portable paths, case/prefix collisions, signed lengths and exact host offset range; return real bytes. |
| `sorted_source_indices(files)` | Deterministic raw UTF-8 path order; indices retain the original caller mapping. |
| `plan_torrent(files, options)` | Validate paths/options and piece limits; return real/logical storage bytes, virtual data padding, file/empty counts, both scheme piece counts, exact v1 hash bytes and an upper bound for v2 piece-layer bytes. No read callback, payload hashes or infohash. |
| `plan_json(plan)` | Lossless byte-size strings in schema `swarmcrate.plan.v1`; counts are numeric. |
| `create_torrent(files, options, read)` | Return canonical `.torrent` bytes. Callback `(originalIndex, offset:Int64, size:Int) -> Bytes raise CrateError` must return exactly `size` bytes. All creation modes read each real byte once. Hybrid feeds the same bounded reads to both hash schemes, generates virtual zeros internally, and preserves deterministic file order and original callback indices. Reads are at most 1 MiB, never cross a piece boundary, and v2 Merkle leaves remain 16 KiB regardless of read size. |
| `inspect_torrent(data)` | Strict syntax, file layout, hash count, v2 piece-layer/root validation and hybrid layout consistency. Return **real files only**, no virtual padding files. |
| `expected_v1_pieces(data)` | Concatenated 20-byte SHA-1 hashes; empty for pure v2. |
| `expected_v2_files(data)` | File roots and piece layers in inspected file order; empty for pure v1. |
| `torrent_storage_bytes(data)` | V1/hybrid storage including virtual padding; real bytes for pure v2. |
| `sha1_pieces(files, pieceLength, read)` | Hash the supplied order exactly; crosses file boundaries and skips empty files. Reject invalid widths and more than 400,000 pieces before any read callback. |
| `piece_spans(files, pieceLength, index)` | Map a v1 piece in the supplied **unpadded** file concatenation to real ranges. For padded v1/hybrid input use `verify_payload` reports, which account for virtual padding internally. |
| `hash_v2_file(length, pieceLength, read)` | Block/Merkle hashing; callback has no file index. Empty files return empty root/layer. |
| `validate_piece_layer(root, length, pieceLength, layer)` | Authenticate a multi-piece file's entire layer against its root. |
| `verification_scope(data, selected_files=None)` | Resolve selected and required file indices before I/O. `None` selects all. Explicit indices are sorted/deduplicated; empty/out-of-range selections raise. Required indices also include v1 boundary neighbours, without expanding recursively. |
| `verify_payload(data, sizes, read, detail_limit=4096, selected_files=None)` | `sizes` is `Array[Int64?]` in **inspected order**, including placeholders for unselected files; `None` indicates unavailable file. Hash/size failures return `ok=false`; invalid metainfo/mappings raise. Only selected pieces and necessary v1 boundary bytes are read. |
| `torrent_magnet(data)` | Exact validated infohashes, display name, unique trackers. |
| `parse_magnet(uri)` / `format_magnet(value)` | v1 hex/Base32 input, v2 `1220` SHA-256 multihash; reject conflicting hashes, invalid escapes and invalid UTF-8. |
| `edit_distribution(data, trackers, webSeeds)` | Replace both lists, preserve unknown top-level fields and exact original `info` bytes; private inputs must retain trackers. |
| `info_json`, `verification_json`, `verification_html` | Schema-tagged data and escaped, script-free offline report. |
| `verify_with_receipt(data, lengths, read, detail_limit=4096, selected_files=None)` | Return `(VerificationReport, VerificationReceipt)` from the same parsed input and verification. Capture exact full-metainfo SHA-256 and copy caller sizes before read callbacks. |
| `receipt_json_text(receipt)` | Return the immutable `swarmcrate.receipt.v1` JSON snapshot; private receipt fields prevent external construction from unrelated report/subject objects. This is not a signature or saved-receipt revalidation API. |

`CrateError(code, detail)` is a structured checked error. Missing or incorrectly sized empty files still fail verification, even though they contain no pieces. Read failures are unreadable failures, never trusted zero data. A v2 read error affects only the current piece and checking continues at the next piece; a v1 boundary piece lists all contributing real ranges. Piece counts in a hybrid report count both SHA-1 and SHA-256 checks. Detail limits cap stored piece failure entries; aggregate counters continue through the whole payload.

From 0.5.0, hybrid verification shares each real read between SHA-1 and SHA-256. Successful full verification reads each real byte once; a subset reads only selected real files because validated hybrid files are piece-aligned. Requests remain at most 1 MiB and never cross a piece boundary. SHA-1 includes virtual data zeros where required; SHA-256 keeps its separate zero-hash/Merkle rules. Neither scheme can certify the other: both expected hashes must match. Public signatures and JSON schemas are unchanged.

If a chunk is unavailable, both checks for that piece are unreadable; checking resumes at the next piece without retrying the failed piece for another scheme. This also applies to transient errors and callbacks that cannot rewind. Stable inputs and deterministic errors retain the former two-pass report: SHA-1 failures first, then SHA-256, with the same indices, spans, counters and returned-detail limit. Later SHA-1 failures take precedence over earlier deferred SHA-256 details when the limit is small. Pending diagnostic storage is bounded; no second actual file piece layer is constructed.

`VerificationReport.scope.complete` is true only when **every listed file, including empty files, was selected**. `ok` describes the selected scope, not skipped files. `checked_pieces + skipped_pieces` accounts for every hash check in the metainfo. Required neighbours contribute only bytes of shared v1 pieces; their other bytes are not certified. Size errors for required neighbours also fail the selected scope. v2 and hybrid alignment need no neighbouring real file bytes. The JSON verification schema is `swarmcrate.verify.v2` from version 0.2.0; consumers must inspect `complete`, `selected_files`, `required_files` and `skipped_pieces` before treating a result as a whole-payload check. HTML explicitly labels successful subsets **PARTIAL PASS**.

CLI `verify --file PATH` is repeatable. Match the exact path shown by `inspect`, case-sensitive, with `/` separators and without prepending the torrent's name. An unknown path is an input error; duplicate selections are harmless. Omitting `--file` retains full verification.

CLI `create` defaults to `--piece-length auto` from 0.3.0. The chosen size appears in its `info.piece_length` output. Pass an explicit integer byte count to pin a layout; automatic and explicit choices with the same size produce the same infohashes. The library's `create_options` default stays at 256 KiB for existing callers; use `recommend_piece_length` explicitly for an automatic policy. v1 sizing counts concatenated bytes; v2/hybrid sizing counts separately aligned file pieces. A selected layout is checked before payload I/O. Command-level `--help` works without input files.

From 0.7.0, `plan INPUT [creation options]` uses the same option preparation and automatic policy as creation, scans filesystem metadata, computes a plan, and checks scan consistency. It does not open payload files, hash content or write outputs. `single_file` identifies the layout chosen for the input. Creation also runs the same planning preflight before payload I/O; its existing output format is unchanged.

`storage_bytes` describes logical layout and includes virtual v1 data padding for hybrids, not physical disk allocation or network traffic. Padding follows canonical file order; a trailing empty file can require padding after the previous nonempty file. V2 Merkle zero hashes do not add data bytes. The v2 layer bound sums per-file layers before equal roots can be deduplicated. It is not the size of complete bencoded metainfo, a runtime/memory estimate, or a guarantee that final metadata fits all encoding/node limits. Actual creation/inspection still enforces those limits. Planning cannot establish payload integrity or content identity.

Creating a directory omits empty directories from torrent metadata (BitTorrent represents files). From 0.6.0, the CLI nevertheless snapshots **all scanned directories**, including empty ones, and rechecks membership and identity before writing created metainfo. New/removed/renamed entries or directory replacement abort creation with exit 1, without committing new output or modifying an existing output. Entry order alone is not a change. Directory identity tokens are checked around listing; sorted names are compared as well, without relying solely on timestamp resolution.

Source tokens contain device/inode/size/mtime/ctime; the CLI checks them around reads, before committing created metadata, and after verification before writing reports. `host.check_unchanged(path, snapshot)` also detects a change after the last read, including an empty or missing file that was never read. A detected change aborts with exit 1 and no HTML output. Only required files are inspected for a selected verification. These sequential checks detect ordinary concurrent edits; they do not create an atomic filesystem snapshot or defeat a privileged adversary racing/replacing directory ancestors. Use stable, trusted local files for publication. Extra files not mentioned by the torrent do not affect verification; this verifies the listed payload, not a complete filesystem snapshot.

Tracker ordering is retained as tiers. Web Seeds use BEP 19 `url-list`. Private metadata uses `private=1`. Unknown top-level extensions survive edits; unsupported file attributes or future meta versions are rejected explicitly.

From 0.4.0, standalone v1 multifile inputs also accept BEP 47 `attr="p"` padding. Each padding entry must fill the remainder of an already partial piece (positive length strictly below the piece length). Trailing padding is accepted; padding does not require every subsequent real file to be aligned. Its path may be omitted; present paths must have 1..32 safe components, but may repeat and are never accessed on disk. Only real files appear in `TorrentInfo.files`, `sizes`, read callback indices, selections and failure spans. `torrent_storage_bytes` and expected SHA-1 counts include the virtual zero bytes. At most 65,536 **storage entries including padding** and 400,000 storage pieces are accepted. The creator continues producing unpadded standalone v1 output.

This is a deliberate subset of BEP 47, not full extended-attribute support. Unknown attributes, attribute combinations, symlink targets and single-file attributes other than empty are rejected. In particular, single-file link attributes that older versions ignored now raise a metainfo error. Padding paths never become filesystem requests; real `.pad` source paths remain reserved.

## Cooperative creation and staged publication

From 0.11.0, `create_torrent_with_control(files, options, read, on_progress)` returns complete validated bytes, or raises without returning metainfo on cancellation/read/observer errors. Inputs are preflighted before copying mutable path/endpoint arrays. Original callback indices, canonical output and single-pass reads are preserved. Creation events distinguish start, before/after successful real reads, before encoding and finalization; byte progress is not metainfo completion.

JS-only `cli.execute_create_with_control(args, callback)` adds a final `before_write` checkpoint and checks the scan both before and after that callback. It uses new `host.write_new_atomic`: exclusive temporary staging, fsync/close, non-replacing hard-link publication, then owned-stage cleanup. Hard-link support is required; unsupported filesystems fail closed. Ordinary CLI/host write interfaces are unchanged. Cancel returns command code 3; old command exit codes remain unchanged.

No cancellable callback runs after publication begins. A process interrupted during staging leaves no published target; interruption after the link leaves complete bytes, though hidden staging data may remain. Source checks are sequential, not an atomic filesystem snapshot. See [creation checkpoints, portability and fault evidence](creation-control.md).

## Cooperative verification control

`verify_with_control(data, sizes, read, on_progress, detail_limit=4096, selected_files=None)` is additive from 0.10.0 and returns the same report/receipt pair as `verify_with_receipt`. `on_progress(VerificationProgress) -> Bool raise CrateError` receives synchronous scalar snapshots at start, before/after each real read, after each hash comparison, and before finalization. False raises `cancelled`; observer errors retain their code/detail. Neither returns a report or receipt, including when all checks have already finished. Ordinary reader errors remain unreadable-piece failures, even if their code is `cancelled`.

Check totals describe the selected scope and count both hybrid schemes. Completed counts are not a passing result. Read counts represent callback attempts; validated byte counts include only exact successful chunks, not failed/partial reads or OS traffic. An empty-file selection may have zero checks but still fail its size/existence check. Source selection, hash layout and legacy JSON/exit codes are unchanged.

Cancellation is cooperative, not preemptive or asynchronous; browser event-loop/Worker integration and resume are not implemented here. Creation has its own separate control API described above. Callers own storage stability and only save returned receipts after their own final source check. See [checkpoint fields, limits and executable consumers](sdk-control.md).

## Per-file candidate ranges

From 0.9.0, `receipt_diagnostics_json_text(receipt)` exports immutable `swarmcrate.diagnostics.v1` JSON, also embedded as the additive `file_diagnostics` field in new `swarmcrate.receipt.v1` receipts. CLI `verify --diagnostics NEW.json` exports it independently of `--receipt`; HTML includes the grouped table. Outputs share preflight and post-read consistency checks; stdout and exit codes stay unchanged.

Rows cover only required files and retain original indices/paths, distinguishing selected files from v1 boundary dependencies. Adjacent/overlapping physical ranges are unioned per category and across both hash schemes. Missing content and extra tails are separate; the attention union excludes extra bytes beyond expected content. Missing empty files retain issues with no byte interval. Hash-mismatch and unreadable-piece ranges are candidates, not exact corrupt-byte or failed-read locations; a shared v1 piece cannot identify the file causing a read failure.

`--detail-limit N` selects 1..10000 stored failure checks (default 4096), without limiting actual selected-scope checking or aggregate counts. Truncation makes every row's `ranges_complete=false`; recorded file counts/ranges may omit problems, and an empty row is not evidence of success. Size issues remain exhaustive. See [diagnostic schema and examples](file-diagnostics.md) for size-mismatch read policy, 64-bit string offsets and range semantics.

## Verification receipts (portable core, optional CLI export)

From 0.8.0, optional CLI `verify --receipt NEW.json` saves an identity/scope-bound snapshot, while stdout remains `swarmcrate.verify.v2` and exit codes remain unchanged. Receipts are also saved for failed verification (exit 2). Full raw metainfo SHA-256, infohashes, the complete real-file map, required-file observed sizes, and the same report are captured together; `PASS`, `PARTIAL_PASS` and `FAIL` stay distinct. The file map does not imply all files were checked, and a required v1 neighbour is only checked where it contributes to selected pieces.

The receipt factory copies size inputs before callbacks and serializes before returning mutable report arrays. The caller must still supply stable, honest storage. There is no signing, publisher authentication, trusted timestamp, cached-verification claim, or imported-receipt validation. Exact metainfo identity changes after a tracker-only edit even though infohashes remain unchanged. No local absolute paths, stat tokens, trackers, web seeds or comments are exported. See the [receipt schema and workflow](verification-receipts.md).

CLI report outputs are preflighted before payload reads: existing paths, normalized/case-folded duplicate destinations, missing parents and overlap with any listed payload path are rejected. This conservatively includes missing/unselected payload files and may reject distinct case-sensitive names. Required-file consistency checks complete before either output; detected source changes abort with exit 1 and no receipt or HTML. Receipt and HTML are written sequentially with exclusive creation, not as an atomic pair; a later I/O error can leave an earlier complete output. Sequential filesystem checks remain subject to the stability limitations above.

## Filesystem scan snapshots (JavaScript only)

Import `Novanke/swarmcrate/host` in a JS-targeted package. `scan_snapshot(path, max_entries=200000)` returns `ScanSnapshot { name, single_file, sources, directories }`. Read through `read_source(snapshot.sources, ...)`, then call `check_scan_unchanged(snapshot)` immediately before `write_new`. The CLI follows this sequence automatically. Preserve the returned tokens and name arrays; they describe a local consistency check, not a serialized or authenticated certificate.

`DirectorySnapshot` holds an absolute path, the initial `FileStat`, and sorted immediate child names. `check_scan_unchanged` rechecks all listed files and directories, including empty directories. The existing `scan(path)` tuple API remains available, but its tuple does not retain directory snapshots; use the new API when writing created metadata.

Scanning counts both files and directories against a shared descendant-entry budget, excluding the input root. The budget can be lowered to any value in 1..200000. Single-file input consumes one source slot. Existing limits of 65536 real files and 32 path components also apply. Directory enumeration is incremental and stops when the supplied remaining budget is exhausted; the OS handle closes on success and errors. Limit violations retain error code `limit`, while filesystem failures retain their I/O cause rather than being masked as JSON parsing failures. These filesystem policies are implemented in MoonBit; the Node bridge only enumerates and reads/writes OS objects.

From 0.11.1, file-close failures stay inside `CrateError("io", detail)` handling; a verification read-close failure marks that piece unreadable and later pieces continue. A descriptor is relinquished before close and is not retried after an ambiguous failure. Windows path and handle identity comparisons account for Node 22.16.0 device-field reporting without removing inode/size/nanosecond-time checks. See [runtime compatibility and fault reproduction](runtime-compatibility.md).

## Batch audit (0.12.0)

`parse_batch_manifest(Bytes) -> BatchManifest` accepts bounded strict UTF-8 JSON; `batch_manifest_tasks` exports copies. `audit_batch(manifest, load, observe, read, finish=None, detail_limit=128, max_summary_bytes=16777216) -> BatchAudit` freezes definitions, executes tasks sequentially and isolates checked task errors. `batch_audit_json_text` and `batch_audit_exit_code` expose the opaque immutable result. The JS CLI command is `audit-batch MANIFEST.json SUMMARY.json`; relative inputs use the manifest directory. See [complete callback, identity, quota, output and exit-code contracts](batch-audit.md). Existing single-task APIs and schemas are unchanged. `host.resolve_from(directory, input)` provides explicit-base OS path resolution; the batch adapter additionally rejects ambiguous drive-relative locators.

## Controlled batch audit (0.13.0)

`audit_batch_with_control(manifest, load, observe, read, on_progress, finish=None, detail_limit=128, max_summary_bytes=16777216)` adds global cooperative cancellation. `BatchProgress` carries immutable counters and optional scalar `VerificationProgress`; `batch_progress_json` emits telemetry schema `swarmcrate.batch-progress.v1`. False or observer errors return no BatchAudit, even after final serialization; storage errors remain task-local. JS `cli.execute_batch_with_control(args, callback)` adds a final before_write gate, source/manifest rechecks and cancellation code 3. See [complete checkpoint, error, I/O and publication contract](batch-control.md).
