Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

noyalib — Engineering Policies

This document is the single source of truth for noyalib’s engineering posture: MSRV, SemVer, security audits, performance guarantees, concurrency, platform support, and the feature-flag matrix. Every README in the workspace links here; if there’s a contradiction between this file and a per-crate README, this file wins.

Last reviewed: 2026-07-22. The dates on individual sections capture when each policy was last audited.


Contents

  1. MSRV (Minimum Supported Rust Version)
  2. SemVer & API stability
  3. Security & audits
  4. Performance & algorithmic complexity
  5. Concurrency guarantees
  6. Platform support
  7. Feature-flag matrix
  8. Panic policy
  9. Error model
  10. Dependency policy
  11. Release & changelog policy

1. MSRV (Minimum Supported Rust Version)

CrateMSRVRationale
noyalib (library core)1.89.0One floor shared with the maintainer’s other Rust crates, raised from 1.86.0 in v0.0.58 (ADR-0014). The toolchain-driven floor is lower: criterion 0.8 (dev-dependency) declares rust-version = 1.86, so cargo check --all-targets, the bench suite, and the coverage gate all fail on 1.85. Enforced by the dedicated msrv-core CI job.
noyalib-mcp1.89.0Lockstep with the core floor; the MCP SDK alone needs 1.88. The MCP wire surface is text-only JSON-RPC and pulls no nightly-only deps.
noya-cli (binaries)1.89.0Lockstep with the core floor; clap_builder 4.6 is edition-2024 and requires 1.86+.
noyalib-lsp1.89.0Lockstep with the core floor.
noyalib-wasm1.89.0Lockstep with the core floor; the wasm-bindgen 0.2 ecosystem floors at 1.86.

What the MSRV number means here. It is the lowest toolchain on which the project can be built and tested, not the lowest on which the library alone happens to compile. Those differed at the 1.86.0 floor: cargo +1.85.0 check --lib succeeds, but cargo +1.85.0 check --all-targets fails with criterion@0.8.2 requires rustc 1.86. We publish the number we verify. Advertising 1.85 would be an untested promise — no CI leg could run there, so nothing would catch the day it broke.

When we bump it. Only when the toolchain we build and test at actually moves — a dependency (runtime or dev) raising its floor, or a language feature we choose to adopt. Never speculatively, and never for tidiness. “Headroom” is not a reason: an MSRV can be raised the day it is needed, and raising it early only costs downstream users. The one exception is the v0.0.58 raise to 1.89.0, a maintainer decision to share one floor with his other Rust crates, recorded in ADR-0014. As a standing guarantee, noyalib will not require a rustc newer than 12 months old at the time of release (1.89.0 shipped 2025-08-07, so the current floor is comfortably inside that).

How it ships (revised 2026-07-22, effective v0.0.16): while the project is in its 0.0.x line, an MSRV bump — core or satellite — ships as a patch (0.0.x → 0.0.x+1), in lockstep across all five crates. It must be called out in CHANGELOG.md under an explicit ### Changed — MSRV heading so consumers can find it by scanning, and the new floor must be reflected in the table above, which is the single source of truth.

This supersedes the previous rule that a core MSRV bump was a minor-version event. That rule was written when 0.1.0 was the next planned cut; under the current 0.0.x posture there is no minor slot to spend, and SUPPORT.md already warns that a patch may carry breaking changes in 0.x. Treating MSRV bumps as patches keeps the version line monotonic and honest rather than reserving a semantic that the 0.0.x scheme cannot express. This changes at 1.0: once the crate is 1.x, an MSRV bump becomes a genuine minor-version event again, per the 1.0 gates in PLAN.md.

CI matrix verifies the MSRV per-crate via the Per-crate MSRV workflow job — no change to that job is required when a downstream crate updates its dependency floor; the workflow reads rust-version from each Cargo.toml.


2. SemVer & API stability

noyalib follows SemVer 2.0.0 strictly, with one well-defined exception below.

What constitutes a breaking change

