Core error taxonomy + name validation (ADR-008 §4/§5, ADR-017 §3); AGENTS.md updated to Phase 1 posture
This commit is contained in:
1 parent
34e0b9732d
commit
74105a16ae
8 files changed
+413
-84
No files matched your search
@@ -7,27 +7,30 @@ unless their own prompts say otherwise.
|
||||
|
||||
## Repo posture
|
||||
|
||||
This repo is in **Phase 0 (Exploration)** per `docs/sdd_process.md`.
|
||||
There is no crate yet — no `Cargo.toml`, no `src/`, no README (the README
|
||||
gets written just before first publish, so it can be honest about what
|
||||
shipped). What exists is `docs/research/phase-0.md` (the Phase 0
|
||||
document: vision, prior art, open questions, POC register), the SDD
|
||||
process (`docs/sdd_process.md`), and the agent defs
|
||||
(`.opencode/agents/`). Do not scaffold implementation code
|
||||
from this posture unless a task or the user asks for a POC.
|
||||
This repo is in **Phase 1 (Implementation)** per `docs/sdd_process.md`.
|
||||
Phase 0 concluded with the converged recommendation and the accepted ADRs
|
||||
(`docs/architecture/decisions/`, `docs/architecture/README.md`); the
|
||||
implementation plan (`docs/plans/implementation.md`) decomposes waves of
|
||||
work into task files in `tasks/`. Three crates exist (ADR-001's Cargo
|
||||
workspace split): `alkstore` (core — the contract artifact),
|
||||
`alkstore-sqlite`, `alkstore-postgres`. Work proceeds task-by-task from
|
||||
`tasks/`; check a task's `status` and `depends_on` before picking it up.
|
||||
|
||||
Do not treat `docs/research/phase-0.md` as settled architecture — it is
|
||||
a working research document whose open questions are genuinely open.
|
||||
The hedging rules of `docs/sdd_process.md` (no circular deferrals, no
|
||||
"resolved with escape hatches") still apply to it.
|
||||
The architecture documents — `docs/architecture/` (core-contract.md, the
|
||||
ADRs, the engine specs) — are the normative spec for code work. Where
|
||||
code and ADR disagree, the ADR wins: fix the code, or (if the ADR is
|
||||
genuinely wrong for a reason no ADR anticipated) raise the discrepancy
|
||||
with the user before deviating. ADR amendments pre-implementation are
|
||||
class-1 events under ADR-017 §2 — but that class terminates at first
|
||||
release, so the window is now, not forever.
|
||||
|
||||
## Git Workflow
|
||||
|
||||
**Commit and push when reasonable.** When a change is complete and
|
||||
verified, commit and push to `origin/main` without asking. This
|
||||
overrides the built-in default of "only commit when explicitly asked."
|
||||
In Phase 0 "verified" means the doc checks below — there is no build
|
||||
gate yet.
|
||||
"Verified" means the Verification Commands below pass for any crate the
|
||||
change touches.
|
||||
|
||||
Exceptions — **do not** commit or push without asking:
|
||||
|
||||
@@ -44,87 +47,78 @@ failed one.
|
||||
Git identity is preconfigured (`glm-5.3-flash <glm-5.3-flash@alk.dev>`). Do
|
||||
not change `git config`, skip hooks, or use `git commit -i`.
|
||||
|
||||
## Phase 0 conventions
|
||||
## Code conventions
|
||||
|
||||
1. **Findings land in `docs/research/`.** Research notes, POC
|
||||
specifications, and POC findings all go there, named consistently
|
||||
(`poc-<name>-spec.md` / `poc-<name>-findings.md` follows the
|
||||
alkblobs precedent). A POC that needs this repo's code runs in a
|
||||
worktree (`.worktrees/research/<task-id>/` per the SDD process); a
|
||||
self-contained POC (like the alkblobs ones) runs as a standalone
|
||||
crate in the global workspace. Findings land in this repo either
|
||||
way.
|
||||
2. **Open questions get OQ-ST-NN IDs** and live in the register in
|
||||
`docs/research/phase-0.md` until Phase 1 promotes them to
|
||||
`docs/architecture/open-questions.md`. Numbering is stable — never
|
||||
renumber; append. POCs live in the register there too (numbered,
|
||||
spec'd under `docs/research/poc-<name>-spec.md`, findings in
|
||||
`poc-<name>-findings.md`).
|
||||
3. **Cite reference checkouts by path and revision.** The external
|
||||
references for this crate (`/workspace/honker` @ f4e53c6,
|
||||
`/workspace/pgboss-rs` @ 98f7d9e) are reference checkouts of
|
||||
third-party projects — read freely, but never wire the checkout
|
||||
itself into our code. Use the published version unless we vendor or
|
||||
fork it (alksocks' fast-socks5 precedent); if a fork is warranted,
|
||||
forking is normal work. Note the checkout state when your findings
|
||||
depend on code specifics; licenses and provenance get recorded when
|
||||
we adopt code, not while only reading.
|
||||
4. **POC code stays out of this repo** until Phase 1 adopts it. A POC
|
||||
crate proves the hypothesis and reports findings; it does not
|
||||
pre-scaffold the real crate layout.
|
||||
5. **No mock/main-repo documentation.** No README, no CHANGELOG, no
|
||||
crate-level docs until the shape exists. Docs that describe a thing
|
||||
that doesn't exist are exactly the kind of dishonest artifact we
|
||||
write docs to avoid.
|
||||
6. **No comments in code** (when POC crates get written) unless the
|
||||
user explicitly asks. Doc comments (`///`, `//!`) are fine on public
|
||||
API. Inline `//` comments only when a non-obvious safety/correctness
|
||||
constraint would otherwise be missed, or the user asks.
|
||||
|
||||
## Conventions that will apply once the crate exists
|
||||
|
||||
These are family-standard and pre-committed; they'll be duplicated into
|
||||
task prompts and tightened by Phase 1 ADRs:
|
||||
Family-standard and pre-committed; the ADRs carry the load-bearing
|
||||
decisions:
|
||||
|
||||
- Rust crate, `tokio` async runtime, `thiserror` error types, no panics
|
||||
in library code, no `unwrap()`/`expect()` outside tests.
|
||||
- No comments in code (see 6 above).
|
||||
- No comments in code unless the user explicitly asks. Doc comments
|
||||
(`///`, `//!`) are fine on public API — and required where a task pins
|
||||
semantics into doc text (core's docs are part of the contract).
|
||||
Inline `//` comments only when a non-obvious safety/correctness
|
||||
constraint would otherwise be missed, or the user asks.
|
||||
- Module-per-file under `src/`, re-exported from `src/lib.rs`; public
|
||||
API surface is the lib re-exports.
|
||||
- Optional dependencies (the postgres engine likely being the first)
|
||||
are feature-gated; the base crate compiles lean; both
|
||||
`cargo test --features <set>` and default-feature builds pass when
|
||||
features exist.
|
||||
- Categorical estimates (`scope`, `risk`, `impact`, `level`) in every
|
||||
task file — per `docs/sdd_process.md` these are structurally
|
||||
required, not optional metadata.
|
||||
- No CI wiring (manual CI per `docs/plans/implementation.md`).
|
||||
- `#[non_exhaustive]` policy is ADR-017 §3's: on consumer-*read* types
|
||||
(`Error`, `StreamEvent`, `Job`, `Schedule`, `Wake`), never on opts
|
||||
structs consumers construct.
|
||||
- Core (`alkstore`) has no driver dependencies, ever (ADR-001); payload
|
||||
encoding's `serde`/`serde_json` are the sole core deps (ADR-020 §4).
|
||||
- Engine crates depend on core with the versioned path pin (ADR-017
|
||||
§4.1) — never the reverse.
|
||||
|
||||
## Task files
|
||||
|
||||
Every task file in `tasks/` carries categorical estimates (`scope`,
|
||||
`risk`, `impact`, `level`) — per `docs/sdd_process.md` these are
|
||||
structurally required, not optional metadata. On completion: update the
|
||||
task's `status` to `completed`, and fill its **Notes** (decisions of
|
||||
record the implementation made that the description didn't pin) and
|
||||
**Summary** (what landed, verified how) sections — the next agent reads
|
||||
those instead of re-deriving them.
|
||||
|
||||
## Verification Commands
|
||||
|
||||
Phase 0 — none (no code yet). The doc conventions above are checked by
|
||||
reading. When the crate appears, this section gets the standard Rust
|
||||
gates (`cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check`, and the fuzz corpus replay if fuzzing is adopted —
|
||||
the alksocks/alktty/alktunnels pattern) and becomes the merge gate the
|
||||
coordinator runs.
|
||||
Standard Rust gates, run from the workspace root; they are the merge
|
||||
gate:
|
||||
|
||||
```
|
||||
cargo build # workspace compiles
|
||||
cargo test # workspace tests (task-scoped: cargo test -p <crate>)
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
cargo fmt --check
|
||||
```
|
||||
|
||||
No fuzzing is adopted (no fuzz corpus replay gate — the
|
||||
alksocks/alktty/alktunnels fuzz posture was not carried into this
|
||||
crate's plan); revisit only if a task or ADR introduces it.
|
||||
|
||||
## Architecture Context
|
||||
|
||||
- `docs/research/phase-0.md` — the Phase 0 document: vision, prior
|
||||
art, open questions (OQ-ST-01..NN), the POC register, and (eventually)
|
||||
the converged recommendation. Read it before any non-trivial work in
|
||||
this repo.
|
||||
- `docs/research/consumer-inventory.md` — the per-feature scope
|
||||
synthesis answering OQ-ST-01 from the paused consumers' documents.
|
||||
New consumer needs get a row there *before* any scope decision
|
||||
assumes them.
|
||||
- `docs/sdd_process.md` — the SDD process. Phase 0 in progress;
|
||||
`docs/architecture/` does not exist yet.
|
||||
- `docs/architecture/README.md` — the decision index; start here.
|
||||
- `docs/architecture/core-contract.md` — the contract of record for the
|
||||
core crate's surface (the traits, types, error taxonomy, naming /
|
||||
reserved namespace, delivery-guarantee table, verification backlog).
|
||||
- `docs/architecture/decisions/` — the accepted ADRs (ADR-001..021).
|
||||
- `docs/architecture/open-questions.md` — the open-question register;
|
||||
resolved OQs cite their ADRs. Numbering is stable — never renumber;
|
||||
append.
|
||||
- `docs/plans/implementation.md` — the wave-based implementation plan;
|
||||
`tasks/` decomposes its waves.
|
||||
- `docs/research/` — the Phase 0 record (phase-0.md, POC specs and
|
||||
findings, the honker/pgboss reference reads,
|
||||
consumer-inventory.md). Reading it is background, not obligation;
|
||||
the ADRs supersede it where they disagree. New consumer needs still
|
||||
get a `docs/research/consumer-inventory.md` row *before* any scope
|
||||
decision assumes them (the row-first gate, ADR-017 §2).
|
||||
- The SDD process and agent defs were copied from downstream projects
|
||||
(alkblobs) in the workspace; project-specific stale references were
|
||||
cleaned at Phase 0 setup (2026-10-03). If you find a section that
|
||||
assumes another crate's machinery, fix it rather than working around
|
||||
it — same discipline as stale TODOs.
|
||||
- If a TODO or doc reference cites a design direction that a later ADR
|
||||
or Phase 0 decision rejected, the reference is stale — remove it and
|
||||
align; do not implement the rejected design.
|
||||
or decision rejected, the reference is stale — remove it and align;
|
||||
do not implement the rejected design.
|
||||
Generated
+3
@@ -5,6 +5,9 @@ version = 4
|
||||
[[package]]
|
||||
name = "alkstore"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alkstore-postgres"
|
||||
|
||||
+4
-1
@@ -3,4 +3,7 @@ name = "alkstore"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = "2"
|
||||
@@ -0,0 +1,61 @@
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// The payload exceeds the engine's notification payload limit.
|
||||
///
|
||||
/// Universal variant: the Postgres engine produces it (the
|
||||
/// client-side check on the serde_json serialization of the payload
|
||||
/// `Value` — the same byte string ADR-020 §4 pins as the stored row
|
||||
/// bytes); the SQLite engine never produces it (no limit). The
|
||||
/// documented workaround for large payloads on Postgres is the
|
||||
/// outbox shape — the id in the notification, the payload in a
|
||||
/// table row.
|
||||
#[error("payload exceeds the engine's notification payload limit (limit: {limit} bytes)")]
|
||||
PayloadTooLarge { limit: usize },
|
||||
/// A name carrying the reserved `__alkstore_` prefix was supplied
|
||||
/// to a shared-namespace entry point.
|
||||
#[error(
|
||||
"reserved name `{name}`: names carrying the reserved `__alkstore_` prefix are engine-owned"
|
||||
)]
|
||||
ReservedName { name: String },
|
||||
/// A name failed entry-point validation.
|
||||
#[error("invalid name `{name}`")]
|
||||
InvalidName { name: String },
|
||||
/// An operation against a closed receiver/source.
|
||||
///
|
||||
/// The `try_recv`/`recv_timeout` form of wake close (never
|
||||
/// returned by `recv_timeout`'s timeout expiry, which yields
|
||||
/// `Ok(None)`); never a timeout signal.
|
||||
#[error("the receiver or source is closed")]
|
||||
Closed,
|
||||
/// Payload serialization or deserialization failure.
|
||||
///
|
||||
/// `payload_as` on stream events and job payloads; wakes carry no
|
||||
/// payload, so no wake path produces this variant.
|
||||
#[error("payload codec failure: {0}")]
|
||||
Codec(String),
|
||||
/// The opaque fallback: everything else.
|
||||
///
|
||||
/// Driver/connection/SQL errors, opaque to the contract; engine
|
||||
/// detail is preserved via the error source chain (`#[source]`). No
|
||||
/// engine-specific variants are ever minted for its contents.
|
||||
#[error("database error")]
|
||||
Database(#[source] Box<dyn std::error::Error + Send + Sync>),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Convenience constructor for the opaque [`Error::Database`]
|
||||
/// fallback.
|
||||
pub fn database<E>(err: E) -> Self
|
||||
where
|
||||
E: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||
{
|
||||
Error::Database(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// The crate's result type — every fallible operation in the contract
|
||||
/// surface returns this. `Ok` never carries a "null" sentinel: no-work
|
||||
/// claims and unlanded job/lock ops are values, not errors (ADR-008
|
||||
/// §5).
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -1 +1,18 @@
|
||||
//! alkstore — the unified trait surface, value types, error model, and
|
||||
//! contract documentation of the store contract (ADR-001, ADR-008).
|
||||
//!
|
||||
//! This crate is the contract artifact: no driver dependencies, ever.
|
||||
//! Engine crates (`alkstore-sqlite`, `alkstore-postgres`) depend on
|
||||
//! this crate and implement its traits.
|
||||
|
||||
mod error;
|
||||
mod validation;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
pub use validation::{
|
||||
NameKind, RESERVED_LISTENER_RECONNECTED, RESERVED_PREFIX, validate_local_name, validate_name,
|
||||
validate_shared_name,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,131 @@
|
||||
use crate::validation::{
|
||||
NameKind, RESERVED_LISTENER_RECONNECTED, RESERVED_PREFIX, validate_local_name, validate_name,
|
||||
validate_shared_name,
|
||||
};
|
||||
use crate::{Error, Result};
|
||||
|
||||
fn assert_invalidname(r: Result<()>, expected: &str) {
|
||||
match r {
|
||||
Err(Error::InvalidName { name }) => assert_eq!(name, expected),
|
||||
other => panic!("expected InvalidName({expected}), got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_reservedname(r: Result<()>, expected: &str) {
|
||||
match r {
|
||||
Err(Error::ReservedName { name }) => assert_eq!(name, expected),
|
||||
other => panic!("expected ReservedName({expected}), got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_name_accepts_legal_names() {
|
||||
for legal in [
|
||||
"orders",
|
||||
"a",
|
||||
"names with spaces",
|
||||
"__not_the_reserved_prefix",
|
||||
"_alkstore_x",
|
||||
"__alkstor_e_",
|
||||
"__ALKSTORE_lower",
|
||||
"uniße-名前",
|
||||
] {
|
||||
validate_shared_name(legal).unwrap_or_else(|e| panic!("legal {legal:?} rejected: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_name_rejects_empty() {
|
||||
assert_invalidname(validate_shared_name(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_name_rejects_whitespace_only() {
|
||||
for ws in [" ", "\t", "\n", " \t \n "] {
|
||||
assert_invalidname(validate_shared_name(ws), ws);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_name_rejects_reserved_prefix() {
|
||||
for reserved in [
|
||||
"__alkstore_",
|
||||
"__alkstore_outbox:foo",
|
||||
"__alkstore_listener_reconnected__",
|
||||
] {
|
||||
assert_reservedname(validate_shared_name(reserved), reserved);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_name_accepts_legal_names() {
|
||||
for legal in ["worker-1", "__alkstore_my_local_consumer"] {
|
||||
validate_local_name(legal).unwrap_or_else(|e| panic!("legal {legal:?} rejected: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_name_rejects_empty_and_whitespace_only() {
|
||||
assert_invalidname(validate_local_name(""), "");
|
||||
for ws in [" ", "\t\n "] {
|
||||
assert_invalidname(validate_local_name(ws), ws);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_kind_dispatch_via_validate_name() {
|
||||
assert!(validate_name(NameKind::Shared, "__alkstore_x").is_err());
|
||||
assert!(validate_name(NameKind::Local, "__alkstore_x").is_ok());
|
||||
assert!(validate_name(NameKind::Shared, "ok").is_ok());
|
||||
assert!(validate_name(NameKind::Local, "ok").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_display_texts() {
|
||||
let e = Error::PayloadTooLarge { limit: 8000 };
|
||||
assert!(e.to_string().contains("8000"));
|
||||
assert!(
|
||||
Error::ReservedName { name: "x".into() }
|
||||
.to_string()
|
||||
.contains("x")
|
||||
);
|
||||
assert!(
|
||||
Error::InvalidName { name: "y".into() }
|
||||
.to_string()
|
||||
.contains("y")
|
||||
);
|
||||
assert_eq!(
|
||||
Error::Closed.to_string(),
|
||||
"the receiver or source is closed"
|
||||
);
|
||||
assert!(
|
||||
Error::Codec("bad utf8 at 3".into())
|
||||
.to_string()
|
||||
.contains("bad utf8")
|
||||
);
|
||||
let db = std::io::Error::other("boom");
|
||||
let e = Error::Database(Box::new(db));
|
||||
assert!(e.to_string().contains("database"));
|
||||
let src = std::error::Error::source(&e).expect("Database carries #[source]");
|
||||
assert_eq!(src.to_string(), "boom");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_database_constructor() {
|
||||
let e = Error::database(std::io::Error::other("disk gone"));
|
||||
match e {
|
||||
Error::Database(source) => {
|
||||
assert_eq!(source.to_string(), "disk gone");
|
||||
}
|
||||
other => panic!("expected Database, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_constants_exact() {
|
||||
assert_eq!(RESERVED_PREFIX, "__alkstore_");
|
||||
assert_eq!(
|
||||
RESERVED_LISTENER_RECONNECTED,
|
||||
"__alkstore_listener_reconnected__"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//! Entry-point name validation (ADR-008 §4) and the reserved
|
||||
//! namespace.
|
||||
//!
|
||||
//! Every name-bearing entry point validates before any engine round
|
||||
//! trip — on the auto-commit and `*_tx` paths alike — via these
|
||||
//! helpers.
|
||||
|
||||
/// The reserved namespace prefix (ADR-008 §4). Engine-derived names in
|
||||
/// consumer namespaces (e.g. an outbox's backing queue) carry it;
|
||||
/// consumer-supplied shared-namespace names carrying it are rejected
|
||||
/// with [`Error::ReservedName`](crate::Error::ReservedName).
|
||||
pub const RESERVED_PREFIX: &str = "__alkstore_";
|
||||
|
||||
/// The one v1-reserved string (ADR-008 §4): the Postgres engine's
|
||||
/// synthetic reconnect-wake channel, delivered as
|
||||
/// `Wake { channel: RESERVED_LISTENER_RECONNECTED }` on every
|
||||
/// subscriber's receiver after a watcher reconnect. Public in core so
|
||||
/// the pg engine and the contract suite reference the same constant.
|
||||
pub const RESERVED_LISTENER_RECONNECTED: &str = "__alkstore_listener_reconnected__";
|
||||
|
||||
/// Which namespace class a name belongs to (ADR-008 §4).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NameKind {
|
||||
/// Shared-namespace kinds — channels, streams, queues, outboxes,
|
||||
/// locks, schedule names. Non-empty, and the reserved prefix is
|
||||
/// rejected.
|
||||
Shared,
|
||||
/// Consumer-local identifiers — stream-consumer names, `owner` on
|
||||
/// `try_lock`, `worker_id` on claims. Non-empty only: a leading
|
||||
/// `__alkstore_` is legal (they are not a shared engine namespace,
|
||||
/// so there is nothing to collide with).
|
||||
Local,
|
||||
}
|
||||
|
||||
/// Validate a name per its [`NameKind`].
|
||||
///
|
||||
/// - non-empty (after no trimming — whitespace-only counts as
|
||||
/// non-empty content but is nonetheless rejected below) →
|
||||
/// [`Error::InvalidName`] when empty
|
||||
/// - `NameKind::Shared` additionally rejects names carrying
|
||||
/// [`RESERVED_PREFIX`] with [`Error::ReservedName`]
|
||||
///
|
||||
/// Whitespace-only names are rejected as [`Error::InvalidName`] for
|
||||
/// both kinds: they are never a meaningful name on any engine.
|
||||
pub fn validate_name(kind: NameKind, name: &str) -> crate::Result<()> {
|
||||
if name.is_empty() || name.trim().is_empty() {
|
||||
return Err(crate::Error::InvalidName {
|
||||
name: name.to_string(),
|
||||
});
|
||||
}
|
||||
if kind == NameKind::Shared && name.starts_with(RESERVED_PREFIX) {
|
||||
return Err(crate::Error::ReservedName {
|
||||
name: name.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate a shared-namespace name (channels, streams, queues,
|
||||
/// outboxes, locks, schedule names).
|
||||
pub fn validate_shared_name(name: &str) -> crate::Result<()> {
|
||||
validate_name(NameKind::Shared, name)
|
||||
}
|
||||
|
||||
/// Validate a consumer-local identifier (stream-consumer names, `owner`
|
||||
/// on `try_lock`, `worker_id` on claims): non-empty only.
|
||||
pub fn validate_local_name(name: &str) -> crate::Result<()> {
|
||||
validate_name(NameKind::Local, name)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: core-errors-and-validation
|
||||
name: Core error taxonomy and name validation
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [scaffold-workspace]
|
||||
scope: narrow
|
||||
risk: low
|
||||
@@ -65,8 +65,59 @@ engine and the contract suite reference the same constant.
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
- `thiserror` v2 added to `alkstore` — the crate's first (and, until
|
||||
wave 2's serde work, only) dependency.
|
||||
- `Error::Database` carries
|
||||
`#[source] Box<dyn std::error::Error + Send + Sync>` — the opaque
|
||||
engine-detail carrier per ADR-008 §5. The `#[error(...)]` text is
|
||||
deliberately terse ("database error") so it doesn't shadow the
|
||||
source chain's detail. An `Error::database(err)` convenience
|
||||
constructor wraps anything `Into<Box<dyn Error + Send + Sync>>`.
|
||||
- `Error::PayloadTooLarge { limit: usize }` — the limit is `usize`
|
||||
(a byte count, not a std::io::ErrorKind-style re-export); doc text
|
||||
carries the ADR-020 §4 serialization-quantity pin and the
|
||||
pg-only/never-SQLite asymmetry.
|
||||
- Validation helpers split by name class exactly per ADR-008 §4's
|
||||
third-round annotation: `validate_shared_name` (six shared kinds —
|
||||
non-empty + reserved-prefix rejection) and `validate_local_name`
|
||||
(stream-consumer names, `try_lock` owner, `worker_id` — non-empty
|
||||
only). `NameKind::Shared`/`NameKind::Local` + `validate_name(kind,
|
||||
name)` is the underlying pair; the two named helpers are the
|
||||
ergonomic surface. Whitespace-only names are rejected as
|
||||
`InvalidName` for both kinds (never a meaningful name on any
|
||||
engine); names are not trimmed (the caller's name is stored as
|
||||
given — engine quoting is the engine's obligation).
|
||||
- The reserved-string test asserts its shared-name rejection carries
|
||||
`ReservedName` (it carries the reserved prefix, so it can never be
|
||||
a legal *shared* name) while `validate_local_name` accepts it — the
|
||||
two name classes distinguished in tests, mirroring the ADR text.
|
||||
- `#[non_exhaustive]` was verified on the enum definition, not in
|
||||
this crate's tests: in-crate matching stays exhaustive (the
|
||||
mechanical catch-all enforcement binds downstream crates, per the
|
||||
Rust rules ADR-017 §3 relies on) — a test here asserting a
|
||||
necessary wildcard is unreachable-code, flagged clippy `warn(unreachable_patterns)`.
|
||||
- `schedule()`/`run_schedules`' `InvalidSpec`/`LeadershipLost`
|
||||
variants (core-contract §Errors, ADR-009 §6) are *not* minted here
|
||||
— the six-variant list is this task's exact scope; the scheduler
|
||||
variants land with the trait-surface task that pins their
|
||||
signatures.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Core error taxonomy and name validation implemented in `alkstore`.
|
||||
`src/error.rs`: `Error` — exactly the six v1 variants
|
||||
(`PayloadTooLarge { limit }`, `ReservedName { name }`,
|
||||
`InvalidName { name }`, `Closed`, `Codec(String)`, `Database`
|
||||
with `#[source]`), `thiserror`-derived, `#[non_exhaustive]`, plus
|
||||
the `Result<T>` alias (the prelude shape downstream crates use).
|
||||
`src/validation.rs`: `RESERVED_PREFIX = "__alkstore_"`,
|
||||
`RESERVED_LISTENER_RECONNECTED =
|
||||
"__alkstore_listener_reconnected__"` (public for the pg engine and
|
||||
contract suite), `NameKind::{Shared, Local}`, and
|
||||
`validate_shared_name`/`validate_local_name`/`validate_name`
|
||||
helpers. All types re-exported from `src/lib.rs`. Ten unit tests
|
||||
cover empty/whitespace-only/reserved-prefix/reserved-string/legal
|
||||
names for both classes and the error display/source behavior.
|
||||
`cargo test -p alkstore` (10 pass), `cargo clippy
|
||||
--all-targets -- -D warnings`, and `cargo fmt --check` all clean;
|
||||
workspace `cargo build` compiles all three crates.
|
||||
Reference in new issue
Block a user