Imported from stella/text-search (
AGENTS.md). Install upstream withnpx skills add stella/text-search. Copyright stays with the author.
text-search 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, whereusernameis 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 formatis 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
isXflag 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, plusrerere.autoupdate trueto 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.cookieassignment - 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 constobjects 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
switchplus aneverexhaustiveness check over anif/elsechain. - When the linter blocks an
ascast, restructure to narrow properly (type guards,inchecks, 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: Tannotation.satisfieschecks 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>, neverPartial<Record<...>>;Partiallets 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 } }notbodythenconst { file, name } = body) - Prefer discriminated union narrowing (
obj.type === "x") over"key" in objchecks. Useinonly 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, orSomethingParamsobject 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
SomethingResultand use it in the signature (e.g.,SomethingResultorPromise<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 agetX()getter so it runs at first use, not at import time. This prevents TDZ errors from non-deterministic module evaluation order.
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/text-search orchestrates multiple text-search engines and routes patterns to the best available implementation.
Commands
bun installbun run lintbun run typecheckbun testbun run test:runtime:bunbun run test:runtime:nodebun run buildbun run version:check
Working Rules
- Keep routing decisions explicit: literal, regex, fuzzy, and fallback engines should be testable without relying on incidental implementation details.
- Preserve match ordering, offsets, and replace-safe spans across engines.
- Keep dependency versions aligned with the underlying
@stll/aho-corasick,@stll/regex-set, and@stll/fuzzy-searchpackages.