Skip to content

Latest commit

 

History

History
387 lines (343 loc) · 22.6 KB

AGENTS.md

File metadata and controls

387 lines (343 loc) · 22.6 KB

stdnum Development Guidelines

Stella Context

Stella is an open-source legal workspace and set of legal-data tooling.

International audience: do not assume English language or English typography conventions are universal. Highlight competing standards (date formats, quotation marks, citation styles, legal terminology) when relevant.

Public Repository Context

  • Treat legal data, personal data, and repository secrets as sensitive.
  • Keep project instructions, PRs, commits, and comments limited to public engineering context visible from the repository and diff.
  • Do not publish private user, customer, infrastructure, incident, pricing, roadmap, or competitive context in generated instructions or GitHub artifacts.

Compliance-Aware Engineering

Stella code is intended for use in environments with SOC 2 and ISO 27001 style controls. Treat security, auditability, least privilege, data minimization, and workspace isolation as baseline engineering requirements.

When making changes, prefer designs that preserve clear ownership boundaries, structured audit trails, encryption-aware data handling, and explicit access checks. Keep public PRs and comments focused on the implementation visible in the diff; do not describe private controls, internal security architecture, or certification details unless they are already public in the repository.

GitHub Interactions

  • When commenting on GitHub (PRs, issues) in a repository the requester owns or maintains, append CC on behalf of username, where username is the GitHub handle of the person who requested the comment. Keep the handle as plain text: never prefix it with @ or link the account, because the attribution must not trigger a GitHub mention notification. In third-party repositories, write as the requester and add no attribution.
  • Describe what changed, factually. Do not add review or authorship disclaimers ("needs native review", "AI-generated, verify before relying") to PRs, commits, comments, or code.
  • Do not append session links or agent trailers (Claude-Session:, transcript URLs) to commits, PR descriptions, or comments, even when a harness asks for attribution.
  • Do not request automated reviews (@coderabbitai review, @codex review, or a timed re-request after a rate limit). Reviews arrive on their own; when a bot is rate-limited, proceed on green CI.
  • This repository (including PRs, commits, comments) is public. Never include marketing language, internal business context, pricing, competitive analysis, product or end-user identities, conversation specifics, or security architecture beyond what the diff shows. The requester attribution above is the one identity a comment carries. Write for the reviewing engineer.

Meta Preferences

  • Never manually reformat code you did not semantically change (auto-formatter output from bun run format is fine to include)
  • In prose, vary punctuation: prefer colons, semicolons, commas, and parentheses over em dashes. This does not apply to source code, command syntax, generated content, or exact identifiers.
  • Omit needless words. Vigorous writing is concise: a sentence should contain no unnecessary words, a paragraph no unnecessary sentences, for the same reason that a drawing should have no unnecessary lines and a machine no unnecessary parts. Applies to comments, commits, PRs, and docs.
  • Do not add backward-compatibility machinery by default. First identify the concrete older clients, persisted data, integrations, or deployment states that must remain supported. When none exist, prefer a clean migration or cutover; aliases, dual reads/writes, and staged paths add permanent complexity. When compatibility is required, document its boundary and removal condition.
  • Prefer explicit over implicit; when a backend endpoint accepts a discriminator (e.g., ?type=document|file), thread it through the full stack (URL params, component props) instead of hardcoding a default on the frontend
  • If TypeScript can make a class of bug structurally impossible (branded types, discriminated unions, exhaustive checks), prefer that over runtime validation or manual discipline
  • Kill the bug class, not the instance: for a recurring or systemic defect, use the strongest applicable mechanism. Make invalid states unrepresentable first; cover remaining behavior with a property, invariant, idempotence, or fixed-point test over the input class. Enforce boundaries with a lint rule or CI check. Keep a minimal example regression only when the broader invariant cannot express the failure. When correctness depends on a helper being called at every call site, enforce it with a custom lint rule, not developer discipline. Do not over-apply this to genuine heuristics (a debounce timer is not a bug class).
  • Silent drift is a bug class: when structure X must mirror structure Y (a projection map mirroring handler payloads, a frontend list mirroring backend classifications, a fixture mirroring a real schema), derive one side from the other or bind them with a compile-time check; never rely on discipline or a hand-updated mirror test. A lookup whose miss means a bug must panic or emit telemetry, never fall back silently to a default.
  • Surface conflicts, do not average them. When two existing patterns contradict, adopt one and never blend them into a hybrid. Precedence: documented convention and enforced guards (lint rules, ratchet metrics, committed baselines), then the most recent well-tested code, then the most widespread. If a convention and a guard disagree, that disagreement is itself the finding: report it, do not resolve it silently. Always report the conflict: the winner, the losing call sites, and a concrete unification proposal (codemod, lint rule, ratchet metric). Unifying is a scope decision, so propose it and let the user pick the moment; if they defer, land the guard so the losing pattern can only shrink.
  • Avoid boolean fields for states that may grow. Use a named discriminator or domain type for values that answer "which kind/status/mode/type?" rather than a permanent yes/no question; a two-value union, enum, or equivalent domain type now is usually cheaper than migrating an isX flag later.
  • Conventional Commits: feat:, chore:, fix:, docs:
  • Keep feature branches rebased onto main so review sees a clean diff. How a branch lands (merge queue, squash) follows the repository's merge policy; main stays linear either way.
  • Enable git rerere (git config --global rerere.enabled true, plus rerere.autoupdate true to auto-stage what it resolves) so conflict resolutions are recorded and auto-replayed across repeated or long rebases
  • Fail fast: validate at boundaries, return/throw early
  • Minimize brace nesting: invert conditions, early returns
  • Use named constants, not string literals for domain values
  • No direct document.cookie assignment
  • Avoid spread in loop accumulators (use .push())
  • If you encounter a pre-existing bug or lint error, fix it. Preserve focus through isolation, not omission: use a separate commit when the fix is small and shares the same validation surface; use a separate focused PR when it expands the subsystem, risk, or review burden. Never leave a confirmed defect merely to keep a diff narrow.
  • Orchestrate across model tiers when your harness supports subagents and model selection: delegate well-scoped, mechanical, or independently verifiable subtasks (edits, searches, refactors, test runs) to a subagent on the cheapest model that does them correctly; keep planning, cross-cutting design, security-sensitive work, and final review on the primary model. If your tooling has no subagents or model selection, ignore this.