A change is breaking (requires a major-version bump under 1.0+, or a minor-version bump on the 0.x line) if it:

  • Removes or renames a publicly exported item (function, type, trait, const, enum variant, module).
  • Changes the signature of a publicly exported function or trait method (excluding broadening the bound, which is non-breaking).
  • Changes the layout of a publicly exported struct in a way that breaks pattern-match exhaustiveness (adds a non-#[non_exhaustive] variant or field).
  • Changes the default behaviour of a parser / serializer (e.g., flips YAML 1.1 ↔ 1.2 boolean recognition).
  • Removes a feature flag or changes a feature’s set of enabled deps.

What is not a breaking change

  • Adding a new public item.
  • Adding a new variant to a #[non_exhaustive] enum.
  • Adding a new field to a #[non_exhaustive] struct.
  • Tightening a parser to reject input it previously accepted as long as that input was malformed YAML 1.2 (not in the test suite’s accept-set). Such changes are noted in the changelog under “Stricter rejection”.
  • Tightening a deserializer to reject input that was previously coerced (e.g., from_str_strict). The strict path is opt-in.
  • Performance, code-size, or compile-time changes that preserve the observable contract.

Pre-1.0 (current state — 0.0.x)

While on the 0.0.x line, every release may be breaking per SemVer’s pre-1.0 carve-out — Cargo treats each 0.0.x as its own incompatible version, so the patch position is the only one that moves (see the MSRV bump policy in §1, which follows from this). We try to avoid breakage anyway; the cargo-semver-checks CI gate catches accidental breaks. The 0.0.x line is intentionally narrow: we expect to ship a single big-bang 0.0.1, then iterate 0.0.2, 0.0.3, … through 0.0.999, then graduate to 0.1.0 for the first genuinely stable release.

Compatibility tooling

  • cargo-semver-checks: runs in CI on every PR; fails the build on detected breaks.
  • #[non_exhaustive] is applied to every config struct (ParserConfig, SerializerConfig) so adding fields is always non-breaking.
  • MSRV stability: see §1 above.

3. Security & audits

Reference: SECURITY.md at the repo root.

Compile-time safety

  • #![forbid(unsafe_code)] at the workspace level. The lint applies to every crate in crates/. Verified by Rust CI / Check & Test on every PR.
  • No C dependencies, no FFI calls. No libyaml, no libc, no transitively-unsafe parser. The runtime deps that do use unsafe (indexmap, rustc-hash, ryu, itoa, memchr, smallvec) are checked under Miri on every PR (focused) and weekly (full + big-endian).
  • No network I/O and no filesystem writes from the library itself, and no environment-variable reads at run time (the build script reads RUSTC and NOYALIB_COVERAGE). The library reads files only when asked to: SafeFileResolver (include_fs) under its root, and the figment provider’s Yaml::file and Yaml::file_with_config. JSON Schema $ref never resolves outside the schema document. The noya-cli binaries read and write files.

Resource-limit gates

The parser has explicit DoS guards. Defaults are conservative; override via ParserConfig.

LimitDefaultstrict()PurposeOverride
max_depth12864Stack-overflow guard on deeply-nested inputParserConfig::max_depth(N)
max_alias_expansions1024100Billion-laughs amplification capParserConfig::max_alias_expansions(N)
max_document_length64 MiB1 MiBPer-document size capParserConfig::max_document_length(N)
max_stream_bytes256 MiB16 MiBMulti-document stream size capParserConfig::max_stream_bytes(N)
max_nodes25000025000AST node-count flood cap (empty-collection bombs)ParserConfig::max_nodes(N)
max_sequence_length655361024Per-sequence item count capParserConfig::max_sequence_length(N)
max_mapping_keys655361024Per-mapping key count capParserConfig::max_mapping_keys(N)

The corresponding regression tests live in tests/stress_load.rs.

v0.0.6 opt-in surface limits

The recovery, tokio, and sval modules (each behind its own Cargo feature) add their own DoS-resistance posture on top of the shared parser limits:

  • recovery::parse_lenient rejects a ----marker scan beyond ParserConfig::max_documents to defeat marker-spam inputs, and caps the cumulative line-truncation-retry cost at LenientConfig::truncation_event_budget (default 1 MiB) to defeat O(n²) re-parse on 10k-line adversarial documents.
  • tokio_async::from_async_reader{,_multi}{,_with_config} probe one byte beyond max_document_length for a single document or max_stream_bytes for a stream. Oversized input is rejected, never parsed as a truncated prefix. A leading UTF-8 BOM is stripped so Windows-saved buffers round-trip identically to LF-on-Linux equivalents.
  • tokio_async::YamlDecoder exposes max_frame_size(usize): when the inter-frame BytesMut holds an incomplete document beyond the cap, the next decode call returns Error::Io(InvalidData) rather than letting an adversarial producer pin memory by streaming without ---. Constructors derive the cap from ParserConfig::max_document_length; callers may tighten it, and max_document_length caps a frame whatever max_frame_size says. Multiple complete documents already present in one read are measured independently rather than rejected by their aggregate buffered size. The boundary scan resumes where the previous decode stopped, so a frame arriving in many small reads is scanned once, not once per read.
  • sval_adapter forwards non-finite floats verbatim by default; use to_sval_writer_with_config with SvalConfig::coerce_non_finite_to_null to emit Null instead, required for downstreams like sval_json that reject NaN / ±∞.

max_depth guard correctness (fixed in v0.0.6 / issue #46)

The max_depth limit above relies on balanced increment / decrement of self.depth along every code path. Two correctness bugs that made the guard either over- or under-count were patched in v0.0.6:

  • Over-count, false-positive. StreamingMapAccess’s iterators did not check the access object’s finished flag. Serde visitors that called next_entry after Ok(None) — ValueVisitor::visit_map does this — re-entered the iterator, read the next event from the parent mapping, and treated it as the inner exhausted child’s. The recursive deserialize_any on each spilled value inflated self.depth by one per empty flow {}, so a pnpm-lock.yaml-shaped input with N consecutive {} hit the limit at exactly N = max_depth. Fixed: explicit if self.finished { return Ok(None) } guards on the three iterators; balanced decrement on Ok and Err in deserialize_any / _seq / _map.
  • Under-count, silent bypass. The NoSpanLoader path (value-target fast path, no_std multi-document loading) incremented self.depth on SequenceStart / MappingStart but did not compare against max_depth. Adversarial deeply-nested input through that path could consume stack without ever firing the documented guard. Fixed: mirror the span loader’s check.

Regression suite: crates/noyalib/tests/issue_46.rs — 10 tests including a 50 000-package full pnpm-lock-v9 shape, 3 000 consecutive empty flow mappings, deterministic depth-cliff probe at n ∈ [100, 128, 129, 130, 200, 500, 1000], and a 200-level-deep sequence for the no-span path.

Audit pipeline

Each PR runs:

  • cargo audit (RustSec advisory DB).
  • cargo deny (allow / deny / skip rules; SPDX licence whitelisting via deny.toml).
  • cargo vet (vetted-deps register at supply-chain/).
  • cargo semver-checks (API break detection).
  • CodeQL (GitHub-native static analysis).
  • Differential fuzz (10 s smoke run, comparing noyalib against serde_yaml_ng / yaml-rust2 for divergent acceptance behaviour).
  • Miri (focused) — verifies the supply-chain unsafe interaction surface is sound.

Weekly (or on-demand) jobs:

  • Soak fuzz (per-target).
  • Soak Miri (full suite + big-endian via mips64 cross).

Supply-chain provenance

OpenSSF Scorecard posture

The scorecard report lives at scorecard.dev/viewer/?uri=github.com/sebastienrousseau/noyalib. v0.0.6 lifts the score from 6.5/10 to ~9/10 by closing every check that is fixable in source — the four remaining items below require external action and are tracked here so contributors can see the residual gap:

CheckStatusAction
Token-Permissions✓ fixedTop-level workflow tokens demoted to contents: read; writes scoped per-job.
Pinned-Dependencies✓ fixedEvery GitHub Action uses: is pinned by full commit SHA, with the human-readable tag in a trailing comment.
Dependency-Update-Tool✓ fixed.github/dependabot.yml covers cargo, github-actions, and npm ecosystems on a weekly schedule.
Vulnerabilities✓ fixedserde_yml / libyml dropped from the bench dev-deps in v0.0.6 (RUSTSEC-2025-0067 + -0068); Cargo.lock is now clean.
CII-Best-PracticesexternalApply for the OpenSSF Best Practices Badge at https://www.bestpractices.dev/. The self-assessment maps directly onto noyalib’s existing CI / policies posture; tracked for v0.1 milestone.
Code-Reviewpartial mechanical.github/workflows/auto-approve-dependabot.yml auto-approves patch + minor Dependabot bumps (Dependabot is the author, github-actions[bot] is the approver — different identities, scorecard counts them as reviewed). Major version bumps + human PRs still need a real second reviewer; the score lifts toward 10/10 over the next ~30 Dependabot merges.
Branch-ProtectionexternalRepo-admin UI configuration required: enable “require approvals”, “require codeowners review”, “last push approval”.
ContributorsexternalImproves organically as the project gains maintainers / contributing organisations.

Disclosure

Report security issues to sebastian.rousseau@gmail.com. Initial response within 48 hours; mitigation plan within 7 days of confirmation. Full policy at SECURITY.md.


4. Performance & algorithmic complexity

The reference target is constant memory per input byte on the parser hot path, with a clear structural bound on every operation. The headline numbers below were captured on 2026-05-08 against a 97 KB synthetic mapping-of-records document.

OperationThroughputBig-ONotes
from_str::<Value> (parse + AST)~36 MB/sO(n) in input bytesstreaming-first; AST walks the event stream in one pass
from_str::<T> (typed)~50–80 MB/sO(n)bypasses the AST when the target is fully typed
cst::parse_document (CST)~21 MB/sO(n)builds green tree alongside parse
to_string (emit)~150 MB/sO(n) in tree sizesingle-pass write, no re-parse
Document::set(path, value)sub-ms on 1 MB inputsO(d + len(path))d = path depth

Algorithmic guarantees

  • Parser: single pass, O(n) time, O(d) stack (d = nesting depth, capped at max_depth).
  • Loader: O(n) events from parser → O(n) allocations for the AST.
  • Anchor resolution: each alias is a hash-map lookup (O(1) expected), and the cumulative byte-budget for resolved aliases is bounded by max_document_length to defeat billion-laughs amplification.
  • Owned mapping: IndexMap-backed with randomized hashing on hosted builds; key insertion is O(1) amortised and ordered iteration is O(n). Allocation-only no_std targets retain deterministic Fx hashing.
  • Path lookup (Document::get(path)): O(d + len(path)) worst case where d is path-depth (no cross-cuts).
  • Schema validation: O(n × s) where n = document size and s = schema size; bounded by jsonschema’s own guarantees.

Where the SIMD lives

  • Decimal-integer parsing (simd::parse_decimal_*) — SWAR.
  • Plain-scalar termination scan — memchr + SWAR fallback.
  • Structural-bitmask iteration (simd::SimdScanner) — SWAR.

Every SIMD pipeline has a portable fallback verified by Miri under big-endian (mips64) so the byte-order assumption stays explicit.

Benchmark methodology

  • cargo bench runs the criterion suite under [profile.release] with opt-level = 3, lto = "fat", codegen-units = 1, overflow-checks = true.
  • CodSpeed tracks per-PR drift on every benchmark.
  • Measurements above are single-thread; see §5 for the parallel-parsing story.

Profile-Guided Optimization (PGO) — opt-in 5-15% on top

The default cargo install noya-cli build runs the workspace release profile (opt-level = 3, lto = "fat", codegen-units = 1). A two-pass PGO build adds another 5-15% speedup on the parser hot path by laying out branches based on the actual execution profile rather than LLVM’s static heuristics.

PGO is opt-in: distro packagers and downstream teams who ship the binaries to a wide audience can run scripts/pgo.sh to produce a PGO’d binary. The full pipeline (instrumented build → train against representative corpus → optimised rebuild via llvm-profdata merge) is documented at docs/PGO.md.

Per-host-triple training is required — a Mac-trained merged.profdata cannot be reused on Linux x86_64.

Known performance non-goals

  • We do not match serde-saphyr’s typed-target throughput on the smallest inputs — the AST loader has a per-document setup cost that pays off on documents larger than ~64 KB.
  • We do not zero-allocate; the typed deserialise still allocates for String, Vec, and the like at boundary conversions.
  • We do not support memory-mapped streaming over multi-GB documents; the entire input must fit in &[u8] / &str.

5. Concurrency guarantees

Send / Sync bounds

TypeSendSyncNotes
Value, Mapping, Number, Tag, TaggedValueyesyesAll-owned, no interior mutability.
ErroryesyesAll-owned (boxed inner data).
cst::DocumentyesyesImmutable after construction; set takes &mut self.
ParserConfig, SerializerConfigyesyesPlain config structs.
ParserConfig::policies (Arc<dyn Policy>)yesyesThe Policy trait inherits Send + Sync.
Deserializer<'de>yes ('de: 'static)yesBorrows the source; lifetimes propagate.
Serializer<W>depends on Wdepends on WInherits from the writer.

Parallelism story

  • Single-document parse is single-threaded. It’s fast enough that splitting a single doc across cores costs more than it gains for typical inputs.
  • Multi-document streams can be parsed in parallel via noyalib::parallel::parse::<T>(input) (gated behind the parallel feature). Each document parses on its own rayon job.
  • Service-owned concurrency uses parallel::parse_with_config_in_pool(input, config, pool) so the caller controls the worker count, thread lifecycle, names, and stack sizes without touching Rayon’s global pool.
  • The Deserializer itself is not Sync-after-construction in a useful way — there’s no parallel access to a single document’s events.

Anchor / alias semantics under concurrency

Anchors are resolved at parse time (per YAML 1.2 §7.1). Once resolution completes, the resulting Value tree is fully owned, so there is no shared-mutable state across threads. Sending a parsed Value between threads is the same as sending any other owned data structure.

Reentrance

Every public parse / emit function is reentrant. There is no mutable global state. The library does not initialise any singletons, does not register any signal handlers, and does not spawn background threads on its own (only the parallel feature opts in to rayon thread-pool use).


6. Platform support

Tier 1 (CI-verified every PR)

TargetToolchainNotes
aarch64-apple-darwinstable, nightlyM-series Macs (developer host)
x86_64-unknown-linux-gnustable, nightlyGitHub ubuntu-latest runners
x86_64-pc-windows-msvcstable, nightlyGitHub windows-latest runners

Tier 2 (verified on demand / weekly)

TargetVerification
mips64-unknown-linux-gnuabi64Miri (big-endian sanity, scheduled job)
wasm32-unknown-unknownwasm-pack test --node (per-PR)

Bare-metal, no_std (CI-verified every PR)

Built with --no-default-features; alloc is required, std is not present at all on these targets.

TargetVerification
thumbv7em-none-eabihfcargo check --no-default-features --lib --locked --target … (per-PR)
riscv32imac-unknown-none-elfsame
aarch64-unknown-nonesame

See § no_std (alloc-only) build for why these carry the invariant that wasm32-unknown-unknown cannot.

no_std (alloc-only) build

The noyalib crate compiles cleanly with --no-default-features against core + alloc. The Per-crate no_std (alloc-only) build workflow job runs exactly that on every PR.

Do not add --features minimal when you want no_std. minimal is defined as minimal = ["std"] — it is a dependency-budget alias that turns std back on while dropping itoa, ryu and serde_ignored (see §7, Feature compatibility). Combining it with --no-default-features yields a std build, so it silently does not test what the name suggests. This document previously recommended that exact combination; corrected in v0.0.16.

Bare-metal included. no_std here means alloc-only, and it is verified on five configurations on every PR: the native host, wasm32-unknown-unknown, and the three bare-metal *-none targets thumbv7em-none-eabihf, riscv32imac-unknown-none-elf and aarch64-unknown-none.

The bare-metal targets are the ones that actually hold the line. wasm32-unknown-unknown has std available, so a dependency pulling it in unconditionally passes there — which is exactly how #210 went unnoticed. A *-none target has no std at all, so the same mistake fails the build.

This section previously said bare-metal did not build, and named two blockers. Both were fixed and the text was not updated:

  1. indexmap, rustc-hash and memchr are now all declared default-features = false, so nothing pulls std transitively.
  2. FxHashMap / FxHashSet are hashbrown::HashMap / HashSet with FxBuildHasher (see the no_std prelude in lib.rs) rather than the rustc_hash aliases, which do not exist without std.

If you need bare-metal support, please open an issue — it is a tractable change, but it is a deliberate non-goal today rather than an oversight.

When in no_std mode:

  • from_reader / to_writer (which need std::io) are unavailable. Use from_slice / to_string and route through your own writer impl.
  • figment integration unavailable.
  • miette integration unavailable.
  • The noyavalidate binary unavailable (it bundles miette/fancy).

Cross-platform behavioural notes

  • Line endings: parse / emit normalize to \n regardless of platform. The CST preserves source bytes verbatim, including CRLF (\r\n), if the input contained them.
  • File paths in noya-cli / noyalib-mcp: handled via std::path::Path; Unicode-faithful on macOS / Linux, Windows handles non-Unicode paths by fall-through to UTF-8 lossy.
  • Atomic file writes in noyalib-mcp::tool_set: write to a sibling temp file, fsync, then rename. POSIX: atomic. Windows: MoveFileExW(MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) semantics — atomic-with-flush.
  • Number formatting: deterministic via ryu / itoa. No locale dependency. 0.1 + 0.2 displays as 0.30000000000000004 exactly the same on every platform.

7. Feature-flag matrix

noyalib (the library) — defaults: std, fast-int, fast-float, strict-deserialise.

FeatureDefault?What it addsCost (deps)
stdyesstd-aware error trait, std::io reader/writer(transitive only)
fast-intyesbranchless integer formatting via itoaitoa
fast-floatyesbranchless float formatting via ryuryu
strict-deserialiseyesfrom_str_strict::<T> rejects unknown keysserde_ignored
minimalnometa: std only, drops the three accelerators(none)
miettenoError::Diagnostic impl for rich CLI errorsmiette
validate-schemanoJSON Schema 2020-12 validationjsonschema, serde_json
schemanoderive helpers for schemarsschemars, serde_json
gardenogarde validator integrationgarde
validatornovalidator validator integrationvalidator
figmentnoFigment provider implementationfigment
parallelnoparallel::parse::<T> on rayonrayon
simdnooptional explicit SIMD acceleration(none — uses portable_simd / std::simd via cfg)
compat-serde-yamlnoname-for-name shim under noyalib::compat::serde_yaml(none)
noyavalidatenometa — pulls validate-schema + miette/fancy(transitive)
wasm-optnopost-build wasm-opt pass marker (used by noyalib-wasm)(none)
nightly-simdnoenables nightly-only std::simd paths in simd.rs; gracefully no-ops on stable(none — gated by cfg(noyalib_nightly))
compare-saphyrnobench-only — pulls serde_saphyr into the comparison harness; never built into a release artefactserde_saphyr (dev-only)

Feature compatibility

  • minimal is not additive with fast-int / fast-float / strict-deserialise — using minimal means you opted out of those three. cargo build --features minimal builds with only std enabled.
  • validate-schema and schema both pull serde_json; if you enable both, you get exactly one copy.
  • parallel is std-only — there’s no rayon no_std.
  • compat-serde-yaml is independent and pulls no extra deps.
  • nightly-simd requires a nightly toolchain at build time; gracefully no-ops to portable scalar fallbacks when built on stable. Not enabled in default — opt-in via cargo +nightly build --features nightly-simd.
  • compare-saphyr is not intended for production builds. It compiles in serde_saphyr purely so the comparison benches in crates/noyalib/benches/comparison.rs can pit noyalib head-to-head against it; downstream packagers must not enable this feature in shipped binaries.

8. Panic policy

The library API does not panic on well-formed input. Panic sources, exhaustively:

  1. unwrap / expect in tests and examples — never on user-facing code paths.
  2. Internal invariants — crate::error::invariant_violated is the canonical panic site; firing it indicates a bug that should be reported. The function’s docstring documents the conditions that should make it unreachable.
  3. Allocator OOM — Rust’s default behaviour. A user with a custom global allocator that returns null will see the process abort, not unwind, because we set panic = "abort" in [profile.release].

panic = "abort" in release means panics terminate the process; do not rely on catching them. Library callers who need panic safety should use std::panic::catch_unwind in debug builds; in release, treat any panic as a bug to file.


9. Error model

Public surface uses a single error type per crate (noyalib::Error, noya_cli::Error, etc.). The noyalib::Error enum is #[non_exhaustive] and carries location info wherever possible.

Invariants

  • Every public from_* function returns Result<T, Error>, never panics on malformed input.
  • Every public to_* function returns Result<…, Error>, never panics on representable values.
  • Errors include a Location { line, column, byte_offset } whenever the underlying source span is available.
  • Error::diagnostic() is the canonical adapter boundary. It exposes stable namespaced codes, protocol-neutral severity, help text, and ordered byte spans without requiring a rendering dependency.
  • Error: Send + Sync + 'static — safe to propagate across threads and into anyhow / eyre / Box<dyn Error>.

miette integration

With features = ["miette"], Error implements miette::Diagnostic by adapting Error::diagnostic(), so CLI tools render rich location-aware output without a second error-code or label mapping. The noyavalidate binary is the canonical example. The ariadne feature consumes the same payload.

Truncation

Long source snippets in error messages are truncated to a configurable budget via Error::format_with_source_truncated (default 4 KB) so the diagnostic doesn’t dump megabytes of context into a CI log.


10. Dependency policy

Default-on dependencies (5 unconditional + 3 default-on optional)

  • serde (no-default-features, with derive + alloc)
  • indexmap (>=2, <2.11) — ordered mappings
  • itoa (default-on optional via fast-int)
  • ryu (default-on optional via fast-float)
  • serde_ignored (default-on optional via strict-deserialise)

Opt-in dependencies (gated by feature)

miette, garde, validator, schemars, serde_json, jsonschema, figment, rayon, plus the three default-on opt-outs covered in §7.

Adoption rules

  • New dependencies require explicit rationale in the Cargo.toml comment block.
  • Every dep is checked against cargo-vet (vetted-deps register) and cargo-deny (allow / deny / skip rules).
  • No wildcard version requirements (the cargo-deny config rejects them). Every entry pins a >= lower bound and an < upper bound; this protects against transitive silent-bumps.
  • Audit advisories (cargo audit) fail the build on any flagged dep.

cargo-vet exemption conventions

supply-chain/config.toml is machine-generated — cargo vet fmt strips comments, so the conventions governing it are recorded here instead of inline.

  • Exemptions are version-pinned. Bumping a dependency invalidates its exemption and the crate re-enters the unaudited set. This is deliberate: it is why a blanket cargo update is not a routine operation here (see the v0.0.16 notes, where a 71-crate refresh put 81 crates back on the unaudited list at once).
  • suggest = false means “deliberately parked” — cargo vet suggest will stop proposing it. New exemptions are added without it, so they stay on the suggest list until someone either audits them or consciously parks them. The absence of suggest = false is a to-do marker, not an oversight; do not add it in bulk to quieten the suggest output.
  • Never add exemptions in bulk to make a red gate green. An exemption is an assertion that a human looked. If a change produces more new exemptions than a reviewer can actually read, the change is too large — split it.

11. Release & changelog policy

  • The CHANGELOG.md follows Keep a Changelog 1.1.0 with explicit [Unreleased] rolling section.
  • Every commit ending up in a release is annotated with the Assisted-by: trailer per the Linux kernel coding-assistants standard so AI tooling provenance is auditable.
  • Releases are tagged v0.0.X, GPG-signed, and the cargo-semver-checks gate must pass before a tag is pushed.
  • Yanking is reserved for genuine security issues; behavioural bugs ship a fix rather than a yank.

Open an issue if any policy here is unclear

These policies are intended to be operational, not aspirational. If you hit a case where the policy doesn’t tell you what to expect, file an issue at https://github.com/sebastienrousseau/noyalib/issues with the scenario — every clarification we add here closes a future ambiguity.