# Style Guide
Coding conventions for the nu_plugin_nw_ulid project. Each item has a unique ID for easy reference.
## STYLE-0000: Style guide structure
**Tags:** `meta`
### Situation
A new convention needs to be added to this style guide.
### Guidance
Assign the next sequential ID (currently next is `STYLE-0018`) and include:
1. A **Tags** line immediately after the heading — a comma-separated list of category labels
from the tag vocabulary below.
2. Three subheadings:
- **Situation** — when this rule applies
- **Guidance** — what to do (with examples where helpful)
- **Motivation** — why this rule exists
**Tag vocabulary** (extend as needed):
| `meta` | Style guide structure and process |
| `error-handling` | Error types, context messages, panics, suppression |
| `module-organization`| File layout, visibility, cohesion |
| `naming` | Naming conventions for types, functions, files |
| `commits` | Commit message format, scope rules, discipline |
| `documentation` | Doc comments, examples |
| `testing` | Test structure, fixtures, snapshots |
| `code-style` | Imports, clippy, constants, function length |
| `api-design` | Ownership, must_use, string params |
| `dependencies` | Dependency selection and management |
| `unsafe` | Unsafe code policy |
| `feature-scoping` | Feature justification and scope discipline |
A rule may have **multiple tags** — e.g., a rule about error messages in tests could be
tagged `error-handling, testing`.
Items are ordered by ID. **Do not** group items under section headings; use tags for
categorisation instead.
### Motivation
Consistent structure makes the guide scannable, and stable IDs allow code review comments
and ADRs to reference specific rules unambiguously. Tags replace section headings so that
items can remain in strict ID order without needing to be shuffled between sections when
categories overlap or new categories are introduced.
---
## STYLE-0001: Panicking operations
**Tags:** `error-handling`
### Situation
Considering `unwrap()`, `expect()`, or other panicking calls.
### Guidance
**`unwrap()` and `expect()` are acceptable** in **test code only**:
```rust
#[test]
fn test_generate_ulid() {
let ulid = UlidEngine::generate().expect("Should generate ULID");
assert!(UlidEngine::validate(&ulid.to_string()));
}
```
**In production code, use `?` with appropriate error conversion** for Nushell call methods
and engine operations:
```rust
// Good — propagates errors to the Nushell runtime
let count: Option<i64> = call.get_flag("count")?;
let ulid_str: String = call.req(0)?;
let components = UlidEngine::parse(&ulid_str)?;
```
**Use `if let` or `match` for fallible conversions** that return `Option`:
```rust
// Good — handles the None case gracefully
if let Some(datetime) =
chrono::DateTime::from_timestamp(timestamp_secs as i64, timestamp_nanos as u32)
{
record.push("iso8601", Value::string(datetime.format("...").to_string(), span));
}
```
**Never** use `unwrap()` or `expect()` on user-supplied or runtime data in library code.
### Motivation
Panics in plugin code crash the Nushell plugin host and produce poor diagnostics.
The `?` operator propagates errors as `LabeledError` values that Nushell can display
with span information, giving users actionable feedback instead of a panic backtrace.
---
## STYLE-0002: Naming patterns
**Tags:** `naming`
### Situation
Naming a new type, function, CLI command, or constant.
### Guidance
| Structs / Enums | PascalCase | `UlidEngine`, `UlidError`, `SecurityRating` |
| Traits | PascalCase (adj/verb) | `PluginCommand`, `Serialize`, `Display` |
| Functions/Methods | snake_case | `extract_timestamp()`, `validate()` |
| Type aliases | PascalCase | `Result<T>` (for crate-local aliases) |
| Constants | UPPER_SNAKE_CASE | `ULID_STRING_LENGTH`, `DEFAULT_BATCH_SIZE` |
| CLI commands | kebab-case | `ulid generate`, `ulid encode-base32` |
| Modules / files | snake_case | `ulid_engine.rs`, `security.rs` |
### Motivation
Standard Rust naming (`PascalCase` types, `snake_case` functions) is enforced by compiler
warnings and `clippy`. Kebab-case CLI commands follow Nushell conventions and are standard
across Unix tools.
---
## STYLE-0003: Commit message format
**Tags:** `commits`
### Situation
Writing a commit message.
### Guidance
Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
```
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
```
**Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `ci`, `perf`,
`build`.
**Scopes** (use the most specific that applies):
| `cargo` | Cargo build configuration and manifests |
| `ci` | CI/CD workflows and configuration |
| `cli` | CLI command methods and argument handling |
| `commands` | Individual command implementations |
| `config` | Project configuration files and tooling setup |
| `core` | Core types, constants, and engine internals |
| `deps` | Dependency updates |
| `docs` | Documentation |
| `engine` | Core UlidEngine operations |
| `error` | Error types and conversion |
| `lib` | Library-level utilities and shared code |
| `plugin` | Plugin registration, command dispatch |
| `release` | Version bumps, release process |
| `security` | Security warnings, rating system |
| `style` | Style guide rules and conventions |
| `test` | Test infrastructure and test utilities |
**Multi-scope commits** — when a change touches multiple scopes equally, list them
comma-separated: `style(engine,commands): standardise doc comments`.
**Subject line rules:**
- Lowercase first word (no capital after the colon)
- No trailing period
- Imperative mood ("add feature" not "added feature")
- Under 72 characters total
### Motivation
Conventional commits produce machine-readable history that enables automated changelogs,
version bumping, and filtering by scope. Consistent subject lines make `git log --oneline`
scannable.
---
## STYLE-0004: Doc comments
**Tags:** `documentation`
### Situation
Adding or updating documentation on a module, type, or function.
### Guidance
**Module-level docs** — every module file starts with a `//!` comment:
```rust
//! Core ULID engine providing all ULID operations for the plugin.
```
**Item-level docs** — every public struct, enum, field, variant, and method gets `///`:
```rust
/// Parsed components of a ULID.
pub struct UlidComponents {
/// ISO 8601 timestamp extracted from the ULID.
pub timestamp: String,
/// Hexadecimal representation of the random component.
pub randomness_hex: String,
}
```
**Summary line style** — write in **third-person singular present indicative** per
[RFC 505](https://rust-lang.github.io/rfcs/0505-api-comment-conventions.html). Use full
sentences ending with a period:
```rust
/// Generates a new ULID with the current timestamp.
pub fn generate() -> Result<String, UlidError> { ... }
/// Returns `true` if the string is a valid ULID.
pub fn validate(input: &str) -> bool { ... }
```
| `/// Returns the length.` | `/// Return the length.` |
| `/// Creates a new client.` | `/// Create a new client.` |
| `/// Parses the input string.` | `/// Parse the input string.` |
### Motivation
The third-person convention matches the Rust standard library and `rustdoc` output, where
doc summaries read as descriptions of what the item *does* (e.g., `Vec::push` — "Appends
an element to the back of a collection."). RFC 505 codifies this as the official Rust API
documentation style.
---
## STYLE-0005: Import ordering
**Tags:** `code-style`
### Situation
Adding `use` statements to a file.
### Guidance
Group imports into three blocks separated by a blank line, in this order:
1. **Standard library** (`std`, `core`, `alloc`)
2. **External crates** (everything from `Cargo.toml` dependencies)
3. **Crate-internal** (`crate::`, `super::`, `self::`)
Within each group, let `cargo fmt` sort alphabetically.
```rust
use std::str::FromStr;
use nu_protocol::{Record, Span, Value};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::security::SecurityWarnings;
use crate::UlidEngine;
```
**Enforcement note:** The rustfmt option `group_imports = "StdExternalCrate"` that codifies
this convention is still unstable. The three-group ordering is therefore a manual discipline
— `cargo fmt` will sort *within* a group but will not insert or enforce the blank-line
separators between groups. Review for this during code review.
### Motivation
Grouped imports make it easy to see at a glance what a module depends on externally versus
internally. The three-group convention is widely used in the Rust ecosystem. Alphabetical
ordering within groups is enforced by `cargo fmt`.
---
## STYLE-0006: Clippy configuration
**Tags:** `code-style`
### Situation
Configuring or overriding Clippy lints.
### Guidance
Run Clippy with `-D warnings` in CI so that lint violations fail the build.
When suppressing a lint on a specific item, use `#[allow(clippy::...)]` with a justification
comment explaining why the suppression is necessary:
```rust
#[allow(clippy::too_many_arguments)] // Builder pattern requires all fields at construction
fn new(title: &str, description: &str, ...) -> Self { ... }
```
Do not add blanket `#[allow(...)]` at module or crate level to silence warnings. Fix the
warning or suppress it at the narrowest possible scope.
### Motivation
`-D warnings` ensures lint violations are caught in CI. Requiring justification comments on
suppressions ensures each override is a deliberate decision rather than a way to silence
noise. Narrow-scope suppression prevents accidentally disabling a lint for unrelated code.
---
## STYLE-0007: Unsafe policy
**Tags:** `unsafe`, `code-style`
### Situation
Considering the use of `unsafe` code.
### Guidance
This project should not require `unsafe` code. If `unsafe` is ever needed, it must be:
1. Justified in an ADR
2. Isolated in a dedicated module
3. Annotated with a `// SAFETY:` comment per Clippy's `undocumented_unsafe_blocks` lint
### Motivation
This project has no need for `unsafe` — it delegates low-level operations to well-audited
dependencies (`ulid`, `chrono`, `sha2`, `blake3`). Requiring an ADR for any exception
ensures the decision is reviewed and documented.
---
## STYLE-0008: `#[must_use]` annotation
**Tags:** `api-design`
### Situation
A public function or method returns a computed value without side effects.
### Guidance
Apply `#[must_use]` to public functions whose return value is the entire point of the call.
Discarding the result is almost certainly a bug:
```rust
#[must_use]
pub fn validate(input: &str) -> bool { ... }
#[must_use]
pub fn is_security_sensitive_context(description: &str) -> bool { ... }
```
**Do not apply** `#[must_use]` to:
- Functions that return `Result` — the `#[must_use]` on `Result` itself already covers this.
- Functions with meaningful side effects (I/O, mutation) where the return value is
supplementary.
### Motivation
`#[must_use]` turns silent logic errors (ignoring a return value) into compiler warnings.
Applying it deliberately to pure computations catches bugs at compile time without producing
false positives on side-effectful functions. This aligns with `clippy::must_use_candidate`
from the `pedantic` group.
---
## STYLE-0009: String parameter ownership
**Tags:** `api-design`
### Situation
Deciding whether a function parameter should be `&str`, `String`, or generic.
### Guidance
Use the cheapest type that satisfies the function's needs:
| Only reads the string | `&str` | `fn validate(input: &str) -> bool` |
| Stores the string in a struct/`Vec` | `String` | `fn set_title(&mut self, title: String)` |
Prefer `&str` for engine methods and internal helpers. Since this project's public interface
is the Nushell plugin command layer (where `nu-protocol` types dictate parameter shapes), the
`&str` vs `String` decision applies primarily to internal function signatures.
For return types, prefer `&str` when returning a reference to owned data, and `String` when
returning a newly constructed value.
```rust
// Good — borrows for read-only access
pub fn timestamp(&self) -> &str {
&self.timestamp
}
// Good — constructs a new string
pub fn format_summary(&self) -> String {
format!("{}: {}", self.timestamp, self.randomness_hex)
}
```
### Motivation
Accepting `&str` avoids unnecessary allocations on the caller side. Taking `String` when
ownership is needed makes the transfer explicit and avoids hidden `.to_string()` calls
inside the function.
---
## STYLE-0010: Named constants
**Tags:** `code-style`, `naming`
### Situation
Using a numeric or string literal whose meaning is not obvious from surrounding context.
### Guidance
Extract **magic literals** into named constants or `const` items. A literal is "magic" when its
purpose is not self-evident at the usage site:
```rust
// Bad — what does 26 mean?
if input.len() != 26 {
// Good — the name documents the intent
const ULID_STRING_LENGTH: usize = 26;
if input.len() != ULID_STRING_LENGTH {
```
```rust
// Bad — why 10_000?
if count > 10_000 {
return Err(...)
}
// Good
const MAX_BULK_GENERATION: usize = 10_000;
if count > MAX_BULK_GENERATION {
return Err(...)
}
```
Literals that do **not** need extraction:
- **Structural zeros and ones** — `Vec::with_capacity(1)`, `index + 1`, `slice[0]`.
- **Format strings** — `format!("{}: {}", key, value)`.
- **Known-safe constructor arguments** — `FixedOffset::east_opt(0)` (covered by STYLE-0001).
- **Test assertions** — `assert_eq!(result.len(), 3)` where the value is local to the test.
Before defining a new constant, check whether one already exists for the same value — reuse it
rather than introducing a duplicate. Domain-wide constants (e.g., `ULID_STRING_LENGTH`,
`MS_PER_SECOND`) live as `pub const` in `ulid_engine.rs`; grep there first.
Place constants at the narrowest useful scope: module-level `const` if used across functions in
the same module, crate-level `pub const` in `ulid_engine.rs` if shared across modules, or
function-local `const` if truly local.
### Motivation
Named constants make the code self-documenting and provide a single point of change when a value
needs updating. Searching for `ULID_STRING_LENGTH` finds every usage; searching for `26` returns
hundreds of false positives. The exceptions prevent over-extraction of trivially obvious values.
---
## STYLE-0011: Function length
**Tags:** `code-style`
### Situation
Writing or reviewing a function that is growing long.
### Guidance
Keep functions **under ~50 lines** of logic (excluding doc comments, blank lines, and closing
braces). When a function exceeds this guideline, look for opportunities to extract coherent
sub-operations into well-named helper functions.
Common extraction targets:
- **Setup / teardown** — opening resources, building configuration structs.
- **Distinct phases** — validation, transformation, output formatting.
- **Repeated patterns** — similar blocks that differ only in parameters.
- **Nested closures** — iterator chains with complex transformation logic.
```rust
// Before — 120-line run() mixing validation, parsing, formatting, and output
fn run(&self, ...) -> Result<PipelineData, LabeledError> {
// ... 120 lines ...
}
// After — orchestrator delegates to focused helpers
fn run(&self, ...) -> Result<PipelineData, LabeledError> {
let input = self.parse_input(&call)?;
let components = self.build_components(&input)?;
let record = self.format_output(components, &call)?;
Ok(Value::record(record, span).into_pipeline_data())
}
```
This is a **guideline, not a hard limit**. A 60-line function that reads linearly may be clearer
than three 20-line functions with non-obvious data flow. Use judgement — the goal is readability,
not a line count.
### Motivation
Long functions are harder to name, test, and review. Extracting sub-operations gives each piece
a name that serves as documentation and makes the top-level flow scannable. The ~50-line
heuristic is a common industry threshold (Clean Code, Effective Rust) that balances granularity
against fragmentation.
---
## STYLE-0012: Silent error suppression
**Tags:** `error-handling`
### Situation
Handling a `Result` or `Option` where the error/`None` case is intentionally ignored.
### Guidance
**Never silently discard an error that could indicate a real problem.** Three patterns to watch
for:
1. **`let _ = fallible_call();`** — If the operation can meaningfully fail, propagate with `?`
or report via `eprintln!`. If the failure is truly inconsequential, add a comment
explaining why:
```rust
let _ = call.get_flag::<String>("format");
let format = call.get_flag::<String>("format")?;
if let Err(e) = UlidEngine::parse(&input) {
eprintln!("Skipping invalid ULID: {e}");
}
```
2. **`if let Ok(x) = ... { use(x) }`** with no `else` — returning a silent default on parse
or I/O failure hides broken data from the user:
```rust
let timestamp = UlidEngine::extract_timestamp(input).unwrap_or(0);
let timestamp = match UlidEngine::extract_timestamp(input) {
Ok(ts) => ts,
Err(e) => {
eprintln!("Failed to extract timestamp: {e}");
0
}
};
```
3. **`.unwrap_or_default()` on non-trivial results** — acceptable for genuinely optional data,
but not as a blanket substitute for error handling on operations that should succeed.
**Acceptable silent discards:**
- Best-effort error reporting in streaming mode, where the stream should continue processing
remaining items (see `stream.rs` for the existing pattern).
- Optional metadata that may be absent and has a sensible default (e.g., timezone info from
a parsed timestamp).
### Motivation
Silent error suppression is one of the hardest bugs to diagnose because nothing visibly fails —
the program simply produces wrong results or missing data. Reporting errors via `eprintln!`
costs nothing on the success path and provides a trail when something goes wrong. The explicit
comment requirement for `let _ =` forces the author to justify the suppression at write time,
which often reveals that the error should not be ignored after all.
---
## STYLE-0013: Single-purpose commits
**Tags:** `commits`
### Situation
Preparing a set of changes that involves refactoring, new functionality, or bug fixes.
### Guidance
Each commit should do **one kind of work**. Keep refactoring commits separate from
implementation commits, and both separate from bug-fix commits.
If a refactoring would make a subsequent implementation or fix cleaner, land the refactoring
as an **earlier** commit so that:
1. The refactoring can be reviewed on its own terms (no behaviour change expected).
2. The implementation commit starts from a cleaner baseline and is easier to understand.
3. Either commit can be reverted independently if needed.
```
# Good — reviewable, bisectable, revertible
git log --oneline
a1b2c3 refactor(engine): extract timestamp formatting helper
d4e5f6 feat(commands): add --json output to inspect command
# Bad — mixed intent, hard to review or revert half of it
git log --oneline
f7g8h9 feat(commands): add --json output and refactor timestamp formatting
```
**Acceptable exceptions:**
- Trivial renames or import cleanups that are a natural by-product of the implementation
(a few lines, not a standalone refactoring effort).
- Prototype or spike branches where commit hygiene is deferred to a squash before merge.
### Motivation
Single-purpose commits make `git bisect` reliable, code review focused, and reverts
surgical. When refactoring is interleaved with behaviour changes, reviewers cannot tell
whether a difference is a deliberate new behaviour or a mechanical restructuring — so they
must verify every line as if it were new logic. Separating the two cuts review effort
roughly in half.
---
## STYLE-0014: Module cohesion
**Tags:** `module-organization`
### Situation
A source file is accumulating types, functions, or `impl` blocks that serve unrelated
purposes.
### Guidance
Each module should have a **single, nameable responsibility**. When you find it hard to
describe what a module does without using "and," it likely contains unrelated code that
would be clearer in separate modules.
**Signals that a module should be split:**
- It contains multiple independent command or handler types that share little or no private
state (e.g., `UlidSortCommand` and `UlidInspectCommand` in one file).
- Unrelated sections require scanning past hundreds of lines to find the piece you need.
- Changes to one logical area routinely cause merge conflicts with work in another area of
the same file.
- You struggle to name the file — broad names like `commands.rs` or `helpers.rs` suggest
mixed responsibilities.
**What is *not* a reason to split:**
- Line count alone. A 400-line module with a single cohesive type and its helpers is fine.
- A few shared utility functions that genuinely serve every type in the module.
#### Command domain grouping
For plugin commands specifically, group by **command domain** — the shared subcommand noun
in the command name. Commands that share a domain noun typically share private helpers,
constants, and error-handling logic, making them cohesive within a single file.
For example, `ulid hash sha256`, `ulid hash sha512`, and `ulid hash blake3` all belong to
the `hash` domain and live together in `hash.rs`. A single-command domain like `ulid sort`
still gets its own file (`sort.rs`).
The current domains in `src/commands/` are:
| `ulid.rs` | core | generate, validate, parse, security-advice |
| `encode.rs` | encode | encode base32, decode base32, encode hex, decode hex |
| `hash.rs` | hash | hash sha256, hash sha512, hash blake3, hash random |
| `time.rs` | time | time now, time parse, time millis |
| `uuid.rs` | uuid | uuid generate, uuid validate, uuid parse |
| `stream.rs` | stream | stream, generate-stream |
| `sort.rs` | sort | sort |
| `inspect.rs` | inspect | inspect |
| `info.rs` | info | info |
**When to create a new domain file vs. extending an existing one:**
- If a new command shares the subcommand noun of an existing domain, add it to that file.
- If a new command introduces a new noun (e.g., `ulid format …`), create a new file named
after the noun (`format.rs`).
- Prefer domain-level files over one-file-per-command. A domain file with several related
commands and their helpers is more navigable than many thin files that scatter shared
context.
### Motivation
A module that mixes unrelated responsibilities is hard to navigate, produces noisy diffs,
and invites merge conflicts between independent work streams. Splitting by responsibility
makes each file's purpose obvious from its name, keeps diffs focused on the change at hand,
and lets reviewers evaluate one concern at a time. The emphasis on cohesion rather than a
rigid line limit avoids unnecessary churn on files that are large but focused, while still
flagging files that are large *because* they mix concerns.
Grouping commands by domain rather than putting all 23 commands in one file or giving each
its own file strikes a balance: related commands share helpers and constants without
cross-domain coupling, while the file tree remains compact enough to scan at a glance.
---
## STYLE-0015: Prefer standard library over external crates
**Tags:** `dependencies`
### Situation
Adding a new dependency, or a Rust release has stabilised functionality that an existing
dependency provides.
### Guidance
Before adding an external crate, check whether `std` already provides equivalent
functionality at the project's current MSRV. If it does, use `std`.
When a new Rust release absorbs a crate's functionality into `std` and the MSRV supports it,
migrate existing usage to the `std` equivalent and remove the external dependency.
This applies only where `std` provides a **genuine equivalent**. Domain-specific crates with
no `std` counterpart (e.g., `chrono`, `serde`, `blake3`, `ulid`) are unaffected.
```rust
// Good — std::sync::LazyLock (stable since 1.80.0)
use std::sync::LazyLock;
// Avoid — once_cell when std equivalent is available at MSRV
use once_cell::sync::Lazy;
If a crate's API is significantly more ergonomic or feature-rich than its `std` equivalent,
evaluate on a case-by-case basis whether the ergonomic benefit justifies the dependency cost.
### Motivation
Each external dependency carries costs: supply chain risk (see ADR-0002), compilation time,
upgrade burden, and version conflict surface. When `std` offers equivalent functionality,
these costs can be eliminated entirely. The Rust ecosystem follows a well-established pattern
of absorbing popular crate APIs into the standard library — aligning with this trend keeps
the dependency tree lean and reduces long-term maintenance.
---
## STYLE-0016: Error boundary conversion
**Tags:** `error-handling`
### Situation
A command calls an `UlidEngine` method that returns `Result<_, UlidError>`.
### Guidance
Engine functions return `UlidError` — they must not depend on `nu-protocol`. Commands
convert to `LabeledError` at the call boundary using `.map_err()` with the call span:
```rust
// Standard commands — return Result<PipelineData, LabeledError>
let components = UlidEngine::parse(&ulid_str)
.map_err(|e| LabeledError::new("Parse failed")
.with_label(e.to_string(), call.head))?;
```
Stream helpers that return `Result<_, Box<LabeledError>>` box the error instead:
```rust
// Stream helpers — return Result<_, Box<LabeledError>>
let ulid = UlidEngine::generate_with_timestamp(ts).map_err(|e| {
Box::new(LabeledError::new("Generation failed")
.with_label(e.to_string(), call_head))
})?;
```
The `LabeledError::new()` message should be a short category (e.g., "Parse failed",
"Generation failed"). The `.with_label()` carries the detailed `UlidError` display string
and the span for Nushell's error rendering.
### Motivation
Keeping `UlidError` free of `nu-protocol` means the engine can be tested and reused without
pulling in Nushell types. Converting at the command boundary — rather than inside the engine
or via a blanket `impl From` — makes the span available for error labels and keeps the
conversion visible at each call site. The pattern is consistent across ~8 call sites in
`ulid.rs`, `inspect.rs`, `time.rs`, and `stream.rs`.
---
## STYLE-0017: Feature justification
**Tags:** `feature-scoping`
### Situation
Proposing or implementing a new feature, command, flag, or module.
### Guidance
Every feature must solve a real problem for the user. Before building a feature, describe
a concrete user scenario where it provides value. If no such scenario exists, do not build
it.
**Design principles are not features.** Principles like security, performance, or
correctness describe *how* code should be written, not *what* to build. Apply them to the
code you are already writing:
- **Security:** do not introduce vulnerabilities (e.g., avoid `unsafe`, validate external
input at system boundaries, use constant-time comparison where needed).
- **Performance:** choose efficient algorithms and data structures; avoid unnecessary
allocations.
- **Correctness:** handle edge cases, propagate errors, write tests.
Do not create standalone features (new modules, commands, flags, or output fields) solely
to demonstrate compliance with a principle.
**Questions to ask before adding a feature:**
1. What user problem does this solve?
2. Can the user already solve it with existing commands or Nushell primitives?
3. Is the complexity proportional to the value delivered?
### Motivation
Vague principle-level directives (e.g., "prioritise security") can be misinterpreted as
feature requests, leading to invented functionality that provides no real value — such as
runtime keyword detection systems or advisory flags that no user asked for. This happened
in #69 and #91. Requiring a concrete user scenario as a prerequisite for every feature
prevents speculative artifacts and keeps the codebase focused on genuine user needs.