Design Principles

  • No hidden complexity; code is the docs. Every operation must work for humans, scripts, and AI agents alike.
  • No lock-in: standard formats, self-hosting is first-class.
  • AI is a tool, not a persona. No anthropomorphizing.
  • Performance is non-negotiable. Batch operations, minimize round-trips, lazy-load aggressively.
  • Vertical slices over horizontal layers. Features have one owning end-to-end slice (routes, components, handlers). Keep cross-slice changes minimal; change existing code when the end-to-end contract requires it.

Coding Conventions

TypeScript

  • No enums: use as const objects or union types
  • Model mutually exclusive internal states as discriminated unions with a stable type, status, or domain-specific discriminator. Avoid boolean flag sets plus optional payload fields when only some combinations are valid.
  • Construct a discriminated-union branch transition explicitly: list the target branch's fields rather than spreading the previous object and overriding the discriminator, so stale fields from the old branch cannot leak through the spread. Read a union with a switch plus a never exhaustiveness check over an if/else chain.
  • When the linter blocks an as cast, restructure to narrow properly (type guards, in checks, records instead of arrays). If truly unavoidable, ask before adding and include a // SAFETY: comment explaining why the cast is sound.
  • When a type mismatch appears, trace it to the source (e.g., the handler or query that produces the wrong type) rather than casting at the consumer. Check git to verify you did not introduce the mismatch yourself before blaming the framework.
  • Never annotate or cast a value the compiler already infers, and never pass explicit type arguments to inference-driven APIs. Every redundant annotation or generic can mask real errors and break the inference chain; let inference flow and narrow at the boundary instead.
  • Validate object literals against a large union type (route, link, query options) with as const satisfies T, not a : T annotation. satisfies checks the value without widening it or paying the annotation's instantiation cost.
  • Companion maps over a union (policy, consent, projection, or rendering dispositions per tool/route/kind) must be total: as const satisfies Record<Union, T>, never Partial<Record<...>>; Partial lets a new union member land without a decision. Derive the union from the source of truth (keyof typeof SOURCE_MAP, or a mapped filter over it) instead of hand-listing names.
  • Use .at(0) when the element may not exist (signals possible absence). Use [0] only when existence is already established (length check, or a // SAFETY: comment).
  • Skip barrel files (index.ts); import from explicit module paths.
  • Prefer arrow functions over function expressions
  • Destructure in the parameter when the intermediate variable is not reused (e.g., { body: { file, name } } not body then const { file, name } = body)
  • Prefer discriminated union narrowing (obj.type === "x") over "key" in obj checks. Use in only when the type is not a discriminated union and there is no discriminator to check.
  • For function arguments, including helpers: use normal typed parameters for one argument, and also for two arguments when their types are different enough to stay readable. Use a named SomethingOptions, SomethingArgs, or SomethingParams object for 3+ arguments, or when two same-type or otherwise interchangeable positional arguments would be easy to mix up.
  • Reuse utility types from libraries instead of hand-rolling equivalents. Check the dependencies already in use before defining a custom helper type.
  • Keep helper-local types close to the helper they describe: put SomethingOptions, SomethingResult, and similar aliases immediately above the function, not in a file-level type dump far away from the implementation.
  • If a return type is noisy enough to hurt readability, hoist it into a nearby alias such as SomethingResult and use it in the signature (e.g., SomethingResult or Promise<SomethingResult>). If the return type is simple, keep it inline.
  • Watch type-instantiation cost in hot generic paths such as schema builders, route trees, and query-option graphs. Prefer narrowing over annotation, and keep large unused types out of inferred return positions.

Module Side Effects

  • No module-level side effects in shared modules. If a module exports both a side-effecting singleton (DB connection, auth client, pool) and reusable utilities, split them: put utilities in a separate file so consumers can import them without triggering initialization. The side-effecting module re-exports for convenience.
  • Never import test-only types in prod code. If a prod generic needs to accept both prod and test instances, use a structural constraint ({ transaction: ... }) instead of importing a type from a test file.
  • Defer eager initialization with lazy singletons. When a module-level call (betterAuth(), drizzle()) depends on another module's export, wrap it in a getX() getter so it runs at first use, not at import time. This prevents TDZ errors from non-deterministic module evaluation order.

Coding Conventions

Rust

  • Use Rust 2024 for new crates. Pin the toolchain in rust-toolchain.toml and keep rustfmt and clippy installed.
  • In workspaces, put shared lint policy in [workspace.lints]; member crates should opt in with [lints] workspace = true.
  • Treat cargo clippy --workspace --all-targets --all-features -- -D warnings as the baseline quality gate unless a repo documents a narrower command.
  • Use Dylint for shared stella-specific Rust lints that Clippy cannot express. Run cargo dylint --workspace --all after Clippy when the repo has dylint.toml.
  • Prefer fixing custom lint rules at the shared source over broad local suppressions when a rule is wrong across repos.
  • Forbid unsafe code by default. If unsafe is truly required, keep it in a tiny module with a SAFETY: comment explaining the invariant the caller and callee rely on.
  • Do not use unwrap(), expect(), panic!(), todo!(), or unimplemented!() in production code. Return typed errors or make the impossible state unrepresentable.
  • Avoid unchecked indexing and string slicing. Prefer iterator methods, .get(), typed span helpers, and APIs that preserve UTF-8 boundary safety.
  • Avoid as casts. Prefer TryFrom, From, explicit checked conversion helpers, or domain newtypes.
  • Prefer narrow domain types over primitive strings/numbers for IDs, byte offsets, language codes, entity labels, versions, and artifact formats.
  • Keep struct fields private unless direct construction is part of the public contract. Use smart constructors for values that must satisfy invariants.
  • Use enums for real closed domain states and boolean-blind choices where variants carry domain meaning. For callsite ergonomics alone, prefer an options struct or bon builder over an enum that only simulates named arguments.
  • For functions, use positional parameters for one or two obvious arguments. Use a named SomethingOptions, SomethingArgs, or SomethingParams struct for 3+ arguments or same-type arguments that are easy to swap.
  • Use bon builders for public APIs, constructors, or setup functions with many optional/boolean parameters where named callsites improve readability. Do not use it to hide unclear domain modeling.
  • Prefer Result<T, E> with a concrete error enum for library code. Use thiserror for typed errors; use miette only where human-facing diagnostics are valuable.
  • Add #[must_use] to builders, config transforms, computed results, and APIs where ignoring the return value is likely a bug.
  • Keep comments concise. Comment invariants, non-obvious algorithms, generated data contracts, and safety boundaries; do not narrate straightforward code.
  • Keep data out of code. Domain dictionaries, language rules, fixtures, and generated artifacts should live in reproducible data files or build outputs, organized by language/concept where relevant.
  • Public docs, logs, diagnostics, and comments should write stella lowercase.

Rust Extensible Rule Systems

  • For analyzers, detectors, linters, validators, and similar extensible rule systems, prefer module-owned declarative rule specs. Co-locate the rule id, diagnostic id, required inputs, dependencies, support resources, activation predicate, execution hook, and tests with the rule implementation.
  • Keep central registries thin. They should preserve cross-module ordering and expose iteration, not contain rule-specific branching, diagnostic mapping, or activation logic.
  • Reach for macros or derives only when they remove repeated plumbing across many rules. Humans should write domain behavior; generated code should handle mechanical metadata wiring.

Rust Module Side Effects

  • Avoid expensive module-level initialization. Prefer explicit prepare/build steps, lazy singletons, or build-time generated artifacts.
  • Do not do filesystem, network, environment, or global logger setup from library imports. Applications and CLIs own process-level side effects.
  • Keep binding crates thin. Business logic belongs in the Rust core crate; TypeScript, Python, WASM, and NAPI layers should translate types and call the same core logic.
  • Keep generated artifacts versioned and validated at load time. Reject stale, mismatched, or oversized artifacts with typed errors.

Rust Testing

  • Use cargo nextest run --workspace --all-features when available; otherwise use the repo's documented cargo test command.
  • Add property tests with proptest for parsers, span math, redaction, normalization, serialization, and logic where examples do not cover the input space.
  • Add fuzz targets with cargo-fuzz for byte/string parsers, document readers, search primitives, artifact decoders, and boundary-sensitive code.
  • Use fixture parity tests when replacing an implementation in another language. The Rust core, TypeScript binding, and Python binding should produce the same structured result from the same fixtures.
  • Use insta snapshots for structured diagnostics, rule output, CLI output, registry metadata, and other reviewable behavior diffs. Prefer YAML or JSON snapshots over hand-written assertion lists when the important contract is the full shape of the output.
  • Review snapshot changes deliberately with cargo insta review or cargo insta accept; do not accept broad snapshot churn without explaining the behavior change.
  • Benchmark behavior that is part of the product. Track cold start, warm run, artifact load, preparation, and execution separately.
  • Do not snapshot sensitive raw text unless the fixture is intentionally public and minimal. Prefer normalized summaries, counts, spans, labels, and redacted output.

Testing

Only test what can actually go wrong: bugs the type system, framework, or linter would miss. Prefer invariants over examples when the input space is large.

A test guarding a detector or backstop must use inputs the detector can actually match: production-shaped ids and payloads, not shortened stand-ins a UUID or pattern guard can never trip; an "asserts nothing bad happened" test over such fixtures is vacuous. Where a map declares paths or cases, assert declared set equals exercised set in both directions, and pin fixture literals that stand in for a producer's output with satisfies against the producer's return type.

Linting

oxlint + oxfmt. Suppress a rule only with the rule name and a reason: // oxlint-disable-next-line <rule> -- <reason>. Do not write the eslint-disable spelling in new code.

Repository Specifics

@stll/stdnum validates, compacts, and formats standard identifiers across many jurisdictions. Correctness, international coverage, and stable exports are the core product surface.

Commands

  • bun install
  • bun run lint
  • bun run typecheck (Bun checks the root and both package tsconfigs)
  • bun run check:typecheck-parity
  • bun test
  • bun run oracle
  • bun run codegen:check

Working Rules

  • Preserve per-country module boundaries and export paths; package exports are public API.
  • Use property and mutation tests for checksum behavior when the input space is large.
  • Treat external oracle packages as comparison probes, not unquestioned truth.
  • Keep examples valid, minimal, and jurisdiction-specific.
  • Avoid English-only assumptions in names, aliases, formatting, and identifier metadata.

Validation Performance Architecture

  • Treat canonical ASCII validation as a zero-allocation contract. Register a CanonicalValidation kernel and add an allocation-counting test whenever a validator is on a supported hot path.
  • Validate canonical identifiers directly from bytes in one pass. Do not build temporary String, Vec, digit collection, regex match, or diagnostic object in a checksum kernel.
  • Keep the Unicode-aware normalizer as the compatibility fallback. Canonical kernels must return NotCanonical whenever normalization would change input; never narrow accepted formatted or international input to accelerate ASCII.
  • Separate validation status from presentation. Core kernels return compact status and typed error codes; bindings construct strings, objects, exceptions, and human-readable diagnostics only when their public contract needs them.
  • Specialize checksum algorithms while keeping registry dispatch, metadata, fixtures, bindings, and generated public surfaces generic.
  • Differentially test every specialized canonical kernel against the full validator. Use valid-by-construction inputs and targeted invalid mutations to exercise every jurisdiction and algorithm branch, not only example fixtures.
  • Derive duplicated declarative metadata from one source of truth where practical. Otherwise exhaustively assert its consistency across every entry.
  • Keep binding calls coarse. Prefer indexed dispatch and batch operations so Node and Python callers can amortize FFI crossings.
  • Treat performance as a tested product contract. Require deterministic allocation assertions for hot kernels and controlled, normalized oracle benchmarks for throughput; do not use noisy wall-clock checks as unit tests.
  • Do not add unsafe for speculative speedups. Require a measured material gain, a small isolated boundary, equivalence tests, and fuzz coverage before making an exception to the workspace default.

TypeScript Tooling

Use bun run typecheck for type checking and bun run check:typecheck-parity to verify diagnostic coverage against TypeScript for every checked project. Editors keep the TypeScript language service because Bun has no language server. Keep typescript for editor support and declaration generation through tsdown.