NeoHaskell

repository·main·Indexed 43 days ago

https://github.com/neohaskell/neohaskell

A newcomer-friendly dialect of Haskell focused on productivity and reliable application deployment. It includes the neo CLI for project scaffolding, building, running, and testing, as well as a specialized codemap localization system for rapid API discovery and symbol resolution.

Tokens
167.7K
Snippets
448
Records
698
Agent score
56%

What's inside NeoHaskell

  1. What is NeoHaskell?

    main

    NeoHaskell is a dialect of Haskell designed for beginners and pragmatists. It treats event sourcing as a first-class language feature rather than a library pattern.

    Key characteristics include:

    • Language Primitives: Events, entities, commands, queries, services, and integrations are built into the language.
    • Compiler Guarantees: State transitions are verified at compile time; incorrect transitions will cause compilation errors.
    • Core vs. Shell Model: Developers focus on writing the core (business rules), while NeoHaskell automatically manages the shell (HTTP endpoints, JSON schemas, CLI parsers, event persistence, and audit trails).
  2. Understand the NeoHaskell Monorepo Structure

    main

    NeoHaskell is organized as a monorepo containing several distinct packages. Understanding this structure helps you locate the correct tools and libraries for your development needs:

    • cli: The nhcli package, which provides the neo command-line executable.
    • core: The nhcore package, containing the standard library and core primitives.
    • testbed: The nhtestbed application, used for integration testing (e.g., the Cart example).
    • website: The documentation website built with Astro/Starlight.
  3. What is the Layout library and how to use it

    main

    The Layout library is a beginner-friendly wrapper around prettyprinter used for constructing and rendering formatted text. It uses a Blueprint abstraction to represent a layout plan that is only rendered to Text when explicitly requested.

    Core Mental Model

    1. Construct: Use functions like text, stack, or joinWords to build a Blueprint.
    2. Annotate (Optional): Attach metadata to parts of the layout using withAnnotation.
    3. Render: Convert the Blueprint into Text using render, renderWithWidth, or renderWith (for custom RenderOptions).

    Quick Start

    import Layout (Blueprint)
    import Layout qualified
    
    -- Simplest possible use:
    greeting :: Text
    greeting =
      [Layout.text "Hello,", Layout.text "world!"]
        |> Layout.joinWords
        |> Layout.render
    -- => "Hello, world!"
    
    -- Stacking lines with custom width:
    twoLines :: Text
    twoLines =
      [Layout.text "line one", Layout.text "line two"]
        |> Layout.stack
        |> Layout.renderWithWidth 40
    -- => "line one\nline two"
    import Layout (Blueprint)
    import Layout qualified
    
    greeting :: Text
    greeting =
      [Layout.text "Hello,", Layout.text "world!"]
        |> Layout.joinWords
        |> Layout.render
    -- => "Hello, world!"
  4. Overview of the NeoHaskell Layout Library

    main

    The Layout Library is a beginner-friendly wrapper around the prettyprinter engine, designed to provide structured text layout and automatic line-breaking for Jess. Unlike basic Text primitives, this library allows you to build composable 'layout plans' (blueprints) that can be rendered in a single pass with consistent formatting.

    Key features include:

    • Automatic Line-Breaking: Sensible defaults (e.g., 80 columns) with smart layout decisions.
    • Separation of Concerns: You build layout structures in pure code and only render them to text at the 'edge' of your application.
    • Beginner-Friendly API: Uses plain English verbs and nouns instead of complex operators or low-level prettyprinter internals.

    Common use cases include generating formatted source code for the transpiler, formatting parser diagnostics (with caret pointers and aligned columns), and creating structured CLI output like build summaries and progress tables.

  5. Layout library module structure and types

    main

    The layout library is organized to provide an opaque interface for building and rendering documents.

    Module Placement

    • The library is located in core/layout/.

    Key Abstractions

    • Blueprint annotation: The core opaque type used to represent a layout. Because it is opaque, the internal rendering algorithm or the underlying prettyprinter version can change without breaking your code.
    • Layout.render: The primary function for converting a Blueprint into a rendered format.
    • Layout.append / Layout.stack: Functions used to compose Blueprint values.

    Future Capabilities

    Planned features include Layout.Style presets (e.g., error, warning, hint), domain-specific helpers like Layout.Json or Layout.Tree, and width presets like narrow, standard, or wide.

  6. How codemap regeneration works for contributors

    main

    Codemap regeneration is a maintainer-triggered process used to update documentation artifacts (like codemap/MAP.md and signatures) on a contributor's Pull Request.

    To ensure security, the process is split into two distinct phases:

    1. Generation (Untrusted): A job runs in a restricted environment with contents: read permissions and no access to secrets. It executes ./dev codemap and produces an artifact containing only allowlisted files and a manifest.
    2. Publication (Trusted): A separate job, gated by a GitHub Environment requiring maintainer approval, uses a dedicated token (CODEMAP_PUBLISH_TOKEN) to push the updated files back to the contributor's branch. This job runs no contributor-provided code and treats the PR branch as data only.

    Key Security Constraints:

    • The process only works for PRs where "Allow edits by maintainers" is enabled.
    • It only supports user-owned forks (organization-owned forks are rejected).
    • It will never create a fallback PR; it either updates the existing branch or fails loudly.
    # The generation phase runs this command to produce the codemap
    ./dev codemap
  7. Understand Query Pagination and Result Limits

    main

    NeoHaskell query endpoints have transitioned from returning unbounded JSON arrays to returning a structured QueryPageResponse object. This change introduces pagination via limit and offset parameters to prevent memory exhaustion, network saturation, and denial-of-service vectors.

    Key changes for consumers:

    • Breaking Change: API responses are no longer bare arrays; they are now objects containing metadata.
    • Pagination Parameters: Use limit (max items per page) and offset (starting position) as URL query parameters.
    • Automatic Capping: If a requested limit exceeds the query's defined maxResults or the system's absoluteMaxLimit, the server will silently cap the result set. The actual limit applied is surfaced in the response via effectiveLimit.
  8. Understand the NeoHaskell Integration Pattern personas

    main

    NeoHaskell uses a two-persona integration pattern to separate effectful complexity from domain configuration. This ensures that domain logic remains pure while allowing for robust interactions with external systems.

    • Integration Developer (Nick): Responsible for building the 'black box' integration packages (e.g., integration-sendgrid, integration-stripe). They handle the complex, effectful logic including HTTP, retries, circuit breakers, and authentication.
    • Integration User (Jess): Responsible for configuring the integrations within their specific application. They only write pure mapping functions and do not interact with Task, Http, or error handling logic directly.
  9. Implement per-command access control using `canAccess`

    main

    NeoHaskell allows you to define specific authentication and authorization policies for individual commands, rather than applying a global authentication requirement to all endpoints. This follows the same pattern used for Queries.

    Mental Model

    • Whitelist by default: If you do not define a canAccess function for a command, it defaults to authenticatedAccess, meaning it requires a valid JWT.
    • Colocation: The canAccess declaration should be placed in the same module as your command's decide logic.
    • Error Types: Access control failures return a CommandAuthError, which uses the following constructors:
      • Unauthenticated: No valid token provided.
      • Forbidden: Token is valid, but the user is not allowed to perform this action.
      • InsufficientPermissions [Text]: The user lacks one or more required permissions (the list contains the missing permission names).

    Available Permission Helpers

    You can use these helpers to define your canAccess logic:

    • publicAccess: Allows anyone (including anonymous users) to execute the command.
    • authenticatedAccess: Requires a valid JWT.
    • requirePermission <permission_name>: Requires a specific permission.
    • requireAnyPermission [<permission_names>]: Requires at least one of the listed permissions.
    • requireAllPermissions [<permission_names>]: Requires all of the listed permissions.
    -- Example: A public command for feedback
    canAccess = publicAccess
    
    decide :: FeedbackData -> CommandResult
    decide data = ...
    
    -- Example: An admin-only command
    canAccess = requirePermission "admin:delete"
    
    decide :: AccountId -> CommandResult
    decide id = ...
  10. Understand the per-stream subscription connection lifecycle

    main

    NeoHaskell manages per-stream subscriptions (using PostgreSQL LISTEN) by tying the lifetime of a database connection to a SubscriptionId.

    When a stream is subscribed to, a connection is allocated. When the subscription is removed (via unsubscribe or system teardown), the connection is automatically released through a single cleanup path in removeSubscription. This prevents connection leaks during high-churn subscribe/unsubscribe cycles.

    Key Lifecycle Behaviors:

    • Single Funnel: All removal routes (explicit unsubscription or system close-time teardown) go through removeSubscription to ensure a single release path.
    • Best-Effort Cleanup: If a connection fails to release, the error is logged as a warn, but the unsubscription proceeds. This prevents cleanup failures from blocking the unsubscription process.
    • Idempotent Removal: Calling removeSubscription multiple times for the same ID is safe; the first call executes the cleanup, and subsequent calls find no remaining cleanup task and return unit without error.
  11. Understand command authentication error mapping

    main

    The Web transport maps CommandAuthError (which is an alias for QueryAuthError) to standard HTTP status codes.

    ScenarioResulting HTTP Status
    No Authorization header (on publicAccess command)200 (Anonymous context)
    No Authorization header (on default/protected command)401 Authentication required
    Valid JWT, missing required permission403 Insufficient permissions
    Expired/Malformed/Bad Signature JWT401 Authentication failed
    JWKS unavailable503 Service temporarily unavailable
  12. Understand SimpleEventStore persistence format and structure

    main

    When persistent = True, events are stored in a hierarchical directory structure using the JSONL (JSON Lines) format. Each stream is isolated in its own file, following the pattern {EntityName}/{StreamId}.jsonl.

    Directory Structure Example:

    .neo/events/
    ├── Cart/
    │   ├── cart-123.jsonl
    │   └── cart-456.jsonl
    └── User/
        └── user-789.jsonl

    File Format: Each line in a .jsonl file is a single serialized Event Json.Value. This format ensures append-only writes are atomic at the line level and allows for human-readable debugging.

    Example JSONL line:

    {"entityName":"Cart","streamId":"cart-123","event":{"type":"ItemAdded","productId":"prod-1","quantity":2},"metadata":{"eventId":"evt-001","timestamp":"2026-02-19T10:30:00Z","localPosition":0,"globalPosition":42}}