Skip to content

Latest commit

 

History

History
285 lines (249 loc) · 16.1 KB

File metadata and controls

285 lines (249 loc) · 16.1 KB

folio 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), 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.
  • This repository (including PRs, commits, comments) is public. Never include marketing language, internal business context, pricing, competitive analysis, user identities, conversation specifics, or security architecture beyond what the diff shows. 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.
  • 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).
  • 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:
  • Rebase feature branches onto main (linear history)
  • 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 are independent end-to-end slices (own routes, components, handlers). New capabilities land in their own slice; existing code stays untouched.

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.
  • 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.

Error Handling

  • Use better-result for typed error handling. Do not use try-catch for control flow; wrap failable operations with Result instead. Try-catch is only acceptable at boundary layers (top-level request handlers, framework hooks).
  • Split error semantics deliberately: use panic(...) for impossible internal invariants and programmer misuse, TaggedError subclasses for expected business/config/runtime failures, and analytics/logging capture for telemetry-only paths that continue execution.
  • Prefer tagged errors (APIError, TaggedError subclasses) over bare new Error(). Tagged errors carry structured context (status, cause) for error handling and reporting. Every TaggedError must include a message: string field.
  • All errors must be surfaced to the user (toast) or propagated to the caller. Capture errors before throwing (PostHog). Never swallow errors silently.
  • Do not leave ad hoc console.error(...) in product code. Route telemetry-only failures through the shared analytics or logging helpers so observability stays structured.

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. Full conventions in /conventions-testing.

Linting

oxlint (ultracite preset) + oxfmt. To suppress a rule: // eslint-disable-next-line rule-name

Repository Specifics

folio is a Bun-first TypeScript monorepo for browser-based Word-document (.docx) editing. Its published packages have explicit ownership boundaries:

  • @stll/docx-core (packages/docx-core) owns the typed OOXML document model, validation, serialization, legal-source compiler, and portable DOCX projection kernel. The kernel's Rust source lives in crates/docx-kernel; the package exposes its browser binding through @stll/docx-core/projection.
  • @stll/folio-core (packages/core) owns DOCX parsing, ProseMirror integration, framework-neutral editor behavior, and page layout.
  • @stll/folio-react and @stll/folio-vue (packages/react, packages/vue) are thin framework adapters over folio-core.
  • @stll/folio-nuxt (packages/nuxt) provides the SSR-safe Nuxt integration.
  • @stll/folio-agents (packages/agents) provides agent tooling over the public editor contracts.
  • packages/playground and packages/playground-vue are private test applications; they are not published.

Commands

  • bun install
  • bun run build
  • bun run typecheck
  • bun run test
  • bun run test:property
  • bun run lint
  • bun run format:check
  • bun run validate-dist
  • bun run test:interactions
  • bun run test:e2e:vue
  • bun run test:e2e:parity
  • bun run test:differential
  • bun run orient -- path/to/file.ts or bun run orient -- --diff main

Working Rules

  • At the start of an unfamiliar change or regression, run bun run orient on the suspected files or current diff. Use its seam, source relationships, focused tests, required checks, and changeset report as the initial investigation route; verify behavior in code before acting on any advisory result.
  • Preserve upstream attribution. folio is a fork of the Eigenpal docx-editor (see NOTICE.md). NOTICE.md, LICENSE, and the eigenpal / docx-editor attribution comments must stay verbatim; never scrub them.
  • Keep React and Vue public contracts in parity. Run bun run check:parity-contract and bun run check:export-parity when an adapter contract changes.
  • Return minimal data from public APIs; do not export types with no consumer.
  • Keep DOCX projection semantics in crates/docx-kernel. TypeScript bindings may initialize WebAssembly, preserve its versioned result, and translate boundary errors; they must not contain a fallback OOXML parser or duplicate Rust logic.
  • Keep the portable DOCX kernel single-threaded and browser-native: no WASI, workers, shared memory, SharedArrayBuffer, or cross-origin-isolation requirement. Run bun run check:docx-kernel after Rust changes; the gate checks deterministic generation, runtime constraints, and transfer-size budgets.
  • Resolve OOXML elements by namespace URI plus local name, never by a hard-coded prefix. Support Strict and Transitional namespace profiles explicitly, bound ZIP and XML resource use, and preserve package paragraph identifiers as facts rather than treating them as durable application or host-navigation identities.
  • Add a changeset for any published-package src change. Select every affected package, choose the appropriate bump, and add a one-line summary. Use bunx changeset --empty only when the source change intentionally needs no release. The private playground packages need no changeset.
  • Never delete or regenerate bun.lock to apply package version bumps. Run bun scripts/check-lockfile-workspace-versions.ts --write, then bun install --frozen-lockfile. The synchronizer owns cached workspace self-versions; dependency-graph changes belong in an explicit install.

Cursor Cloud specific instructions

Toolchain and standard commands live under ## Repository Specifics### Commands; this section records only non-obvious cloud-VM caveats.

  • Toolchain lives in the user profile, not the base image. Bun 1.3.14 is at ~/.bun/bin; Node is provided by nvm (default v22.22.2). A login shell sources ~/.bashrc and activates both. A plain non-login bash -c falls back to /exec-daemon/node (Node 22.14).
  • Build and dist validation need Node ≥ 22.18. Use the nvm default Node before running commands that load tsdown configuration.
  • Playwright Chromium is optional locally and required for interaction, visual, and adapter e2e suites. python-docx is optional for differential tests, which skip when it is unavailable.
  • .ai/shared is a submodule. Initialize it before running bun run sync-ai; do not fetch a floating shared revision during normal CI.