Agent-First Slug v0.9: Checked Before It Is Needed

by Agent-First Kit Contributors

A fallback that broke its own configuration used to fail only on the rare input that needed it. Fallbacks are now checked before any input is read, every afslug error names the rule it broke, decimal dots need digits the character set actually keeps, and errors identify invisible characters by code point.

A fallback exists for the input nobody tested: the title that is all emoji, the name that is all punctuation. That is also why a broken fallback could live in a configuration for a long time. It was only checked when a slug came out empty, so every test that never produced an empty slug passed.

A fallback is checked on every call

v0.8 required a UseFallbackSlug value to be something the configuration could itself have produced. The check was right; the moment it ran was not. Given ordinary input, a fallback that broke the rules went unnoticed:

$ afslug slugify "hello" --fallback "a--b"

v0.8 returned hello. v0.9 refuses the configuration before it reads the input:

{"error":{"code":"fallback_violates_config","hint":"pass a --fallback this configuration could itself have produced, or --fallback-verbatim to insert the value as written","message":"fallback slug does not satisfy this configuration: a generated slug never repeats the replacement delimiter","retryable":false},"kind":"error","trace":{}}

The same holds for UseVerbatimFallbackSlug, which is still checked against the target surface only, and for an empty fallback of either kind, which is now its own error (SlugError::EmptyFallbackSlug):

$ afslug slugify "hello" --fallback ""
{"error":{"code":"empty_fallback_slug","hint":"omit the fallback to keep an empty slug, or pass a nonempty value","message":"fallback slug must not be empty","retryable":false},"kind":"error","trace":{}}

An empty fallback would turn an empty slug into the same empty slug, so it can only be a mistake. If an empty result is acceptable, leave the fallback out.

The upshot is that a call that succeeds tells you the fallback is valid, not merely that it was never reached.

Every error names its rule

afslug used to report every library failure as slug_error, so a caller that wanted to tell “this value contains a separator” from “this delimiter is ambiguous” had to parse the message. Each SlugError now has its own error.code:

$ afslug validate "a/b" --policy local-path
{"error":{"code":"path_segment_separator","message":"path segment must not contain separator `/`","retryable":false},"kind":"error","trace":{}}

The exit status now separates two kinds of failure. A configuration spelled out wrongly in the flags (a fallback that breaks the rules, an ambiguous delimiter) exits 2, like any other invalid argument value: the call was never made, so retrying it unchanged cannot help. A value that fails validation still exits 1.

Errors you can read when the character is invisible

A validation error used to quote the offending whitespace between backticks. For a no-break space or an ideographic space that printed as a space, and for a tab or a newline it broke the line. Whitespace is now named by code point:

$ afslug validate $'a b' --policy url-path
{"error":{"code":"path_segment_whitespace","message":"path segment must not contain whitespace U+00A0","retryable":false},"kind":"error","trace":{}}

Delimiters get the same treatment. A combining mark quoted between backticks fused with the opening backtick, so the error that refused it could not show which character it meant:

$ afslug slugify "x" --delimiter $'́' --charset unicode-letters-marks-digits
{"error":{"code":"ambiguous_replacement_delimiter","hint":"pass a --delimiter this configuration filters out, such as `-` or `_`","message":"replacement delimiter U+0301 is ambiguous: the allowed character set keeps it from the input","retryable":false},"kind":"error","trace":{}}

A decimal dot needs digits that survive

PreserveDotsBetweenDecimalDigits keeps a dot that sits between two decimal digits, so ubuntu 16.04 stays ubuntu-16.04. It asked whether the neighbours were decimal digits, not whether the character set kept them. Under an ASCII-only set, ١ (Arabic-Indic one) is a decimal digit that gets filtered out, so ١.2 became .2: a slug starting with a dot that no kept digit explains.

A dot is now preserved only between digits the character set keeps:

$ afslug slugify "١.2" --charset ascii-alphanumeric --dots preserve-between-digits
{"kind":"result","result":{"changed_from_input":true,"code":"slugify","slug":"2"},"trace":{}}

Inputs whose digits are all kept are unaffected.

Breaking changes

Run v0.8 and v0.9 over your stored inputs and diff before upgrading.

  • SlugError gains EmptyFallbackSlug. An exhaustive match stops compiling until it handles it.
  • Fallbacks are validated before any input is read. A configuration with an invalid or empty fallback now fails on every call, not only on input that slugifies to nothing.
  • afslug error codes are per rule (path_segment_separator, fallback_violates_config, ambiguous_replacement_delimiter, …) instead of slug_error, and configuration errors exit 2 instead of 1.
  • Decimal dots next to filtered digits become delimiters. ١.2 under an ASCII-only set was .2, now 2.
  • Whitespace and delimiter errors name characters by code point instead of quoting them. Code matching on message text needs updating; match on error.code instead.

Also in this release: transliteration allocates only once a pattern matches, truncation no longer indexes the whole input, a --max-chars too large for the platform’s pointer size is refused instead of wrapping, a property test checks that every generated slug is accepted as its own fallback and slugifies to itself, the minimum Rust version is declared as 1.98, and Agent-First Data moves to 0.36.1, whose argument errors now give a hint that fits the rule that was broken.

Getting it

$ brew install agentfirstkit/tap/afslug
$ cargo install agent-first-slug