This file provides guidance to AI coding assistants working with code in this repository.
Subdirectory guidance: The hyperdb-api-node/ directory has its own AGENTS.md covering the Node.js/TypeScript bindings, napi-rs build system, and JS-specific patterns.
Bootstrapping hyperd: Contributors obtain the hyperd executable by
running make download-hyperd (or .\build.ps1 download-hyperd). The
implementation lives in the hyperdb-bootstrap crate;
the pinned release is baked into
hyperdb-bootstrap/hyperd-version.toml.
Bumping hyperd = edit that file (version + the four [wheel_tag] entries +
the four per-platform sha256s — there is no build_id), then let the
fix(bootstrap): commit drive the version via release-please (the crate uses
version.workspace = true — don't hand-edit a crate version). hyperd comes
out of the PyPI tableauhyperapi wheels, so you don't compute the digests:
read them straight off the JSON API with
curl -s https://pypi.org/pypi/tableauhyperapi/<version>/json | jq -r '.urls[] | "\(.filename) \(.digests.sha256)"'.
The full repeatable procedure — verify the pin, run the suite, A/B benchmark
against the previous pin, and log the result — is captured in the
update-hyperd-release skill.
Performance history is tracked in
docs/hyperd-release-benchmarks.md, which
takes a row on either a hyperd pin bump or a material API change
(edition/MSRV migration, hot-path rewrite, codegen-affecting dependency bump).
Each row is the baseline the next A/B measures against, so conflating the two
variables makes an engine delta unattributable.
This is a pure-Rust implementation of the Hyper database API, using the PostgreSQL wire protocol with Hyper-specific extensions. It allows Rust applications to create, read, and manipulate Hyper database files (.hyper) without any C library dependencies.
Key characteristics:
- 100% pure Rust (no FFI, no C dependencies)
- High performance on a single connection (100M-row benchmark, Apple M3 Max): 68.9M rows/sec inserts via the async
AsyncArrowInserter, 25.0M rows/sec via the syncInserter, 31.1M rows/sec full-scan queries via the sync path. See docs/BENCHMARK_GUIDE.md for the multi-connection and per-platform figures. - Independent library (can be extracted from this repository)
- Zero build system dependencies (uses standard Cargo)
- No feature flags on
hyperdb-api— every capability of the flagship crate (TLS, pooling, geography, transactions, chrono) is always available. A few companion crates do carry optional features; see Feature Flags for the complete list.
The codebase uses a layered architecture. The flagship user-facing crate is hyperdb-api; its implementation details live in hyperdb-api-core, which preserves three internal submodules (types, protocol, client) that contributors navigate independently. Two optional companion crates extend the public surface.
┌─────────────────────────────────────────────────────┐
│ hyperdb-api (High-level API, public) │
│ - Connection, AsyncConnection, HyperProcess │
│ - Inserter, Catalog, Arrow integration │
│ - Pool, gRPC, Transactions │
└────────────────┬────────────────────────────────────┘
│ depends on (exact-match pin)
▼
┌─────────────────────────────────────────────────────┐
│ hyperdb-api-core (internal implementation detail) │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ src/client/ (Connection Management) │ │
│ │ - Sync/Async TCP clients │ │
│ │ - Authentication (MD5, SCRAM-SHA-256) │ │
│ │ - gRPC transport & TLS (rustls) │ │
│ └────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌────────────────▼──────────────────────────────┐ │
│ │ src/protocol/ (Wire Protocol) │ │
│ │ - PostgreSQL protocol messages │ │
│ │ - HyperBinary COPY format │ │
│ │ - Message parsing/encoding │ │
│ └────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌────────────────▼──────────────────────────────┐ │
│ │ src/types/ (Type System) │ │
│ │ - LittleEndian binary encoding │ │
│ │ - Type conversions (Date, Numeric, Geography)│ │
│ │ - SQL type definitions (Oid, SqlType) │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Companion crates (optional, add when needed):
┌──────────────────────────┐ ┌──────────────────────────┐
│ hyperdb-api-salesforce │ │ sea-query-hyperdb │
│ Salesforce Data Cloud │ │ HyperDB dialect backend │
│ OAuth authentication │ │ for sea-query │
└──────────────────────────┘ └──────────────────────────┘
Important: hyperdb-api-core is published to crates.io (Cargo requires it, because hyperdb-api depends on it) but it is not a public API — users should depend on hyperdb-api only. See hyperdb-api-core/README.md for the "forever internal" positioning.
Each submodule has clear boundaries. Always work within the appropriate layer:
- Type encoding issues →
hyperdb-api-core/src/types/ - Protocol message issues →
hyperdb-api-core/src/protocol/ - Connection/transport issues →
hyperdb-api-core/src/client/ - High-level API issues →
hyperdb-api - Salesforce OAuth →
hyperdb-api-salesforce - sea-query SQL generation →
sea-query-hyperdb - MCP server (CLI product) →
hyperdb-mcp hyperdbootstrap helper →hyperdb-bootstrap
The codebase provides both synchronous and asynchronous APIs:
-
Sync API:
Connection,Inserter,HyperProcess- Used in
hyperdb-api/src/connection.rs,inserter.rs - Blocking I/O, simpler API surface
- Used in
-
Async API:
AsyncConnection,AsyncArrowInserter, connection pooling- Used in
hyperdb-api/src/async_connection.rs,async_arrow_inserter.rs - Tokio-based, non-blocking I/O
- Connection pooling via
poolmodule
- Used in
When implementing features, consider whether both sync and async variants need updates.
The API supports two transport protocols:
-
TCP Transport (default): Direct PostgreSQL wire protocol
- Uses
hyperdb_api_core::client::{Connection, AsyncConnection} - Primary transport for most operations
- Supports authentication, TLS, CREATE/INSERT/UPDATE/DELETE
- Uses
-
gRPC Transport: Read-only queries with Arrow IPC format
- Uses
hyperdb_api_core::client::grpc::GrpcClient - Exposed via
hyperdb_api::grpc::GrpcConnection - Read-only (queries only, no mutations)
- Always available
- Used for high-performance streaming queries
- Uses
Architecture pattern: The high-level Connection abstracts over transports via the transport module (hyperdb-api/src/transport.rs).
Tests and examples spawn a real hyperd. Run make download-hyperd (or
.\build.ps1 download-hyperd) once: it installs the release pinned in
hyperdb-bootstrap/hyperd-version.toml
under .hyperd/current/. The make/build.ps1 targets export HYPERD_PATH
themselves, so plain make test needs no further setup.
Always use the pinned binary at .hyperd/current, never an ad-hoc hyperd
elsewhere on the machine. That is the release this repo pins and the one CI
runs against — .github/workflows/ci.yml sets
HYPERD_PATH to .hyperd/current — so local results stay comparable to CI. A
copy sitting somewhere else may be an unrelated or unversioned build, which
makes a local pass weaker than it looks.
For a bare cargo invocation the most robust option is to set HYPERD_PATH to
an absolute path — the executable and its containing directory are both
accepted, and CI uses the directory form. An absolute value is inherited
unchanged by any child process a test spawns, so it works for the whole
suite:
export HYPERD_PATH="$PWD/.hyperd/current" # from the workspace root
cargo test -p hyperdb-mcp --test attach_testsLeaving HYPERD_PATH unset also works for most local runs and is fine for a
quick single-crate check: HyperProcess::new() walks up from the working
directory to find .hyperd/current/hyperd, locating the pinned binary from
anywhere in the repo. The catch is that this discovery is relative to the
process's working directory, so it fails for any test that re-execs a child with
a relocated working directory and HOME — notably hyperdb-mcp's
recovery_tests watchdog/health cases
(slow_health_report_does_not_hold_engine_mutex and the two
slow_health_watchdog_reaps_hyperd_*): the child lands outside the repo, its
upward walk finds no .hyperd, and it fails with Hyper PID was not reported.
Measured on upstream/main, unset gives 5 passed; 3 failed there while an
absolute HYPERD_PATH gives 8 passed — so prefer the absolute form as your
default and reserve unset for quick single-crate runs.
A relative path does not work. Cargo runs an integration test with the
working directory set to the package root (hyperdb-mcp/), not the
workspace root, so HYPERD_PATH=.hyperd/current resolves against the wrong
directory and every hyperd-backed test fails with HYPERD_PATH set to '.hyperd/current' but hyperd executable not found.
The Makefile falls back to .hyperd/current/hyperd only when HYPERD_PATH is
unset — an exported value always wins. Don't leave a stale one in your
shell profile, or make test silently inherits it too.
Confirm the engine before trusting a run:
.hyperd/current/hyperd --versionIt must report the version from hyperdb-bootstrap/hyperd-version.toml. If
it prints __UNVERSIONED_HYPER__ or some other version, the local engine is
not the pinned one and the results are not comparable to CI.
The entire workspace is edition = "2024" as of 1.0.0 — not just a few transitive deps. Older copies of the rust-analyzer binary bundled with the VS Code extension reject that outright:
failed to interpret `cargo metadata`'s json: unknown variant `2024`
rust-toolchain.toml lists rust-analyzer in its components, so rustup installs a matching binary for you when the toolchain is provisioned — no manual rustup component add step. What you may still need is to point the extension at that binary rather than its bundled one, from your user settings (not workspace settings — we intentionally don't commit this, so contributors on newer extensions aren't forced to change anything):
"rust-analyzer.server.path": "rust-analyzer"The rustup-shipped binary tracks the active toolchain, so it stays in lockstep with cargo. "rust-analyzer" with no path resolves via $PATH — the rustup shim under ~/.cargo/bin on Unix or %USERPROFILE%\.cargo\bin on Windows.
One rust-analyzer quirk worth knowing, unrelated to the edition: with the compile-time feature enabled, query_as! validation depends on derive(Table) having registered the type in the same proc-macro process. rust-analyzer expands macros lazily and from cache, so it can expand a query_as! in a process where no derive ran. Validation detects that and skips rather than reporting a false "not registered" error, so the editor should stay quiet on code that cargo check accepts.
The repo provides a Makefile for Linux/macOS and a PowerShell equivalent
build.ps1 for Windows. Both wrappers auto-set HYPERD_PATH for test/run
targets. Plain cargo … invocations also work — set an absolute HYPERD_PATH
(the robust default, e.g. "$PWD/.hyperd/current"), or leave it unset and let
the upward .hyperd/current walk resolve it for most single-crate runs.
Linux / macOS (bash):
make build # Build all crates (debug)
make build-release # Build release binaries
make test # Run all tests (debug) - auto-sets HYPERD_PATH
make test-release # Run tests (release mode)
make examples # Run all examples (or: ./run_all_examples.sh)
make doc # Generate documentation (Hyper crates only, no dependencies)
make clean # Clean build artifacts AND test files (.hyper, logs)
make clean-test-files # Clean only test-generated filesWindows (pwsh / PowerShell):
.\build.ps1 build # Build all crates (debug)
.\build.ps1 build-release # Build release binaries
.\build.ps1 test # Run all tests (debug) - auto-sets HYPERD_PATH
.\build.ps1 test-release # Run tests (release mode)
.\build.ps1 examples # Run all examples (or: .\run_all_examples.ps1)
.\build.ps1 doc # Generate documentation (Hyper crates only, no dependencies)
.\build.ps1 clean # Clean build artifacts AND test files (.hyper, logs)
.\build.ps1 clean-test-files # Clean only test-generated filesThe bare cargo equivalent for any target works on either platform once
.hyperd/ is populated. Prefer an absolute HYPERD_PATH (e.g.
HYPERD_PATH="$PWD/.hyperd/current" cargo test --workspace): a whole-workspace
run includes the recovery_tests child-re-exec cases above, which need it. The
upward .hyperd/current discovery (leaving HYPERD_PATH unset) covers most
narrower single-crate runs.
# Run all tests in a specific crate
cargo test -p hyperdb-api
# Run a specific test
cargo test -p hyperdb-api test_name
# Run tests in a specific file (use module path)
cargo test -p hyperdb-api --test integration_test# Run a specific example
cargo run -p hyperdb-api --example insert_data_into_single_table
# Run companion crate examples
cargo run -p sea-query-hyperdb --example basic_usage
cargo run -p hyperdb-api-salesforce --example salesforce_auth_example
# Run all examples
./run_all_examples.shThe hyperdb-api crate has no feature flags. All capabilities (TLS, pooling,
geography, transactions, chrono) are always enabled. This simplifies dependency
management and matches the C++/Python/Java APIs.
Domain-specific functionality lives in companion crates:
sea-query-hyperdb— HyperDB dialect backend forsea-queryhyperdb-api-salesforce— Salesforce Data Cloud OAuth authentication
Three other crates do define features. hyperdb-api is flag-free; the workspace as a whole is not:
hyperdb-api-core—salesforce-auth(off by default): pulls inhyperdb-api-salesforceandarrow. A plumbing detail; normally picked up transitively throughhyperdb-api.hyperdb-api-derive—compile-time(off by default): enables compile-time SQL validation viaquery_as!andderive(Table)#[hyperdb(register)], pulling inhyperdb-compile-check.hyperdb-bootstrap—cli(on by default): theclap/anyhow/tracing-subscribercommand-line surface. Depend on it withdefault-features = falseto use it as a library.
Tests are organized by crate:
hyperdb-api/tests/ # Integration tests (high-level API)
hyperdb-api/tests/common/ # Shared test utilities
hyperdb-api-core/tests/ # Client-level integration tests
hyperdb-api-core/src/protocol/ # Unit tests (inline with code)
hyperdb-api-core/src/types/ # Unit tests (inline with code)
Test utilities:
hyperdb-api/tests/common/mod.rs- Shared test helpershyperdb-api-core/src/client/test_util.rs- Client test utilities- Both use
HyperProcess::new()to start temporaryhyperdservers
Pattern: Tests create temporary .hyper files and clean them up automatically. The make clean-test-files command removes any leftover test artifacts.
All public APIs return Result<T, Error> where Error is from hyperdb_api::Error:
use hyperdb_api::{Result, Error};
pub fn some_function() -> Result<()> {
// Use ? operator for error propagation
let conn = Connection::connect(...)?;
conn.execute_command("...")?;
Ok(())
}Error types are defined in:
hyperdb-api/src/error.rs- High-level errors withErrorKindvariantshyperdb-api-core/src/client/error.rs- Client-level errorshyperdb-api-core/src/protocol/- Protocol errors (minimal, mostly I/O)
Query results are always streaming to maintain constant memory usage:
let mut result = conn.execute_query("SELECT * FROM large_table")?;
// Process in chunks (default: 16384 rows per chunk)
while let Some(chunk) = result.next_chunk()? {
for row in &chunk {
// Process row
}
}Important: Never load entire result sets into memory. Always use chunk-based iteration.
Type conversions follow these patterns:
-
Reading values: Use
row.get::<T>(col_index)with type inferencelet id: Option<i32> = row.get(0); let name: Option<String> = row.get(1);
-
Writing values: Implement
ToSqlParamorIntoValuetraitsToSqlParam- For query parameters (text format)IntoValue- For inserter values (binary format)
All conversions are in hyperdb-api-core/src/types/types.rs and traits.rs.
Connections have explicit lifecycle management:
// Start a server (manages hyperd process)
let hyper = HyperProcess::new(None, None)?;
let endpoint = hyper.require_endpoint()?;
// Connect (opens TCP connection + PostgreSQL handshake)
let conn = Connection::connect(endpoint, "db.hyper", CreateMode::CreateIfNotExists)?;
// Use connection...
// Close explicitly (or drop will close)
conn.close()?;
// HyperProcess drop handler stops the hyperd processNote: HyperProcess::drop() automatically stops the hyperd subprocess. Tests rely on this for cleanup.
The codebase uses Apache Arrow for high-performance data exchange:
- Reading:
ArrowReaderreads query results as Arrow RecordBatches - Writing:
ArrowInserter/AsyncArrowInserterwrite Arrow data to Hyper - gRPC: All gRPC queries return Arrow IPC format
Arrow types are in hyperdb-api/src/arrow_result.rs, arrow_reader.rs, arrow_inserter.rs.
- Add type definition to
hyperdb-api-core/src/types/oid.rs(OID constant) - Add SQL type constructor to
hyperdb-api-core/src/types/sql_type.rs - Implement
FromBinaryValuetrait inhyperdb-api-core/src/types/types.rs - Implement
ToSqlParamfor query parameters (text format) - Implement
IntoValuefor inserter (binary format) - Add tests in
hyperdb-api-core/src/types/types.rs
- Implement protocol-level support in
hyperdb-api-core/src/protocol/ - Add client-level support in
hyperdb-api-core/src/client/client.rsorasync_client.rs - Expose high-level API in
hyperdb-api/src/connection.rsorasync_connection.rs - Add integration tests in
hyperdb-api/tests/ - Document in the appropriate
README.md(user-facing usage) andDEVELOPMENT.md(internals)
- Implement transport interface in
hyperdb-api-core/src/client/ - Add transport variant to
hyperdb-api/src/transport.rs - Update
Connection::new()to support new transport - Add tests in
hyperdb-api-core/tests/andhyperdb-api/tests/
Whenever an MCP tool is added, renamed, removed, or its parameters/behavior
change — and whenever a feature surfaces through the MCP (new file format, new
export target, new SQL capability worth highlighting) — update
hyperdb-mcp/src/readme.rs so the LLM-facing
README returned by the get_readme tool stays accurate. The structural test in
hyperdb-mcp/tests/readme_tests.rs enforces tool-name coverage; semantic
content (parameter rules, examples, SQL quirks) is human-maintained and won't
fail loudly when stale.
- Inserter API uses binary COPY protocol - 10-100x faster than INSERT statements
- Streaming results - Always process in chunks, never load all rows
- Arrow batching - Use the async
AsyncArrowInserterfor maximum insert throughput: 68.9M rows/sec on a single connection, versus 25.0M rows/sec for the syncInserter. Only the async variant is benchmarked at that rate — the syncArrowInserteris not measured by the suite. Spending extra connections on an Arrow insert buys nothing on the benchmarked host. - Release builds - Use
--releasefor benchmarks (debug is 10x+ slower) - Connection pooling - Use
poolmodule for async high-concurrency scenarios
See docs/BENCHMARK_GUIDE.md for benchmark methodology and reproduction.
The codebase handles platform-specific IPC transport:
- Unix/MacOS: Uses Unix Domain Sockets (UDS) by default, falls back to TCP
- Windows: Uses Named Pipes, falls back to TCP
IPC detection is in hyperdb-api/src/process.rs. Most code is platform-agnostic.
Documentation is split by audience:
- READMEs (
README.md) — user-facing: what the crate does, quick start, usage examples - DEVELOPMENT.md — contributor-facing: internal architecture, design decisions, how to extend, testing
- Source code (
///and//!) — implementation details co-located with code docs/— cross-cutting topics: performance, benchmarks, transactions, comparisonsdocs/superpowers/— planning artifacts kept in-repo and committed: design specs underdocs/superpowers/specs/YYYY-MM-DD-<topic>-design.mdand their implementation plans underdocs/superpowers/plans/YYYY-MM-DD-<feature>.md. Non-trivial features are brainstormed into a spec, then a plan, before code; keeping both in the repo makes the design rationale reviewable alongside the change that implements it.
All public items have /// doc comments. Module-level docs (//!) explain architecture and patterns. Examples are in hyperdb-api/examples/ and tested via run_all_examples.sh. Companion crate examples in hyperdb-api-salesforce/examples/ and sea-query-hyperdb/examples/. API docs are generated via make doc.
See docs/RUST_DOCUMENTATION_STYLE.md for the full documentation style guide.
This project uses Conventional Commits
for commit messages. Versioning and changelog generation are fully automated
by release-please:
.github/workflows/release-please.yml
runs on every push to main and opens (or updates) a
chore(main): release X.Y.Z PR, driven by
release-please-config.json and
.release-please-manifest.json. It also
re-runs on the release: published event a hand-cut tag emits, so cutting the
tag re-anchors the next -rc.N correctly (#308). Never hand-edit a crate
version or the root CHANGELOG.md. See
CONTRIBUTING.md for the full flow and
docs/GITHUB_OPERATIONS.md for the
maintainer steps.
While the workspace is on an -rc.N line, the rc counter increments
automatically from any conventional commit — no Release-As: footer needed —
because release-please-config.json sets
prerelease, prerelease-type, and versioning. Those keys must be removed
when the final release ships, or the next fix: computes another rc instead of
a stable patch. The mechanism and that removal step are documented once, in
docs/GITHUB_OPERATIONS.md → Pre-releases.
All commit messages must follow the format <type>(<scope>): <subject> — for the full specification including commit types, version impact, and examples, see CONTRIBUTING.md.
- Main branch:
main - Test artifacts (
.hyperfiles, logs) are gitignored - Use
make clean-test-filesbefore committing to remove test debris - CI/CD should set
HYPERD_PATHappropriately
-
Never commit
.hyperfiles orhyperd*.logfiles - These are test artifacts -
Always propagate errors with
?- Don't panic in library code -
Test both sync and async APIs when adding features
-
Use
make testinstead ofcargo testto ensureHYPERD_PATHpoints at the pinned.hyperd/currentengine CI uses, not an ad-hoc copy -
Profile in release mode - Debug builds are not representative of performance
-
Write 100% idiomatic Rust - All code must follow Rust idioms and conventions. Flag any existing code that isn't idiomatic when you encounter it.
-
Ban narrowing
ascasts on integers -ascasts between integer types of different widths (e.g.i16 as u8,u32 as u8,i128 as i64) are truncating, not saturating. They silently wrap or drop high bits in release builds and are a documented source of data-corruption bugs in this codebase (seehyperdb_api::Row::get_numeric,hyperdb_api_core::types::Numeric::encode_int64, and the Arrow decimal paths). UseTryFrominstead:- Caller can tolerate failure (returns
Option/Result):u8::try_from(x).ok()?→ propagatesNone. - Caller knows it fits by validated invariant:
u8::try_from(x).expect("<reason the invariant holds>")→ panics loudly with the invariant in the message. - Always fits by type algebra (e.g.
i128::to_le_bytes(),u32→i64, same-width signed/unsigned where sign isn't meaningful): keep the direct conversion, noTryFromneeded.
Rationale:
ason integers silently corrupts wire data, scale values, and indices when invariants are ever violated;TryFrommakes every narrowing a named, visible branch in the code. Don't writeas u8,as i32, etc. on an integer unless you can prove the conversion is always lossless by the source type's range (e.g.bool as u8,u8 as u16,i8 as i16,i32 as i64). If in doubt, useTryFrom.When reviewing existing code or fixing bugs, flag and convert any narrowing
ascasts you encounter, even if they aren't the proximate cause of the bug — they're a latent-corruption vector and cheap to fix in the same change. - Caller can tolerate failure (returns
-
Update the per-crate
CHANGELOG.mdfor user-visible crate-level changes. When a PR adds, changes, or removes any public API surface in a publishable crate (hyperdb-api,hyperdb-api-core,hyperdb-compile-check,hyperdb-api-derive,hyperdb-api-node,hyperdb-api-salesforce,hyperdb-bootstrap,hyperdb-mcp,sea-query-hyperdb), append a bullet to the## [Unreleased]section of that crate'sCHANGELOG.mdunder the appropriate Keep a Changelog heading (### Added,### Changed,### Deprecated,### Removed,### Fixed,### Security). Internal refactors that don't change the public API surface do not require a changelog entry.
Which changelog files you may edit — this is the part that trips people up, because CONTRIBUTING.md says contributors do not hand-edit changelogs. Both rules are correct; they govern different files:
- Root
CHANGELOG.md— never hand-edit. It is release-please-generated, has no## [Unreleased]section, and is the onlychangelog-pathinrelease-please-config.json. - The nine per-crate
CHANGELOG.mdfiles — hand-maintained. Each carries exactly one## [Unreleased]section and none appear in release-please'spackagesorextra-files. This reminder applies to these.hyperdb-compile-checkis the ninth: it is published (release.ymldoes so explicitly, since it declares its own[workspace]and--workspacecannot see it) but was missing from this list, which is why it had no changelog until 1.0.0. - The npm sub-package changelogs under
hyperdb-api-node/npm/*/andhyperdb-mcp/npm/*/— leave alone; they have no## [Unreleased]section.
Nothing rolls a per-crate ## [Unreleased] section over into a dated one
when a release ships; that is a manual maintainer step
(docs/GITHUB_OPERATIONS.md → Rolling over the per-crate
changelogs).
So an entry sitting under "Unreleased" does not mean the work is
unshipped — check the root CHANGELOG.md for that. Append your bullet to the
existing section rather than adding a second ### Fixed sibling (MD024).
-
Never invent
hyperdflags or engine parameters. Obtainhyperdviamake download-hyperd(it bootstraps the release pinned inhyperdb-bootstrap/hyperd-version.toml) and start servers through the documented path —HyperProcess::new()in tests, the Makefile targets, orHYPERD_PATHas described above. If you think a startup flag or parameter is needed, confirm it againsthyperd --help, an existing script, or this file before relying on it. Fabricatedhyperdparameters silently fail against the real binary — they have previously made tests hang while appearing to "run." -
Never report a test/build as passing without seeing real output. Check exit codes. If a command produces no output for ~30s, treat it as hanging/failed, not passing, and say so explicitly. A green claim backed by no captured output is a defect, not a result — tests here start a real
hyperdsubprocess (HyperProcess::drop()stops it), so a misconfigured server hangs rather than erroring cleanly. -
Run markdownlint on any Markdown you touch, before committing. It is not a CI gate, so nothing catches these for you — the only feedback is the editor extension, and an agent working headless gets none at all.
npx markdownlint-cli2No arguments: .markdownlint-cli2.jsonc supplies the globs and the path
exclusions. Rules live in .markdownlint.json (shared with the editor
extension); .markdownlintignore exists only because the extension reads it
and markdownlint-cli2 does not.
There is a pre-existing backlog, so a nonzero count is not automatically
yours. Judge new findings against the file's prior state — git show upstream/main:<path> and re-lint — rather than assuming, or you will "fix"
things that were never broken and miss the ones you introduced.
Four traps that have actually bitten:
- Duplicate
### Fixed/### Addedsiblings under one## [Unreleased](MD024). Changelogs here often already have the section further down. Merge your bullet into the existing one instead of adding a second heading — that also keeps Keep a Changelog ordering. - Bare ``` fences (MD040) need a language. Use
textfor command output, ASCII diagrams, error messages, and templates. - Never bulk-auto-fix fences with a naive script. A language-tagged
opening fence does not match a bare-fence test, so the closing fence gets
mistaken for an opening one and tagged — silently turning a terminator into a
new block. This corrupted 176 fences across 22 files once. Any such pass must
track fence state; prefer
markdownlint-cli2 --fix, which is safe, and note that it cannot fix MD040 because choosing a language needs judgement. - A nested
.markdownlint.jsonreplaces the root config rather than merging with it, so a new one mustextendsthe root or every rule there reverts to default. Dropping that line fromdocs/superpowers/.markdownlint.jsonturns 5 findings into 92 — 70 of them the MD060 disabled just below. It hides well: it makes the linter wrong, not the document.
Beware format-on-save: a Markdown formatter reformatting tables to satisfy
MD060 once stripped the README's badge links ([](target) became
) and split an inline link across a newline into two links. MD060
is disabled in .markdownlint.json for exactly this reason.