UNPKG

@eristack/ai-knowledge

Version:

Eristack knowledge pack for AI agents: recommend packages first, load the right Intent skills, and keep catalog facts in sync

4,513 lines • 166 kB
// src/generated/catalog.ts
var catalog = {
  "generatedAt": "2026-11-02T04:57:43.599Z",
  "packages": [
    {
      "name": "@eristack/abac",
      "version": "0.2.4",
      "description": "Attribute-based access control for Eristack: policy functions over subject/resource/environment attributes",
      "slug": "abac",
      "adapters": [
        "backseat",
        "backseat/store",
        "express",
        "nest",
        "react",
        "testing"
      ],
      "skills": [
        {
          "id": "abac-adapters",
          "name": "abac-adapters",
          "packageName": "@eristack/abac",
          "description": "@eristack/abac adapters: express createRequirePolicy, nest AbacModule + AbacGuard + RequirePolicy + AbacContextFactory, react usePolicy. Use when wiring attribute policy checks into HTTP/UI shells.",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/abac#abac-adapters"
        },
        {
          "id": "abac-core",
          "name": "abac-core",
          "packageName": "@eristack/abac",
          "description": "Pure @eristack/abac: createAbac, registerPolicy, evaluate/authorize, attrs helpers \u2014 attribute-based policies (algorithms with arguments \u2192 boolean). Use for per-user limits and scopes (e.g. max book value) beyond boolean RBAC.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/abac#abac-core"
        }
      ]
    },
    {
      "name": "@eristack/address",
      "version": "0.1.1",
      "description": "Normalized postal addresses with ISO country codes \u2014 string fields, no geocoding",
      "slug": "address",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "address-core",
          "name": "address-core",
          "packageName": "@eristack/address",
          "description": "@eristack/address normalized PostalAddress with ISO alpha-2 country codes \u2014 trim, formatAddressOneLine/Lines, isSameCountry. App owns partner tables; no geocoding.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/address#address-core"
        }
      ]
    },
    {
      "name": "@eristack/ai-dev",
      "version": "0.1.8",
      "description": "Unified agent-first dev tooling for Eristack monorepos: plan (token-minimal), check profiles, sync, compact JSON + MCP",
      "slug": "ai-dev",
      "adapters": [
        "repo"
      ],
      "skills": [
        {
          "id": "ai-dev-core",
          "name": "ai-dev-core",
          "packageName": "@eristack/ai-dev",
          "description": "@eristack/ai-dev unified monorepo tooling: eristack plan (token-minimal), eristack check profiles (catalog/pr/full = CI), sync docs/knowledge, MCP dev_plan/dev_check. Use before ad-hoc pnpm script chains or reading every check doc.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-dev#ai-dev-core"
        }
      ]
    },
    {
      "name": "@eristack/ai-ticket-generator",
      "version": "0.1.2",
      "description": "Generate portable maintainer tickets (bugs + suggestions) for every @eristack package \u2014 logs, scenario, fix plan, and agent-ready handoff files",
      "slug": "ai-ticket-generator",
      "adapters": [],
      "skills": [
        {
          "id": "ai-ticket-bug",
          "name": "ai-ticket-bug",
          "packageName": "@eristack/ai-ticket-generator",
          "description": "Generate a portable @eristack bug ticket (logs, scenario, repro, fix plan, agent handoff) as a markdown file the user can send to maintainers. Use when a consumer hits a package bug or wants a fixer-upper file for support.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-ticket-generator#ai-ticket-bug"
        },
        {
          "id": "ai-ticket-suggest",
          "name": "ai-ticket-suggest",
          "packageName": "@eristack/ai-ticket-generator",
          "description": "Turn a user feature idea into a portable @eristack suggestion ticket with feasibility (possible/partial/unlikely/needs-decision) and an implementation sketch for maintainers/agents. Use when a consumer proposes a change.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-ticket-generator#ai-ticket-suggest"
        }
      ]
    },
    {
      "name": "@eristack/ai-workflow",
      "version": "0.1.2",
      "description": "Local-first AI workflow for Eristack projects: MCP server, FTS+vector index, backlog/sprint/ADR artifacts \u2014 low-token agent tools that do not replace existing editors or Intent",
      "slug": "ai-workflow",
      "adapters": [],
      "skills": [
        {
          "id": "ai-workflow-core",
          "name": "ai-workflow-core",
          "packageName": "@eristack/ai-workflow",
          "description": "Local-first @eristack/ai-workflow: .eristack/workflow backlog/sprints/ADR/summary, FTS+vector index, low-token search discipline. Use when scaffolding AI-native project memory or sprint cadence without replacing Intent, git, or editors.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-workflow#ai-workflow-core"
        },
        {
          "id": "ai-workflow-mcp",
          "name": "ai-workflow-mcp",
          "packageName": "@eristack/ai-workflow",
          "description": "Install and use the eristack-workflow MCP server alongside existing MCP tools. Covers Cursor/Claude config, tool inventory, and when to search vs read_chunk. Use when wiring @eristack/ai-workflow into a consumer project.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-workflow#ai-workflow-mcp"
        }
      ]
    },
    {
      "name": "@eristack/api-key",
      "version": "0.1.0",
      "description": "Generate, hash, and timing-safe verify API keys for partner B2B routes",
      "slug": "api-key",
      "adapters": [],
      "skills": [
        {
          "id": "api-key-core",
          "name": "api-key-core",
          "packageName": "@eristack/api-key",
          "description": "@eristack/api-key generateApiKey (prefix_secret + public keyId), hashApiKey (peppered SHA-256), verifyApiKey (constant-time, never throws) \u2014 partner/B2B machine credentials for /partner routes. App owns the api_keys table (keyId + hash, never the key). Guard order: rate-limit \u2192 api-key \u2192 idempotency. Not human login (@eristack/jwt-auth) or OAuth clients (@eristack/oauth/provider).",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/api-key#api-key-core"
        }
      ]
    },
    {
      "name": "@eristack/backseat",
      "version": "0.1.11",
      "description": "Frontend mock backend engine: in-browser REST server with pluggable store, controllers, and TanStack Query hooks",
      "slug": "backseat",
      "adapters": [
        "adapters",
        "client",
        "drizzle",
        "express",
        "nest",
        "ports",
        "react",
        "seeds",
        "store",
        "testing",
        "workshop"
      ],
      "skills": [
        {
          "id": "backseat-core",
          "name": "backseat-core",
          "packageName": "@eristack/backseat",
          "description": "@eristack/backseat: frontend-first in-browser REST engine \u2014 flexible registerRoute controllers, registerAction, splat paths, IndexedDB store, BackseatDevtools. Memory store for tests only. Agents peek at handlers/snapshots when backend is built later.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/backseat#backseat-core"
        }
      ]
    },
    {
      "name": "@eristack/business-calendar",
      "version": "0.1.0",
      "description": "Business days and holidays on YYYY-MM-DD wall dates \u2014 no timestamp import in core",
      "slug": "business-calendar",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "business-calendar-core",
          "name": "business-calendar-core",
          "packageName": "@eristack/business-calendar",
          "description": "@eristack/business-calendar createBusinessCalendar \u2192 isBusinessDay / nextBusinessDay / addBusinessDays on YYYY-MM-DD wall dates, plus normalizeWallDate/addWallDays. Use for due dates, SLA deadlines, and posting-date guards; holidays come from an app table. Not instants (@eristack/timestamp) or fiscal periods (@eristack/fiscal-calendar).",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/business-calendar#business-calendar-core"
        }
      ]
    },
    {
      "name": "@eristack/checksum",
      "version": "0.1.0",
      "description": "SHA-256 hex normalize and constant-time compare for exports and file refs",
      "slug": "checksum",
      "adapters": [],
      "skills": [
        {
          "id": "checksum-core",
          "name": "checksum-core",
          "packageName": "@eristack/checksum",
          "description": "@eristack/checksum sha256Hex, normalizeChecksumHex, checksumEquals \u2014 SHA-256 digests for file refs and exports with constant-time compare. Use when storing or verifying a checksum; not for password/API-key hashing or hash-chained ledgers.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/checksum#checksum-core"
        }
      ]
    },
    {
      "name": "@eristack/command-palette",
      "version": "0.1.0",
      "description": "Headless command palette state and simple dialog shell",
      "slug": "command-palette",
      "adapters": [],
      "skills": [
        {
          "id": "command-palette-core",
          "name": "command-palette-core",
          "packageName": "@eristack/command-palette",
          "description": "@eristack/command-palette useCommandPalette(initialOpen?) \u2192 { open, setOpen, openPalette, closePalette, togglePalette } + CommandPaletteDialog { open, onClose, title, children } (aria-modal shell, backdrop click closes, null when closed, erista-command-palette* hooks). Use for Cmd/Ctrl+K navigation in ERP apps; app supplies commands (Router routes, rbac-filtered), search, arrow keys, Escape, and CSS. No fuzzy search, registry, or focus trap.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/command-palette#command-palette-core"
        }
      ]
    },
    {
      "name": "@eristack/comms",
      "version": "0.1.3",
      "description": "Transactional email, SMS, and WhatsApp \u2014 SendGrid, Postmark, Mailgun, Resend, Twilio, Vonage, Meta drivers, Drizzle delivery log, Express webhooks",
      "slug": "comms",
      "adapters": [
        "drizzle",
        "express",
        "mailgun",
        "meta-whatsapp",
        "nest",
        "postmark",
        "resend",
        "rest",
        "sendgrid",
        "testing",
        "twilio",
        "vonage",
        "zod"
      ],
      "skills": [
        {
          "id": "comms-adapters",
          "name": "comms-adapters",
          "packageName": "@eristack/comms",
          "description": "@eristack/comms/express createCommsRouter \u2014 POST /send, GET /messages/:id, POST /webhooks/:vendor; Twilio x-twilio-webhook-url header.",
          "type": "adapters",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/comms#comms-adapters"
        },
        {
          "id": "comms-core",
          "name": "comms-core",
          "packageName": "@eristack/comms",
          "description": "@eristack/comms createCommsHub \u2014 idempotent email/SMS/WhatsApp sends, vendor drivers, delivery log. Email: SendGrid, Postmark, Mailgun, Resend. Drizzle default; memory drivers tests only.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/comms#comms-core"
        }
      ]
    },
    {
      "name": "@eristack/contact",
      "version": "0.1.0",
      "description": "Contact roles and channel list normalization on a party \u2014 compose with person/phone/email",
      "slug": "contact",
      "adapters": [],
      "skills": [
        {
          "id": "contact-core",
          "name": "contact-core",
          "packageName": "@eristack/contact",
          "description": "@eristack/contact normalizeContactList, primaryContact, CONTACT_ROLES \u2014 validate a party's contact channels (role, personId/phone/email, one isPrimary max) as a JSON value on app-owned partner rows. Normalize phone/email upstream with @eristack/phone / @eristack/email-address.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/contact#contact-core"
        }
      ]
    },
    {
      "name": "@eristack/currency-pair",
      "version": "0.1.0",
      "description": "Base/quote currency pair validation and canonical pair keys \u2014 no FX rates",
      "slug": "currency-pair",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "currency-pair-core",
          "name": "currency-pair-core",
          "packageName": "@eristack/currency-pair",
          "description": '@eristack/currency-pair normalizeCurrencyPair, formatPairKey ("USD/IDR"), invertPair, currencyPairSchema \u2014 validate base/quote against the money registry and key FX rate tables canonically. No rates or conversion here (that is @eristack/money Conversion).',
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/currency-pair#currency-pair-core"
        }
      ]
    },
    {
      "name": "@eristack/data-grid",
      "version": "0.2.6",
      "description": "Dynamic list query primitives: multi-field filters, search mode, multi-sort, offset/cursor pagination for Eristack services and capabilities",
      "slug": "data-grid",
      "adapters": [
        "backseat",
        "backseat/store",
        "client",
        "drizzle",
        "express",
        "nest",
        "react",
        "rest",
        "testing",
        "zod"
      ],
      "skills": [
        {
          "id": "data-grid-adapters",
          "name": "data-grid-adapters",
          "packageName": "@eristack/data-grid",
          "description": "@eristack/data-grid adapters: drizzle executeDrizzleList + columnsFromSource (app owns joins/aggregates; library runs filter/sort/count/page), buildDrizzleQuery, rest createDataGridListAction + {items,pageInfo,query}, express middleware, nest DataGridModule + ParseDataGridPipe, client createDataGridClient, react useDataGridController (draft/commit filter rows) + useDataGridList. Use when wiring list HTTP/SQL/UI shells.",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/data-grid#data-grid-adapters"
        },
        {
          "id": "data-grid-core",
          "name": "data-grid-core",
          "packageName": "@eristack/data-grid",
          "description": "Pure @eristack/data-grid: createDataGrid, parse/serialize JSON search params (TanStack Router\u2013aligned filters/sorts), decimal/money field types for string amount sort/filter without Number(), toSearch/fromSearch, advanced vs search modes, filter ops, multi-sort, offset/cursor pagination, applyInMemory. Use for dynamic list queries without HTTP or Drizzle.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/data-grid#data-grid-core"
        }
      ]
    },
    {
      "name": "@eristack/design-system",
      "version": "0.1.1",
      "description": "Erista design tokens, Tailwind preset, and React density context for ERP UI",
      "slug": "design-system",
      "adapters": [
        "react",
        "tokens.css"
      ],
      "skills": [
        {
          "id": "design-system-core",
          "name": "design-system-core",
          "packageName": "@eristack/design-system",
          "description": "@eristack/design-system Erista tokens (12 --erista-* CSS vars: HSL colour triplets, radius, density gaps) via tokens.css subpath or ERISTACK_CSS_VARS/eristaCssVarMap, Tailwind v3 tailwindPreset (background/foreground/primary/muted/border/destructive, rounded, density spacing), and React DensityProvider/useDensity()/densityClassNames. Load first for any @eristack/ui-* app; shadcn components stay in the app. No typography/shadow tokens; useDensity has no setter.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/design-system#design-system-core"
        }
      ]
    },
    {
      "name": "@eristack/dimension",
      "version": "0.1.0",
      "description": "L\xD7W\xD7H dimension triple as decimal strings \u2014 cubic volume, optional unit label",
      "slug": "dimension",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "dimension-core",
          "name": "dimension-core",
          "packageName": "@eristack/dimension",
          "description": '@eristack/dimension Dimension { length, width, height, unit? } positive decimal strings: normalizeDimension (trim, positive finite, canonical toFixed, unit label trimmed, DimensionParseError code DIMENSION_PARSE_ERROR), dimensionVolume (L\xD7W\xD7H HALF_UP to scale default 6, padded), formatDimension "L \xD7 W \xD7 H unit", zod dimensionSchema. Use for SKU packaging levels, parcel/pallet dims, volumetric weight, bin fit; unit conversion via @eristack/uom in the app. Drizzle numeric columns.',
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/dimension#dimension-core"
        }
      ]
    },
    {
      "name": "@eristack/doc-number",
      "version": "0.3.6",
      "description": "Document number format, parse, and sequence primitives for Eristack",
      "slug": "doc-number",
      "adapters": [
        "backseat",
        "backseat/store",
        "client",
        "drizzle",
        "express",
        "nest",
        "react",
        "rest",
        "testing",
        "zod"
      ],
      "skills": [
        {
          "id": "doc-number-adapters",
          "name": "doc-number-adapters",
          "packageName": "@eristack/doc-number",
          "description": "@eristack/doc-number adapters: drizzle FormatStore + SequenceStore (doc_number_formats / doc_number_sequences), rest format CRUD + preview, express createDocNumberRouter, nest DocNumberModule, client createDocNumberClient, react DocNumberProvider / useDocNumberFormats. Use when persisting formats or wiring format-configuration HTTP/frontend shells; app injects db + docNumber.",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/doc-number#doc-number-adapters"
        },
        {
          "id": "doc-number-core",
          "name": "doc-number-core",
          "packageName": "@eristack/doc-number",
          "description": "Pure @eristack/doc-number: token patterns ({YYYY}/{YY}/{MM}/{DD}/{SEQ:n}), formatDocumentNumber, parseDocumentNumber, createDocNumber, registerFormat, updateFormat, listFormats, getFormatById, next, peekNext, preview, ResetPeriod, FormatStore, SequenceStore, Incrementer, memory stores. Use for document numbers without HTTP or Drizzle.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/doc-number#doc-number-core"
        }
      ]
    },
    {
      "name": "@eristack/doc-shell",
      "version": "0.1.1",
      "description": "Document detail page shell \u2014 header, actions, body slots",
      "slug": "doc-shell",
      "adapters": [],
      "skills": [
        {
          "id": "doc-shell-core",
          "name": "doc-shell-core",
          "packageName": "@eristack/doc-shell",
          "description": "@eristack/doc-shell presentational document page chrome: DocShell { header, actions, children }, DocHeader { title, subtitle, badges }, DocDetailHeading { number, status, context, primary, print, more }, DocActionBar { leading, trailing } with stable erista-doc-* CSS hooks and data-component attributes. Use for invoice/PO/job detail routes (with line-grid, policy-ui gates, multitab tabs). No state, no styles shipped, no pbac logic \u2014 app owns those.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/doc-shell#doc-shell-core"
        }
      ]
    },
    {
      "name": "@eristack/doc-transitions",
      "version": "0.1.1",
      "description": "Preset ERP document status graphs for @eristack/pbac documents.transitions()",
      "slug": "doc-transitions",
      "adapters": [],
      "skills": [
        {
          "id": "doc-transitions-core",
          "name": "doc-transitions-core",
          "packageName": "@eristack/doc-transitions",
          "description": "@eristack/doc-transitions preset status graphs (publication, decision, journal, lock, outstanding) for pbac documents.transitions(). Use instead of copy-paste status tables when wiring ERP document PATCH actions.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/doc-transitions#doc-transitions-core"
        }
      ]
    },
    {
      "name": "@eristack/drizzle",
      "version": "0.1.1",
      "description": "App-owned Drizzle boot (openDrizzle) plus dialect column helpers and Eristack spine table composer for sqlite and Postgres",
      "slug": "drizzle",
      "adapters": [
        "schema"
      ],
      "skills": [
        {
          "id": "drizzle-core",
          "name": "drizzle-core",
          "packageName": "@eristack/drizzle",
          "description": "@eristack/drizzle app-owned boot: openDrizzle({ schema, migrationsFolder, pool }) \u2192 { db, dialect, location, close } with lazy better-sqlite3/pg, APP_DB_DIALECT or DATABASE_URL resolution, migrate per dialect, redacted Postgres URL for health. App calls once \u2014 Eristack table packages never open connections. @eristack/drizzle/schema: dialect column helpers + createEristackSpineTables (3ss B3 step 2).",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/drizzle#drizzle-core"
        }
      ]
    },
    {
      "name": "@eristack/drizzle-kit-helpers",
      "version": "0.1.1",
      "description": "Shared drizzle-kit config fragments for Eristack consumer monorepos (pg prod, sqlite tests)",
      "slug": "drizzle-kit-helpers",
      "adapters": [],
      "skills": [
        {
          "id": "drizzle-kit-helpers-core",
          "name": "drizzle-kit-helpers-core",
          "packageName": "@eristack/drizzle-kit-helpers",
          "description": "@eristack/drizzle-kit-helpers eristackProdPostgresConfig(schema) / eristackTestSqliteConfig(schema) / defineEristackDrizzleConfig({ dialect, schema, out, dbCredentialsEnv?, migrationsFolder? }) \u2014 conventional drizzle-kit configs (postgresql reads DATABASE_URL, sqlite reads SQLITE_URL, separate out folders per dialect) for apps composing Eristack Drizzle tables. Dev-only; URL read from env at call time. Not MySQL.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/drizzle-kit-helpers#drizzle-kit-helpers-core"
        }
      ]
    },
    {
      "name": "@eristack/email-address",
      "version": "0.1.0",
      "description": "Normalized email local@domain strings for contact channels",
      "slug": "email-address",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "email-address-core",
          "name": "email-address-core",
          "packageName": "@eristack/email-address",
          "description": "@eristack/email-address normalizeEmail, parseEmailAddress, emailEquals, emailAddressSchema \u2014 lower-case local@domain normalization at the API boundary so uniqueness and contact lookups are plain string compares. Not SMTP (@eristack/comms) or templates (@eristack/email-template).",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/email-address#email-address-core"
        }
      ]
    },
    {
      "name": "@eristack/email-template",
      "version": "0.1.0",
      "description": "{{var}} HTML/text email template render and key extraction \u2014 pair with @eristack/comms",
      "slug": "email-template",
      "adapters": [],
      "skills": [
        {
          "id": "email-template-core",
          "name": "email-template-core",
          "packageName": "@eristack/email-template",
          "description": "@eristack/email-template renderEmailTemplate(template, vars, { escapeHtml? }) and extractTemplateKeys(template) \u2014 logic-free {{key}} substitution for tenant-editable transactional email (subject/html/text) rendered in an @eristack/outbox worker and sent via @eristack/comms. Missing keys render empty; validate against a per-message-type variable contract on save. Format money/dates in the app before passing vars.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/email-template#email-template-core"
        }
      ]
    },
    {
      "name": "@eristack/entity-id",
      "version": "0.1.0",
      "description": "UUID v7 entity identifiers \u2014 sortable, parseable, Drizzle column helper",
      "slug": "entity-id",
      "adapters": [
        "drizzle",
        "zod"
      ],
      "skills": [
        {
          "id": "entity-id-core",
          "name": "entity-id-core",
          "packageName": "@eristack/entity-id",
          "description": "@eristack/entity-id UUID v7 generate/parse/compare, entityIdToDate, Drizzle entityIdColumn, zod entityIdSchema \u2014 sortable PKs for new ERP tables. Wave 13 E1; no sibling deps.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/entity-id#entity-id-core"
        }
      ]
    },
    {
      "name": "@eristack/epoch",
      "version": "0.1.5",
      "description": "Headless data-version epochs for cache invalidation: compare client epoch vs server, bump on mutation, Drizzle default",
      "slug": "epoch",
      "adapters": [
        "backseat",
        "backseat/store",
        "client",
        "drizzle",
        "express",
        "logger",
        "nest",
        "react",
        "rest",
        "testing",
        "zod"
      ],
      "skills": [
        {
          "id": "epoch-adapters",
          "name": "epoch-adapters",
          "packageName": "@eristack/epoch",
          "description": "Wire @eristack/epoch: Drizzle createEpochTables/createDrizzleEpochStore, Express createEpochRouter, Nest EpochModule, createEpochClient, useEpochCachePolicy and useEpochInvalidation React hooks, registerEpochBackseat for prototypes.",
          "type": "adapters",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/epoch#epoch-adapters"
        },
        {
          "id": "epoch-core",
          "name": "epoch-core",
          "packageName": "@eristack/epoch",
          "description": "@eristack/epoch headless data-version counters: current/bump per scope, compareEpochs use-cache vs refetch, resolveCachePolicy, StaleEpochError. Drizzle default; memory store tests only.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/epoch#epoch-core"
        }
      ]
    },
    {
      "name": "@eristack/feedback",
      "version": "0.1.1",
      "description": "Headless busy lock, action sequence, and optional progress observer for ERP shells",
      "slug": "feedback",
      "adapters": [
        "react"
      ],
      "skills": [
        {
          "id": "feedback-core",
          "name": "feedback-core",
          "packageName": "@eristack/feedback",
          "description": "@eristack/feedback createBusy().run(work, label) nested counter; runFeedbackSequence confirm\u2192busy\u2192callbacks; createProgressObserver for fetch/mutate/navigate signals. React BusyProvider/useBusyState. App owns lock UI and toasts. Pair with @eristack/backseat/express createErrorCatalog for B2 disposition. Use for ERP shell busy lock (3ss B1/B5).",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/feedback#feedback-core"
        }
      ]
    },
    {
      "name": "@eristack/file-manager",
      "version": "0.1.3",
      "description": "Headless file uploads: S3 presigned PUT/GET (inline view by default; optional attachment filename), server uploads, FileRef for Drizzle columns, REST/Express/React dev tools",
      "slug": "file-manager",
      "adapters": [
        "backseat",
        "backseat/store",
        "client",
        "drizzle",
        "express",
        "nest",
        "react",
        "rest",
        "s3",
        "testing",
        "zod"
      ],
      "skills": [
        {
          "id": "file-manager-adapters",
          "name": "file-manager-adapters",
          "packageName": "@eristack/file-manager",
          "description": "@eristack/file-manager adapters: drizzle tables/store, REST + express createFileManagerRouter, client uploadViaPresign, react FileUploadDropzone and FileManagerDevPanel. Use when wiring S3 uploads in API and Vite apps.",
          "type": "adapters",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/file-manager#file-manager-adapters"
        },
        {
          "id": "file-manager-core",
          "name": "file-manager-core",
          "packageName": "@eristack/file-manager",
          "description": "Pure @eristack/file-manager: createFileManager, FileRef JSON for DB columns, presigned upload sessions, server uploadFromServer, resolveDownloadUrl (inline GET by default; pass downloadFilename for S3 attachment disposition), buildObjectKey. S3 via @eristack/file-manager/s3. Memory driver tests only.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/file-manager#file-manager-core"
        }
      ]
    },
    {
      "name": "@eristack/filter-builder",
      "version": "0.1.0",
      "description": "Stub filter chip bar and sheet UI for data-grid list filters",
      "slug": "filter-builder",
      "adapters": [],
      "skills": [
        {
          "id": "filter-builder-core",
          "name": "filter-builder-core",
          "packageName": "@eristack/filter-builder",
          "description": "@eristack/filter-builder v0 chrome for data-grid list filters: FilterChipBar { children } and FilterSheet { open, title, children, footer } (role=dialog, null when closed) with erista-filter-* CSS hooks. Bind to @eristack/data-grid controller draft (filterRows, fields, opsForField, add/update/removeFilterRow, commitFilters, isDirty) and form-ui editors; string values only. No state, no pickers, no focus trap.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/filter-builder#filter-builder-core"
        }
      ]
    },
    {
      "name": "@eristack/financial-ledger",
      "version": "0.2.6",
      "description": "Accounting ledger on hash-chained-ledger keyed by accountId, amounts via @eristack/money",
      "slug": "financial-ledger",
      "adapters": [
        "backseat",
        "backseat/store",
        "drizzle",
        "testing"
      ],
      "skills": [
        {
          "id": "financial-ledger-adapters",
          "name": "financial-ledger-adapters",
          "packageName": "@eristack/financial-ledger",
          "description": "@eristack/financial-ledger/drizzle: createHashChainedLedgerTables + createDrizzleLedgerStore for durable GL chains on Postgres (Vercel).",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/financial-ledger#financial-ledger-adapters"
        },
        {
          "id": "financial-ledger-core",
          "name": "financial-ledger-core",
          "packageName": "@eristack/financial-ledger",
          "description": "@eristack/financial-ledger: createFinancialLedger post/list/snapshot/verify by accountId+currency with @eristack/money. Default store is Drizzle \u2014 memory is tests only.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/financial-ledger#financial-ledger-core"
        }
      ]
    },
    {
      "name": "@eristack/fiscal-calendar",
      "version": "0.1.2",
      "description": "Fiscal years and periods with open/closed flags \u2014 wall-date boundaries on @eristack/timestamp",
      "slug": "fiscal-calendar",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "fiscal-calendar-core",
          "name": "fiscal-calendar-core",
          "packageName": "@eristack/fiscal-calendar",
          "description": "@eristack/fiscal-calendar fiscal years and open/closed periods on @eristack/timestamp wall dates \u2014 findPeriodForDate, assertPeriodOpen, listPeriods. Pair with doc-transitions lockGraph for period close.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/fiscal-calendar#fiscal-calendar-core"
        }
      ]
    },
    {
      "name": "@eristack/form-ui",
      "version": "0.1.1",
      "description": "Native React form controls wired to @eristack money, percent, and timestamp",
      "slug": "form-ui",
      "adapters": [],
      "skills": [
        {
          "id": "form-ui-core",
          "name": "form-ui-core",
          "packageName": "@eristack/form-ui",
          "description": '@eristack/form-ui string-first native inputs: MoneyInput { amount, currency, onAmountChange, onParsed, round } (blur \u2192 submitAmountOnlyFormValue: HALF_EVEN to currency scale, "12.345"\u2192"12.34", no padding, round:false keeps scale, invalid throws ParseError), PercentInput { value, onValueChange }, TimestampWallInput { value YYYY-MM-DD, onValueChange }, SearchableSelect { value, options, onValueChange, loading }, FormField { label, hint, error }. Use for document header fields, master pickers, line-grid cells, filter editors with TanStack Form; values equal API strings (MoneyJSON/decimal/wall). No number inputs, no styles.',
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/form-ui#form-ui-core"
        }
      ]
    },
    {
      "name": "@eristack/fraction",
      "version": "0.1.0",
      "description": "Exact rational numbers as reduced fractions \u2014 string numerators/denominators, no float literals",
      "slug": "fraction",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "fraction-core",
          "name": "fraction-core",
          "packageName": "@eristack/fraction",
          "description": "@eristack/fraction exact rationals as reduced num/den strings \u2014 parse n/d and mixed numbers, exact arithmetic, approximateFraction for irrationals/decimals with max denominator. Not float math.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/fraction#fraction-core"
        }
      ]
    },
    {
      "name": "@eristack/geo",
      "version": "0.1.0",
      "description": "Latitude and longitude as decimal strings \u2014 normalize and haversine distance",
      "slug": "geo",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "geo-core",
          "name": "geo-core",
          "packageName": "@eristack/geo",
          "description": '@eristack/geo GeoPoint { latitude, longitude } decimal strings: normalizeGeoPoint (trim, range check lat \xB190 / lng \xB1180, canonical toFixed, GeoParseError code GEO_PARSE_ERROR), geoDistanceKm (haversine, R=6371, HALF_UP to scale default 3, zero-padded), formatGeoPoint "lat, lng", zod geoPointSchema. Use for depot/site coordinates and radius checks; geocoding, routing, and PostGIS stay in the app. Drizzle numeric(10,7) not double.',
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/geo#geo-core"
        }
      ]
    },
    {
      "name": "@eristack/hash-chained-ledger",
      "version": "0.1.5",
      "description": "Append-only hash-chained ledger primitive: opening/in/out/adjustment/closing, type refs, chain verify and tamper detection",
      "slug": "hash-chained-ledger",
      "adapters": [
        "backseat",
        "backseat/store",
        "drizzle",
        "testing"
      ],
      "skills": [
        {
          "id": "hash-chained-ledger-adapters",
          "name": "hash-chained-ledger-adapters",
          "packageName": "@eristack/hash-chained-ledger",
          "description": "@eristack/hash-chained-ledger/drizzle: createHashChainedLedgerTables + createDrizzleLedgerStore. Use for durable chains on Postgres (Vercel).",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/hash-chained-ledger#hash-chained-ledger-adapters"
        },
        {
          "id": "hash-chained-ledger-core",
          "name": "hash-chained-ledger-core",
          "packageName": "@eristack/hash-chained-ledger",
          "description": "Pure @eristack/hash-chained-ledger: createHashChainedLedger with Drizzle store by default, append/snapshot/verify, balance equation, SHA-256 chain. Memory store is unit tests only.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/hash-chained-ledger#hash-chained-ledger-core"
        }
      ]
    },
    {
      "name": "@eristack/health",
      "version": "0.1.0",
      "description": "Liveness and readiness health check registry with Express and Nest mount helpers",
      "slug": "health",
      "adapters": [
        "express",
        "nest"
      ],
      "skills": [
        {
          "id": "health-core",
          "name": "health-core",
          "packageName": "@eristack/health",
          "description": "@eristack/health createHealthRegistry + registerCheck(name, fn) \u2192 runLiveness / runReadiness with per-check durationMs; aggregateStatus maps ok\u2192200, degraded\u2192503. Express createHealthRouter {liveness, readiness}; Nest HealthModule.forRoot + HEALTH_REGISTRY. Use for /health and /ready probes (Postgres, outbox lag, S3). Checks must be wrapped so they never throw or hang; they run sequentially.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/health#health-core"
        }
      ]
    },
    {
      "name": "@eristack/id-wilayah",
      "version": "0.1.0",
      "description": "Indonesia official admin division codes \u2014 provinces through villages, unit kinds through hamlet, community block, and neighborhood",
      "slug": "id-wilayah",
      "adapters": [
        "bundled",
        "zod"
      ],
      "skills": [
        {
          "id": "id-wilayah-core",
          "name": "id-wilayah-core",
          "packageName": "@eristack/id-wilayah",
          "description": "@eristack/id-wilayah Indonesia official admin division codes (2/4/7/10 digits), bundled registry through villages, unit kinds through hamlet/community block/neighborhood. Use for Indonesian ERP address masters, KTP-style hierarchy pickers, and validating ministry numeric codes \u2014 not geocoding or postal validation alone.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/id-wilayah#id-wilayah-core"
        }
      ]
    },
    {
      "name": "@eristack/idempotency",
      "version": "0.1.1",
      "description": "Idempotency-Key guard with Drizzle store, scoped keys, lease, Express/Nest/client adapters",
      "slug": "idempotency",
      "adapters": [
        "client",
        "drizzle",
        "express",
        "nest",
        "testing",
        "zod"
      ],
      "skills": [
        {
          "id": "idempotency-adapters",
          "name": "idempotency-adapters",
          "packageName": "@eristack/idempotency",
          "description": "@eristack/idempotency adapters: drizzle createIdempotencyTables + createDrizzleIdempotencyStore (production store), express wrapIdempotentHandler({ guard, scopeFromReq }, handler) \u2192 replay 200 / 409 JSON, nest IdempotencyInterceptor + mapIdempotencyError, client createIdempotencyClientFetch (one key per submit intent), zod schemas. Use when wiring the guard into HTTP and the browser; no header means the handler runs unguarded.",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/idempotency#idempotency-adapters"
        },
        {
          "id": "idempotency-core",
          "name": "idempotency-core",
          "packageName": "@eristack/idempotency",
          "description": "@eristack/idempotency createIdempotencyGuard({ store, defaultLeaseMs, waitOnPending }) \u2192 run(key, fn) / runScoped({ scope: { tenantId, scope }, key, requestHash, fn }): atomic claim with lease, run once, replay stored result, 409 IDEMPOTENCY_REQUEST_MISMATCH on different body, IDEMPOTENCY_CONFLICT while pending. Pair with domain UNIQUE(tenant_id, idempotency_key). Drizzle store is production; memory store tests only. Architecture: #idempotency-and-outbox.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/idempotency#idempotency-core"
        }
      ]
    },
    {
      "name": "@eristack/iso-3166",
      "version": "0.1.0",
      "description": "ISO 3166-1 country codes and ISO 3166-2 subdivision normalization \u2014 assigned alpha-2/alpha-3 registry",
      "slug": "iso-3166",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "iso-3166-core",
          "name": "iso-3166-core",
          "packageName": "@eristack/iso-3166",
          "description": "@eristack/iso-3166 assigned ISO 3166-1 alpha-2/alpha-3 and ISO 3166-2 subdivision normalization. Use when validating country codes beyond two-letter format \u2014 not for postal address shape (address) or port codes (unlocode).",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/iso-3166#iso-3166-core"
        }
      ]
    },
    {
      "name": "@eristack/jwt-auth",
      "version": "0.4.7",
      "description": "Canonical JWT access + refresh-token auth primitives for Eristack",
      "slug": "jwt-auth",
      "adapters": [
        "backseat",
        "backseat/store",
        "client",
        "drizzle",
        "express",
        "nest",
        "react",
        "rest",
        "testing",
        "zod"
      ],
      "skills": [
        {
          "id": "jwt-auth-adapters",
          "name": "jwt-auth-adapters",
          "packageName": "@eristack/jwt-auth",
          "description": "@eristack/jwt-auth adapters: drizzle pgsql/mysql/sqlite RefreshTokenStore + CredentialStore (jwt_auth_credentials child of users), headless rest login/ sessions, express createJwtAuthRouter, nest JwtAuthModule JwtAuthGuard, client createJwtAuthClient login, react JwtAuthProvider useJwtAuth. Use when wiring persistence or HTTP/frontend shells.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/jwt-auth#jwt-auth-adapters"
        },
        {
          "id": "jwt-auth-core",
          "name": "jwt-auth-core",
          "packageName": "@eristack/jwt-auth",
          "description": "Pure @eristack/jwt-auth token + credentials lifecycle: createJwtAuth, registerCredentials, login, changePassword, issueTokens, verifyAccessToken, refresh rotation, revoke, CredentialStore, RefreshTokenStore, opaque refresh hashes, family reuse detection. Use when implementing JWT access + refresh and optional username/password without HTTP/DB frameworks.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/jwt-auth#jwt-auth-core"
        }
      ]
    },
    {
      "name": "@eristack/line-grid",
      "version": "0.1.0",
      "description": "Editable QUPS line table with patchLine recalculation hook",
      "slug": "line-grid",
      "adapters": [],
      "skills": [
        {
          "id": "line-grid-core",
          "name": "line-grid-core",
          "packageName": "@eristack/line-grid",
          "description": "@eristack/line-grid useLineGridRecalc(CalculateLineInput) \u2192 { line: CalculatedLine, applyPatch (PatchLineInput \u2192 qups patchLine), recalculate } and LineGrid { line, columns {id, header}, renderCell(id, line) } single-row table with erista-line-grid hook. Use for invoice/PO/job line editors with form-ui cells; truth modes quantity+unitPrice | quantity+subtotal | unitPrice+subtotal; fields subtotal/net/total (no lineTotal). Same calculateLine on server insert. N lines = N grids or own table; keyboard via spreadsheet-operator.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/line-grid#line-grid-core"
        }
      ]
    },
    {
      "name": "@eristack/list-shell",
      "version": "0.1.0",
      "description": "Presentational list page layout, toolbar, and TanStack Query state banners",
      "slug": "list-shell",
      "adapters": [],
      "skills": [
        {
          "id": "list-shell-core",
          "name": "list-shell-core",
          "packageName": "@eristack/list-shell",
          "description": "@eristack/list-shell presentational list page frame: ListPageLayout { toolbar, banner, children }, ListToolbar { leading, children, trailing }, QueryStateBanner { isLoading, isError, isEmpty, messages } (loading\u2192error\u2192empty precedence, role=status/alert) with erista-list-* CSS hooks. Use with @eristack/data-grid/react useDataGridList ({ schema, client }) \u2192 items/pageInfo/controller and filter-builder chips. No fetching, no table, no filter logic.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/list-shell#list-shell-core"
        }
      ]
    },
    {
      "name": "@eristack/local-monorepo-versioning",
      "version": "0.1.1",
      "description": "Headless version.lock.json bump math for private pnpm monorepos without Changesets or npm publish",
      "slug": "local-monorepo-versioning",
      "adapters": [
        "node"
      ],
      "skills": [
        {
          "id": "local-monorepo-versioning-core",
          "name": "local-monorepo-versioning-core",
          "packageName": "@eristack/local-monorepo-versioning",
          "description": "@eristack/local-monorepo-versioning planRelease + version.lock.json schema for private pnpm monorepos without Changesets: root advances one step at the batch's largest bump level, independent package semver, releases[] audit trail, validateLock, Node load/save/apply, eristack-version CLI bump/apply/manifest/validate. Not for npm-published @eristack library repos.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/local-monorepo-versioning#local-monorepo-versioning-core"
        }
      ]
    },
    {
      "name": "@eristack/logger",
      "version": "0.1.1",
      "description": "JSON-lines structured logger with request context and Express/Nest adapters",
      "slug": "logger",
      "adapters": [
        "express",
        "nest"
      ],
      "skills": [
        {
          "id": "logger-core",
          "name": "logger-core",
          "packageName": "@eristack/logger",
          "description": "@eristack/logger JSON-lines logger: createLogger({ name, level default info, context, sink }) \u2192 debug/info/warn(msg, data), error(msg, err, data), child(context); record { level, message, timestamp, name, context, data, error{name,message,stack} }. Express createLoggerMiddleware ({ logger, requestIdHeader x-request-id, resolveContext }) + getRequestLogger(req) logs request.start/finish with status+durationMs; Nest LoggerModule.forRoot + LoggingInterceptor (APP_INTERCEPTOR) adds request.error. Sink defaults console.log or __ERISTACK_LOGGER_SINK__. Server-only; no redaction/transport.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/logger#logger-core"
        }
      ]
    },
    {
      "name": "@eristack/master-detail",
      "version": "0.1.1",
      "description": "Two-pane master list + detail layout for picker flows",
      "slug": "master-detail",
      "adapters": [],
      "skills": [
        {
          "id": "master-detail-core",
          "name": "master-detail-core",
          "packageName": "@eristack/master-detail",
          "description": "@eristack/master-detail MasterDetailLayout { master, detail } \u2014 aside + section split with erista-master-detail__master/__detail CSS hooks for picker and list-then-edit workspaces. Master is usually @eristack/list-shell + data-grid; selection lives in Router search (?selected=), detail in TanStack Query. pickerMasters(rows, currentId) for deactivated master pickers. No selection state, no responsive logic, no className prop in v0.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/master-detail#master-detail-core"
        }
      ]
    },
    {
      "name": "@eristack/money",
      "version": "0.3.6",
      "description": "Money primitives for Eristack",
      "slug": "money",
      "adapters": [
        "client",
        "drizzle",
        "express",
        "nest",
        "react",
        "react/fields",
        "rest",
        "zod"
      ],
      "skills": [
        {
          "id": "money-adapters",
          "name": "money-adapters",
          "packageName": "@eristack/money",
          "description": "Persist and wire @eristack/money: Drizzle SQL columns, REST wire codec, Zod 4 schemas, Express/Nest HTTP, client revive, React form helpers including createAmountOnlyFieldValidators for flat amount strings + shared row currency (QUPS lines). Use when storing prices in SQL, validating API bodies, or mapping flat DB columns vs MoneyJSON.",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/money#money-adapters"
        },
        {
          "id": "money-amounts",
          "name": "money-amounts",
          "packageName": "@eristack/money",
          "description": "Construct Money with strings or minor units, run same-currency arithmetic, totals (Money.sum/min/max/average), percentages (percentOf/plusPercent/minusPercent), ratios, Discount/Markup/Tax/Percent operators, and compare amounts in @eristack/money. Use when creating prices, taxes, discounts, totals, or when an agent reaches for JS number literals for money.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/money#money-amounts"
        },
        {
          "id": "money-ledger",
          "name": "money-ledger",
          "packageName": "@eristack/money",
          "description": "Round at ledger boundaries, allocate without losing cents, convert with app-supplied FX rates, and serialize Money as JSON decimal strings in @eristack/money. Use for invoices, payment splits, multi-currency reporting, Rounding.currencyDefault, allocate, Conversion.of, moneyToJSON.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/money#money-ledger"
        }
      ]
    },
    {
      "name": "@eristack/multitab",
      "version": "0.2.3",
      "description": "Headless multi-tab workspace for React ERP screens \u2014 document tabs, state preservation, Router sync",
      "slug": "multitab",
      "adapters": [
        "react",
        "react/tanstack"
      ],
      "skills": [
        {
          "id": "multitab-core",
          "name": "multitab-core",
          "packageName": "@eristack/multitab",
          "description": "@eristack/multitab: headless multi-tab workspace for React ERP screens \u2014 tab model, closeGuard, TanStack Router sync. UI chrome stays in the app.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/multitab#multitab-core"
        }
      ]
    },
    {
      "name": "@eristack/oauth",
      "version": "0.1.1",
      "description": "OAuth2 client with 17+ IdP drivers (Google, Microsoft, GitHub, Apple, Okta, \u2026) and authorization-server provider \u2014 PKCE, Drizzle, Express; hand off to jwt-auth",
      "slug": "oauth",
      "adapters": [
        "client",
        "drizzle",
        "express",
        "nest",
        "provider",
        "rest",
        "testing",
        "zod"
      ],
      "skills": [
        {
          "id": "oauth-client-core",
          "name": "oauth-client-core",
          "packageName": "@eristack/oauth",
          "description": "@eristack/oauth consumer: createOAuthConsumer, PKCE, 17+ IdP drivers (Google, Microsoft, GitHub, Apple, Okta, \u2026), Drizzle pending store. End at jwt-auth.issueTokens.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/oauth#oauth-client-core"
        },
        {
          "id": "oauth-provider-core",
          "name": "oauth-provider-core",
          "packageName": "@eristack/oauth",
          "description": "@eristack/oauth/provider: registerClient, authorization codes, PKCE token exchange, opaque access tokens for partner APIs \u2014 user must already be logged in via jwt-auth.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/oauth#oauth-provider-core"
        }
      ]
    },
    {
      "name": "@eristack/opinion",
      "version": "0.1.1",
      "description": "Opinionated ERP HTTP route table: document CRUD + PATCH /:id/:action transitions",
      "slug": "opinion",
      "adapters": [
        "express",
        "nest",
        "openapi"
      ],
      "skills": [
        {
          "id": "opinion-core",
          "name": "opinion-core",
          "packageName": "@eristack/opinion",
          "description": "@eristack/opinion ERP HTTP route table on @eristack/rest: options, data-grid, CRUD, PATCH /:id/:action for pbac/doc-transitions. Use when scaffolding document APIs instead of inventing paths per app.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/opinion#opinion-core"
        }
      ]
    },
    {
      "name": "@eristack/outbox",
      "version": "0.1.1",
      "description": "Transactional outbox enqueue + Drizzle worker batch for reliable comms and payment side effects",
      "slug": "outbox",
      "adapters": [
        "drizzle"
      ],
      "skills": [
        {
          "id": "outbox-core",
          "name": "outbox-core",
          "packageName": "@eristack/outbox",
          "description": "@eristack/outbox transactional outbox: createOutbox(store).enqueue({ id, aggregateType, aggregateId, messageType, payloadJson, idempotencyKey }) inside the domain TX (build the Drizzle store on the tx handle), processBatch(limit, handlers) in a worker \u2192 comms/payment/PDF with outbox:${id} keys. Duplicate key returns existing row; failed is terminal until your SQL sweep; one worker per table. Drizzle store production, memory store tests only.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/outbox#outbox-core"
        }
      ]
    },
    {
      "name": "@eristack/payment-instrument",
      "version": "0.1.0",
      "description": "Token-safe payment card value types \u2014 display + gateway refs, PAN transient only, PCI-minded guards",
      "slug": "payment-instrument",
      "adapters": [
        "express",
        "zod"
      ],
      "skills": [
        {
          "id": "payment-instrument-core",
          "name": "payment-instrument-core",
          "packageName": "@eristack/payment-instrument",
          "description": "@eristack/payment-instrument token-safe card/debit display + gateway refs. CardPan is transient; toPersistable for Drizzle. Use before payment-manager or when modeling saved payment methods \u2014 never store PAN/CVV in SQL.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/payment-instrument#payment-instrument-core"
        }
      ]
    },
    {
      "name": "@eristack/payment-manager",
      "version": "0.1.2",
      "description": "Headless payment intents: Stripe/Xendit drivers, Drizzle history, webhooks, REST/Express/client \u2014 pairs with payment-instrument",
      "slug": "payment-manager",
      "adapters": [
        "backseat",
        "client",
        "drizzle",
        "express",
        "nest",
        "react",
        "rest",
        "stripe",
        "testing",
        "xendit",
        "zod"
      ],
      "skills": [
        {
          "id": "payment-manager-adapters",
          "name": "payment-manager-adapters",
          "packageName": "@eristack/payment-manager",
          "description": "@eristack/payment-manager adapters: drizzle tables/store, express createPaymentManagerRouter, stripe/xendit drivers, client, react hooks, backseat.",
          "type": "adapters",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/payment-manager#payment-manager-adapters"
        },
        {
          "id": "payment-manager-core",
          "name": "payment-manager-core",
          "packageName": "@eristack/payment-manager",
          "description": "Pure @eristack/payment-manager: createPaymentManager, PaymentDriver, idempotency, webhook handleWebhook, Money JSON amounts. Memory driver tests only.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/payment-manager#payment-manager-core"
        }
      ]
    },
    {
      "name": "@eristack/pbac",
      "version": "0.2.3",
      "description": "Policy-based (software) access control for Eristack: business document rules that return true or false",
      "slug": "pbac",
      "adapters": [
        "backseat",
        "backseat/store",
        "express",
        "nest",
        "react",
        "testing"
      ],
      "skills": [
        {
          "id": "pbac-adapters",
          "name": "pbac-adapters",
          "packageName": "@eristack/pbac",
          "description": "@eristack/pbac adapters: express createRequireBusinessPolicy (409 on deny), nest PbacModule + PbacGuard + RequireBusinessPolicy, react useBusinessPolicy. Use when wiring document software policies into HTTP/UI shells.",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/pbac#pbac-adapters"
        },
        {
          "id": "pbac-core",
          "name": "pbac-core",
          "packageName": "@eristack/pbac",
          "description": "Pure @eristack/pbac: createPbac, registerPolicy, check/authorize, documents helpers \u2014 software/business policies over document state (usually not per-user). Use for rules like PO outstanding must be > 0 before goods receipt.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/pbac#pbac-core"
        }
      ]
    },
    {
      "name": "@eristack/pdf-render",
      "version": "0.1.0",
      "description": "HTML to PDF driver interface \u2014 Puppeteer/Playwright stays in the app or optional adapter",
      "slug": "pdf-render",
      "adapters": [],
      "skills": [
        {
          "id": "pdf-render-core",
          "name": "pdf-render-core",
          "packageName": "@eristack/pdf-render",
          "description": "@eristack/pdf-render createPdfRenderer(driver).render({ html, title? }) \u2192 { bytes, contentType } behind a PdfRenderDriver seam; createStubPdfDriver for tests/Backseat (not a valid PDF). App owns the engine (Puppeteer singleton or Gotenberg HTTP). Use for invoice/delivery-note PDFs rendered in an @eristack/outbox worker and stored via @eristack/file-manager.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/pdf-render#pdf-render-core"
        }
      ]
    },
    {
      "name": "@eristack/percent",
      "version": "0.1.2",
      "description": "Percent and basis-point ratios as strings \u2014 tax, discount, markup without float literals",
      "slug": "percent",
      "adapters": [
        "react",
        "zod"
      ],
      "skills": [
        {
          "id": "percent-core",
          "name": "percent-core",
          "packageName": "@eristack/percent",
          "description": "@eristack/percent ratio strings, basis points, percentOf/plus/minus for tax and discounts without float literals. Use before @eristack/money rounding at boundaries.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/percent#percent-core"
        }
      ]
    },
    {
      "name": "@eristack/person",
      "version": "0.1.0",
      "description": "Structured person name and gender identity \u2014 normalize and display, not HRIS",
      "slug": "person",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "person-core",
          "name": "person-core",
          "packageName": "@eristack/person",
          "description": '@eristack/person Person { name { given, family, middle?, prefix?, suffix? }, gender?, genderOther? }: normalizePerson / normalizePersonName (trim, required given+family, genderOther iff gender "other", PersonParseError code PERSON_PARSE), GENDER_IDENTITIES [unknown, woman, man, non_binary, prefer_not_to_say, other], normalizeGenderIdentity ("Non-Binary" \u2192 non_binary), formatPersonDisplay "Prefix Given Middle Family Suffix", formatPersonSortable "Family Suffix, Given Middle", zod personSchema. Use for contact/employee rows with structured Drizzle columns; compose with phone/email/contact in the handler \u2014 no sibling imports. Not org names or HRIS.',
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/person#person-core"
        }
      ]
    },
    {
      "name": "@eristack/phone",
      "version": "0.1.0",
      "description": "E.164 phone normalization \u2014 strict plus prefix, no libphonenumber in core",
      "slug": "phone",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "phone-core",
          "name": "phone-core",
          "packageName": "@eristack/phone",
          "description": '@eristack/phone normalizeE164, isValidE164, e164PhoneSchema, branded E164Phone \u2014 strict "+CC\u2026" normalization at the API boundary for contacts and @eristack/comms SMS/WhatsApp. No country inference or libphonenumber; national-number forms add the dial code in the app/UI.',
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/phone#phone-core"
        }
      ]
    },
    {
      "name": "@eristack/policy-ui",
      "version": "0.1.0",
      "description": "RBAC and PBAC gate components with v0 allowed override",
      "slug": "policy-ui",
      "adapters": [],
      "skills": [
        {
          "id": "policy-ui-core",
          "name": "policy-ui-core",
          "packageName": "@eristack/policy-ui",
          "description": "@eristack/policy-ui Can { permission, allowed, fallback } and BusinessPolicyGate { policyId, allowed, fallback } \u2014 React gates that render children or fallback from a boolean. Use for action buttons on document pages/list toolbars; allowed comes from rbac useCan, pbac useBusinessPolicy, or server allowedActions. v0: allowed defaults true, ids are labels only, hidden is not enforcement.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/policy-ui#policy-ui-core"
        }
      ]
    },
    {
      "name": "@eristack/qups",
      "version": "0.3.5",
      "description": "Quantity / unit price / subtotal (QUPS) with 2-of-3 sources of truth, plus modifiers and tax \u2014 business line pricing on @eristack/money",
      "slug": "qups",
      "adapters": [
        "backseat",
        "backseat/store",
        "drizzle",
        "testing"
      ],
      "skills": [
        {
          "id": "qups-adapters",
          "name": "qups-adapters",
          "packageName": "@eristack/qups",
          "description": "Optional @eristack/qups/drizzle: qupsLineColumns injected into app detail tables; withQupsColumns from calculateLine for inserts. Profile/line stores only if you need a field catalog \u2014 everyday form/BE math uses calculateLine.",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/qups#qups-adapters"
        },
        {
          "id": "qups-core",
          "name": "qups-core",
          "packageName": "@eristack/qups",
          "description": "Pure @eristack/qups business calculator: calculateLine / patchLine (plain strings for TanStack Form + BE), Qups 2-of-3 SoT, QUPS_TRUTH_MODES, isQupsTruthMode, PricingLine, modifiers, tax. Prefer calculateLine over inventing float qty/price math in UI or SQL.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/qups#qups-core"
        },
        {
          "id": "qups-line",
          "name": "qups-line",
          "packageName": "@eristack/qups",
          "description": "@eristack/qups calculateLine/patchLine/withQupsColumns for form recalculation and BE insert; PricingLine when you already have Money. Use for invoice/order lines in the business layer \u2014 not float math in React.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/qups#qups-line"
        }
      ]
    },
    {
      "name": "@eristack/rate-limit",
      "version": "0.1.0",
      "description": "Fixed-window in-memory rate limiter \u2014 Redis adapter in app or later package",
      "slug": "rate-limit",
      "adapters": [],
      "skills": [
        {
          "id": "rate-limit-core",
          "name": "rate-limit-core",
          "packageName": "@eristack/rate-limit",
          "description": "@eristack/rate-limit createRateLimiter({ windowMs, max }).check(key, nowMs?) \u2192 { allowed, limit, remaining, resetAt } \u2014 fixed-window, in-process limiter for single-instance APIs, dev, and tests; first guard on partner routes before @eristack/api-key. Per-process counters: implement the same RateLimiter contract over Redis for multi-instance production.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/rate-limit#rate-limit-core"
        }
      ]
    },
    {
      "name": "@eristack/rbac",
      "version": "0.2.4",
      "description": "Role-based access control for Eristack: subjects, roles, and boolean permissions",
      "slug": "rbac",
      "adapters": [
        "backseat",
        "backseat/store",
        "drizzle",
        "express",
        "nest",
        "react",
        "testing"
      ],
      "skills": [
        {
          "id": "rbac-adapters",
          "name": "rbac-adapters",
          "packageName": "@eristack/rbac",
          "description": "@eristack/rbac adapters: drizzle createRbacTables + createDrizzleRbacStore (pgsql/mysql/sqlite), express createRequirePermission, nest RbacModule + RbacGuard + RequirePermission, react useCan. Use when wiring RBAC persistence or HTTP/UI shells.",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/rbac#rbac-adapters"
        },
        {
          "id": "rbac-core",
          "name": "rbac-core",
          "packageName": "@eristack/rbac",
          "description": "Pure @eristack/rbac: createRbac, definePermission, defineRole, assignRole, grantPermission, can/canAny/canAll/authorize \u2014 boolean role-based permissions hanging off app subjects. Use for who-can-do-what without attributes or document policies.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/rbac#rbac-core"
        }
      ]
    },
    {
      "name": "@eristack/rest",
      "version": "0.1.3",
      "description": "Declarative REST route definitions with Express and Nest mounting and OpenAPI 3.1 emit",
      "slug": "rest",
      "adapters": [
        "express",
        "nest"
      ],
      "skills": [
        {
          "id": "rest-core",
          "name": "rest-core",
          "packageName": "@eristack/rest",
          "description": '@eristack/rest declarative route table: defineRoutes([{ method, path "/orders/:id", handler(ctx { params, query, body, headers }) \u2192 { status, body?, headers? }, summary, tags }]) \u2192 router.dispatch() for tests; mountExpressRest / createExpressRestMiddleware (Express 5, unmatched \u2192 next) / createExpressRestRouter (Express 4); RestModule.forRoutes (Nest catch-all, 404 JSON); toOpenApiDocument + mergeOpenApiDocuments (3.1 paths only). First-match, :param only, no middleware \u2014 auth/logging/idempotency mount before it. Prefer @eristack/opinion for ERP docs.',
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/rest#rest-core"
        }
      ]
    },
    {
      "name": "@eristack/rounding-policy",
      "version": "0.1.0",
      "description": "Named rounding profiles that resolve to @eristack/money Rounding operators",
      "slug": "rounding-policy",
      "adapters": [],
      "skills": [
        {
          "id": "rounding-policy-core",
          "name": "rounding-policy-core",
          "packageName": "@eristack/rounding-policy",
          "description": "@eristack/rounding-policy createRoundingPolicyRegistry \u2192 roundingFor({ policyId, currency }) resolves named company rounding rules (invoice, tax, payroll) with per-currency overrides to @eristack/money Rounding operators. Use to round once at posting instead of scale/mode literals in services. Math stays in money; qups/tax outputs are unrounded until this is applied.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/rounding-policy#rounding-policy-core"
        }
      ]
    },
    {
      "name": "@eristack/spreadsheet-operator",
      "version": "0.1.1",
      "description": "Headless spreadsheet keyboard operator \u2014 active-grid scope, cell navigation, Excel-like Enter/Tab editing",
      "slug": "spreadsheet-operator",
      "adapters": [
        "react"
      ],
      "skills": [
        {
          "id": "spreadsheet-operator-adapters",
          "name": "spreadsheet-operator-adapters",
          "packageName": "@eristack/spreadsheet-operator",
          "description": "@eristack/spreadsheet-operator/react: SpreadsheetScopeProvider { config, onCommit({ gridId, address, fieldKey, value }), deactivateOnOutsidePointerDown } (window keydown + outside click deactivate), SpreadsheetTable { descriptor } (role grid), SpreadsheetNavCell { address } (role gridcell, data-active / data-editing / aria-selected, roving tabIndex), SpreadsheetTextCell { address, value, onCommit }, useSpreadsheetGrid + SpreadsheetGridIdProvider for div grids, useSpreadsheetCellEditor({ address, readValue, writeValue, onCommit, onCancel }) to bridge form-ui MoneyInput or a Select. Style via data-spreadsheet-active / data-active; no CSS ships. Use when wiring keyboard grids in React ERP screens.",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/spreadsheet-operator#spreadsheet-operator-adapters"
        },
        {
          "id": "spreadsheet-operator-core",
          "name": "spreadsheet-operator-core",
          "packageName": "@eristack/spreadsheet-operator",
          "description": "@eristack/spreadsheet-operator headless Excel-like keyboard machine: createSpreadsheetOperator(config) with registerGrid({ id, rowCount, colCount, cellAt \u2192 { kind editable|select|display|readonly, fieldKey } }), dispatch/handleKeyDown, state inactive \u2192 active \u2192 editing, effects startEdit/commit { fieldKey }/cancel, getNextEditableAddress. Defaults: Tab wraps, arrows stop at edges, Enter edits then commits and moves down, type-to-edit, arrows in edit move the caret. Use for in-browser grids with one active grid per scope; commit \u2192 qups patchLine in the app. Not xlsx export (spreadsheet-render) and not HTTP lists (data-grid).",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/spreadsheet-operator#spreadsheet-operator-core"
        }
      ]
    },
    {
      "name": "@eristack/spreadsheet-render",
      "version": "0.1.0",
      "description": "Declarative workbook model and xlsx/csv render drivers \u2014 ExcelJS/SheetJS in app or adapter",
      "slug": "spreadsheet-render",
      "adapters": [],
      "skills": [
        {
          "id": "spreadsheet-render-core",
          "name": "spreadsheet-render-core",
          "packageName": "@eristack/spreadsheet-render",
          "description": '@eristack/spreadsheet-render workbookFromRows(sheet, columns, string[][]) \u2192 SpreadsheetWorkbook; createSpreadsheetRenderer(driver).renderWorkbook(wb, "csv" | "xlsx") \u2192 { bytes, contentType }. Stub driver emits real RFC 4180 CSV (xlsx is a marker); wrap ExcelJS/SheetJS behind the same SpreadsheetRenderDriver for .xlsx. Use for data-grid "Export" and report downloads; cells stay strings (money amounts, IDs). Output only \u2014 not import, not PDF.',
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/spreadsheet-render#spreadsheet-render-core"
        }
      ]
    },
    {
      "name": "@eristack/stock-movement",
      "version": "0.1.4",
      "description": "Inventory quantity ledger on hash-chained-ledger: locationId, lotId, composable locations, snapshots, tamper checks",
      "slug": "stock-movement",
      "adapters": [
        "backseat",
        "backseat/store",
        "drizzle",
        "testing"
      ],
      "skills": [
        {
          "id": "stock-movement-adapters",
          "name": "stock-movement-adapters",
          "packageName": "@eristack/stock-movement",
          "description": "@eristack/stock-movement/drizzle: re-exports createHashChainedLedgerTables + createDrizzleLedgerStore for Postgres on Vercel. Use as the app default store.",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/stock-movement#stock-movement-adapters"
        },
        {
          "id": "stock-movement-core",
          "name": "stock-movement-core",
          "packageName": "@eristack/stock-movement",
          "description": "@eristack/stock-movement: locationIdFromParts, createStockMovement append/snapshot/verify on hash-chained qty ledger (lotId, optional ownerId). Default store is Drizzle \u2014 never createMemoryLedgerStore in apps.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/stock-movement#stock-movement-core"
        }
      ]
    },
    {
      "name": "@eristack/tax",
      "version": "0.1.1",
      "description": "Tax code registry and effective-dated rates \u2014 math via @eristack/money Tax ops",
      "slug": "tax",
      "adapters": [],
      "skills": [
        {
          "id": "tax-core",
          "name": "tax-core",
          "packageName": "@eristack/tax",
          "description": "@eristack/tax createTaxRegistry \u2192 resolveTaxRate({ code, asOf }) for effective-dated schedules, or buildTaxCatalog / resolveTaxRateFromCatalog / listTaxCodesForPick for flat app masters; applyTaxToAmount(net, rate) returns the unrounded TAX PORTION via @eristack/money Tax.onExclusive. Snapshot the resolved rate on the line. Jurisdiction rules and rounding stay outside.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/tax#tax-core"
        }
      ]
    },
    {
      "name": "@eristack/timestamp",
      "version": "0.1.5",
      "description": "Business timestamps: UTC instants for facts, wall-clock for schedules (DST-safe)",
      "slug": "timestamp",
      "adapters": [
        "client",
        "drizzle",
        "express",
        "nest",
        "react",
        "react/fields",
        "rest",
        "zod"
      ],
      "skills": [
        {
          "id": "timestamp-adapters",
          "name": "timestamp-adapters",
          "packageName": "@eristack/timestamp",
          "description": "@eristack/timestamp adapters (mirror money): Drizzle SQL columns, REST wire codec, Zod 4, Express/Nest HTTP, client revive, React form helpers. Use when persisting instants or wall times in SQL or validating API bodies.",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/timestamp#timestamp-adapters"
        },
        {
          "id": "timestamp-core",
          "name": "timestamp-core",
          "packageName": "@eristack/timestamp",
          "description": "Business timestamps with @eristack/timestamp: instant mode (UTC facts + IANA zone for local dates) and wall mode (local intent, DST-safe schedules). Use for transaction_date, posted_at, due_at, appointments \u2014 not raw Date timezone math.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/timestamp#timestamp-core"
        }
      ]
    },
    {
      "name": "@eristack/unlocode",
      "version": "0.1.0",
      "description": "UN/LOCODE port and place codes \u2014 normalize five-character locodes with ISO 3166 country validation",
      "slug": "unlocode",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "unlocode-core",
          "name": "unlocode-core",
          "packageName": "@eristack/unlocode",
          "description": "@eristack/unlocode UN/LOCODE normalization for ports and trade locations. Depends on @eristack/iso-3166 for country prefix. Use for B/L, forwarding, and logistics locode fields \u2014 not for tenant port masters or full UN datasets.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/unlocode#unlocode-core"
        }
      ]
    },
    {
      "name": "@eristack/uom",
      "version": "0.1.1",
      "description": "Unit of measure quantities with fixed-ratio conversion \u2014 string decimal amounts, no silent float math",
      "slug": "uom",
      "adapters": [
        "zod"
      ],
      "skills": [
        {
          "id": "uom-core",
          "name": "uom-core",
          "packageName": "@eristack/uom",
          "description": "@eristack/uom fixed-ratio unit conversion with string decimal amounts \u2014 kg/g/L/pcs and custom units. Use for inventory qty before qups or stock-movement, not float math.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/uom#uom-core"
        }
      ]
    },
    {
      "name": "@eristack/valuations",
      "version": "0.2.5",
      "description": "Product/lot cost valuation: FIFO, LIFO, FEFO, moving/weighted average, standard cost, specific ID, HIFO/LOFO \u2014 with hash-chained cost ledger",
      "slug": "valuations",
      "adapters": [
        "backseat",
        "backseat/store",
        "drizzle",
        "testing"
      ],
      "skills": [
        {
          "id": "valuations-adapters",
          "name": "valuations-adapters",
          "packageName": "@eristack/valuations",
          "description": "@eristack/valuations/drizzle: createHashChainedLedgerTables + createDrizzleLedgerStore + createValuationLayerTables + createDrizzleLayerStore. Both stores required for production engines on Postgres (Vercel).",
          "type": "adapter",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/valuations#valuations-adapters"
        },
        {
          "id": "valuations-core",
          "name": "valuations-core",
          "packageName": "@eristack/valuations",
          "description": "@eristack/valuations: FIFO/LIFO/FEFO/HIFO/LOFO/movingAverage/weightedAverage/ standardCost/specificIdentification with dual qty/value hash chains. Default stores are Drizzle ledger + Drizzle layers \u2014 memory is tests only.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/valuations#valuations-core"
        }
      ]
    },
    {
      "name": "@eristack/vercel-adapters",
      "version": "0.1.0",
      "description": "Serverless-friendly Express entry helpers for Vercel \u2014 no Vercel SDK in core",
      "slug": "vercel-adapters",
      "adapters": [],
      "skills": [
        {
          "id": "vercel-adapters-core",
          "name": "vercel-adapters-core",
          "packageName": "@eristack/vercel-adapters",
          "description": "@eristack/vercel-adapters createVercelExpressHandler(app) as the single Vercel Node function default export + defaultVercelDeployNotes (60s maxDuration, ~4.5MB body, singleton/lazy-pool cold-start rules). Use when deploying an Express + Drizzle Eristack API to Vercel: one rewrite to the function, module-scope app and pool, outbox via cron route, uploads via presigned S3. No Vercel SDK.",
          "type": "core",
          "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/vercel-adapters#vercel-adapters-core"
        }
      ]
    }
  ]
};

// src/generated/local-skills.ts
var localSkills = [
  {
    "id": "agent-workflow",
    "name": "agent-workflow",
    "packageName": "@eristack/ai-knowledge",
    "description": "Agent workflow for @eristack: four design targets (cheap tokens, predictable, reliable, clear boundaries \u2014 consumers must not reinvent exports), recommend first, install and load skills before coding, prefer examples. Contributor section: HARD RULE docs+skills+recipes + pnpm knowledge:sync every iteration. Use for multi-package app work or monorepo contributions.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#agent-workflow"
  },
  {
    "id": "ai-toolbox",
    "name": "ai-toolbox",
    "packageName": "@eristack/ai-knowledge",
    "description": "Practical AI agent toolbox for Eristack: feature-brief prompts, skill-load order, money/auth/doc-number guardrail checklists, and recipe-authoring template for keeping @eristack/ai-knowledge discoverable. Use when briefing agents, reviewing plans, or adding recipes after new package capabilities.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#ai-toolbox"
  },
  {
    "id": "architecture-recommend",
    "name": "architecture-recommend",
    "packageName": "@eristack/ai-knowledge",
    "description": "Recommend the canon app architecture for Eristack/Erista-style products: TypeScript, Express or NestJS, Drizzle (Postgres production / SQLite tests), mandatory presentation-business-persistence separation, React + Vite + Tailwind + shadcn, TanStack Router (file-based) + Query + Form + Intent, Zustand for client state, typed API contracts, pnpm monorepo when possible. Use when scaffolding a new app, choosing stack, structuring folders, or when the user asks how to architect a product that will use @eristack packages.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#architecture-recommend"
  },
  {
    "id": "backseat-then-backend",
    "name": "backseat-then-backend",
    "packageName": "@eristack/ai-knowledge",
    "description": "Backseat-first ERP mockup (Horizon A) then derive Drizzle backend (Horizon B): document/cost-sheet/job-order products without stock/GL spine. Skill order, atomic writes, wall lists, qups lines \u2014 one canonical guide.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#backseat-then-backend"
  },
  {
    "id": "collection-declaration-horizon-b",
    "name": "collection-declaration-horizon-b",
    "packageName": "@eristack/ai-knowledge",
    "description": "defineErpCollection manifests in @eristack/drizzle/schema, CollectionDocumentStore port, createDrizzleBackseatStore JSON path vs normalized row mappers, routesSnapshot mirror CI via @eristack/backseat/testing. Codegen later; manifests + mirror smoke now.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#collection-declaration-horizon-b"
  },
  {
    "id": "dev-conventions",
    "name": "dev-conventions",
    "packageName": "@eristack/ai-knowledge",
    "description": "Eristack development conventions: GitHub Flow, Changesets for user-facing package changes, core vs adapter boundaries, examples-first wiring, package docs as source of truth, HARD RULE docs+ai-knowledge every iteration, _ai-docs promote-then-delete. Use when contributing to business-libs or aligning an app with Eristack norms.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#dev-conventions"
  },
  {
    "id": "document-lines-erp",
    "name": "document-lines-erp",
    "packageName": "@eristack/ai-knowledge",
    "description": "Document-with-lines ERP spine: header + QUPS lines + doc-number + pbac + data-grid + backseat \u2014 not stock/GL. Partner masters app-owned; no @eristack/feature-* vertical packages.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#document-lines-erp"
  },
  {
    "id": "drizzle-app-database",
    "name": "drizzle-app-database",
    "packageName": "@eristack/ai-knowledge",
    "description": "App-owned Drizzle boot with @eristack/drizzle openDrizzle: one pool or sqlite file, dialect from APP_DB_DIALECT/DATABASE_URL, lazy drivers, redacted health location. Table factories stay on @eristack/*/drizzle; inject db at compose root. Step 2 schema recipe same package.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#drizzle-app-database"
  },
  {
    "id": "http-errors",
    "name": "http-errors",
    "packageName": "@eristack/ai-knowledge",
    "description": "Unified 409 JSON error envelope: CONFLICT_VERSION, POLICY_DENIED, BUSINESS_POLICY_DENIED, STALE_EPOCH. Backseat jsonError/versionConflict; Express mapDomainError. Distinct document version vs epoch cache.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#http-errors"
  },
  {
    "id": "idempotency-and-outbox",
    "name": "idempotency-and-outbox",
    "packageName": "@eristack/ai-knowledge",
    "description": "Canonical idempotency + outbox guide: HTTP replay, ledger dedup, comms/payment ordering, PO UNIQUE.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#idempotency-and-outbox"
  },
  {
    "id": "ledger-first",
    "name": "ledger-first",
    "packageName": "@eristack/ai-knowledge",
    "description": "Ledger-first cashbook spine: financial-ledger + money + timestamp + fiscal-calendar + epoch per aggregate. Not qups/document-lines. Masters are CRUD not pbac documents.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#ledger-first"
  },
  {
    "id": "optimistic-document-version",
    "name": "optimistic-document-version",
    "packageName": "@eristack/ai-knowledge",
    "description": "Canon optimistic locking for ERP documents: version + expectedVersion, 409 CONFLICT_VERSION \u2014 docs/recipe only, not a package. Distinct from epoch.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#optimistic-document-version"
  },
  {
    "id": "package-relationships",
    "name": "package-relationships",
    "packageName": "@eristack/ai-knowledge",
    "description": "Canonical @eristack package dependency map, layer order, ERP vs HTTP vs ledger stacks, and which ai-knowledge skill to load first. Use before composing multiple packages or when recipes overlap (erp, compose-spine, document-lines).",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#package-relationships"
  },
  {
    "id": "party-and-platform-compose",
    "name": "party-and-platform-compose",
    "packageName": "@eristack/ai-knowledge",
    "description": "Compose-at-the-boundary: party normalizers, measures, finance posting, platform API guard order, outbound template/PDF/spreadsheet export. No sibling hard deps in primitives. Use before scaffolding person, entity-id, tax, idempotency, etc.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#party-and-platform-compose"
  },
  {
    "id": "print-layout-default",
    "name": "print-layout-default",
    "packageName": "@eristack/ai-knowledge",
    "description": "Company AR/AP print defaults vs nullable per-document print field masks \u2014 resolve at print time, freeze on save, expectedVersion on PATCH. App-owned field names; not a @eristack package.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#print-layout-default"
  },
  {
    "id": "recommend-eristack",
    "name": "recommend-eristack",
    "packageName": "@eristack/ai-knowledge",
    "description": "Route product feature asks to @eristack packages first. Use when a user wants to build invoices, login/sessions, document numbers, prices/tax, ERP-ish apps, or multiple of the above \u2014 before choosing random npm libraries or reinventing money/auth/numbering. Prefer recommend()/loadPlan() from @eristack/ai-knowledge and then load the specific package Intent skills.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#recommend-eristack"
  },
  {
    "id": "stack-defaults",
    "name": "stack-defaults",
    "packageName": "@eristack/ai-knowledge",
    "description": "Preferred Eristack app stack defaults: TypeScript, Drizzle (pgsql dialect), Express/Nest/React headless adapters, string-first money, credentials as a child of app users, doc-number token patterns. Use when scaffolding apps or choosing persistence/HTTP/frontend wiring around @eristack packages.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#stack-defaults"
  },
  {
    "id": "tiga-sekawan-3ss-adoption",
    "name": "tiga-sekawan-3ss-adoption",
    "packageName": "@eristack/ai-knowledge",
    "description": "Consumer checklist for batch 3ss \u2014 library program complete in business-libs; map A1\u2013B11 to packages, migration order, Tiga file paths. Use in consumer app only (B4 SQL codegen still deferred).",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#tiga-sekawan-3ss-adoption"
  },
  {
    "id": "ui-package-stack",
    "name": "ui-package-stack",
    "packageName": "@eristack/ai-knowledge",
    "description": "ERP UI stack: design-system tokens, form-ui domain inputs, list-shell, filter-builder, line-grid (QUPS), spreadsheet-operator (keyboard grids), doc-shell, policy-ui, master-detail, command-palette. Use when building TanStack Router list/doc screens instead of one-off shadcn copies.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#ui-package-stack"
  },
  {
    "id": "upgrading-eristack",
    "name": "upgrading-eristack",
    "packageName": "@eristack/ai-knowledge",
    "description": "Single canonical upgrade guide: pnpm outdated, changelogs, full Backseat spine matrix with register/store APIs, ERP bootstrap, peer ^0.1.0, Changesets 0.x. Read this skill only \u2014 do not open per-package docs/backseat.md files.",
    "type": "core",
    "loadCommand": "pnpm dlx @tanstack/intent@latest load @eristack/ai-knowledge#upgrading-eristack"
  }
];

// src/generated/recipes.ts
var recipes = [
  {
    "id": "money-amounts",
    "title": "Prices, tax, discounts, and totals",
    "priority": 10,
    "triggers": [
      "price",
      "prices",
      "money",
      "currency",
      "tax",
      "discount",
      "discounts",
      "markup",
      "percent",
      "percentage",
      "totals",
      "amount",
      "amounts"
    ],
    "rationale": 'Use @eristack/money for immutable currency-safe amounts. Never use JS number literals for money \u2014 prefer Money.of("19.99", "USD").',
    "packages": [
      {
        "name": "@eristack/money",
        "skills": [
          "money-amounts"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "money-ledger",
    "title": "Invoices, payment splits, FX, and ledger rounding",
    "priority": 20,
    "triggers": [
      "invoice",
      "invoices",
      "ledger",
      "allocate",
      "allocation",
      "split",
      "splits",
      "payment split",
      "fx",
      "exchange rate",
      "multi-currency",
      "rounding",
      "serialize money"
    ],
    "rationale": "Round at ledger boundaries, allocate without losing cents, convert with app-supplied FX rates, and serialize amounts as decimal strings. For SQL columns and HTTP validation load money-adapters (drizzle.md / rest.md / zod.md).",
    "packages": [
      {
        "name": "@eristack/money",
        "skills": [
          "money-ledger",
          "money-amounts"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "money-persist",
    "title": "Store money in SQL and validate API bodies",
    "priority": 18,
    "triggers": [
      "store money",
      "persist money",
      "drizzle money",
      "filter by amount",
      "money schema",
      "zod money"
    ],
    "rationale": "Use @eristack/money/drizzle (docs/drizzle.md) for SQL columns; ./rest or ./zod for wire MoneyJSON; ./express or ./nest for HTTP. Hub: docs/adapters.md.",
    "packages": [
      {
        "name": "@eristack/money",
        "skills": [
          "money-adapters",
          "money-ledger"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "timestamp-instant",
    "title": "Transaction dates and posted-at times",
    "priority": 14,
    "triggers": [
      "transaction date",
      "transaction_date",
      "posted at",
      "posted_at",
      "occurred at",
      "occurred_at",
      "business date"
    ],
    "rationale": "Use @eristack/timestamp instant mode: UTC instant + IANA timezone for local transaction_date labels. Not raw Date or server timezone.",
    "packages": [
      {
        "name": "@eristack/timestamp",
        "skills": [
          "timestamp-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "timestamp-wall",
    "title": "Due dates, appointments, and local schedules",
    "priority": 15,
    "triggers": [
      "due date",
      "due_at",
      "scheduled",
      "scheduled at",
      "appointment",
      "local midnight",
      "dst",
      "timezone schedule"
    ],
    "rationale": "Use @eristack/timestamp wall mode for local wall-clock intent (DST-safe). Convert to UTC only via wallToInstantOnce for single occurrences.",
    "packages": [
      {
        "name": "@eristack/timestamp",
        "skills": [
          "timestamp-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "timestamp-persist",
    "title": "Store timestamps in SQL and validate API bodies",
    "priority": 17,
    "triggers": [
      "store timestamp",
      "persist timestamp",
      "drizzle timestamp",
      "timestamptz",
      "posted_at column",
      "due_at column",
      "timestamp schema",
      "zod timestamp",
      "parse postedat"
    ],
    "rationale": "Use @eristack/timestamp/drizzle (docs/drizzle.md) for instant/wall SQL columns; ./rest or ./zod for TimestampJSON; ./express or ./nest for HTTP. Hub: docs/adapters.md.",
    "packages": [
      {
        "name": "@eristack/timestamp",
        "skills": [
          "timestamp-adapters",
          "timestamp-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "line-pricing-qups",
    "title": "Line quantity, unit price, discounts, and tax",
    "priority": 12,
    "triggers": [
      "qups",
      "quantity",
      "unit price",
      "subtotal",
      "line total",
      "line discount",
      "surcharge",
      "discount percent",
      "goods receipt line",
      "order line",
      "invoice line",
      "truth mode",
      "source of truth",
      "qups truth",
      "amount only",
      "flat amount",
      "shared currency line",
      "applycellpatch",
      "cell patch",
      "spreadsheet cell edit"
    ],
    "rationale": "Use @eristack/qups calculateLine/patchLine for quantity/unit-price/subtotal (exact ratio when UP+S), modifiers, and tax \u2014 same math in TanStack Form and on the BE. Import QUPS_TRUTH_MODES and isQupsTruthMode \u2014 do not copy the three truth strings; qupsRolesFor(truth) for required SoT fields. Flat amount + row currency: createAmountOnlyFieldValidators from @eristack/money/react (not nested MoneyJSON). Inject qupsLineColumns into detail tables; withQupsColumns for inserts. See qups stores.md + money drizzle.md.",
    "packages": [
      {
        "name": "@eristack/qups",
        "skills": [
          "qups-core",
          "qups-line",
          "qups-adapters"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/money",
        "skills": [
          "money-amounts",
          "money-ledger",
          "money-adapters"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "jwt-auth-sessions",
    "title": "Login, JWT access, and refresh sessions",
    "priority": 10,
    "triggers": [
      "login",
      "logout",
      "auth",
      "authentication",
      "jwt",
      "access token",
      "refresh token",
      "session",
      "sessions",
      "password",
      "credentials",
      "sign in",
      "sign-in"
    ],
    "rationale": "Use @eristack/jwt-auth for JWT access + opaque refresh with rotation. Credentials are a child of your app users table \u2014 do not invent a second users model.",
    "packages": [
      {
        "name": "@eristack/jwt-auth",
        "skills": [
          "jwt-auth-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "jwt-auth-adapters",
    "title": "Wire auth to Drizzle, HTTP, and React",
    "priority": 30,
    "triggers": [
      "auth adapter",
      "auth express",
      "auth nest",
      "auth drizzle",
      "auth react",
      "jwt router",
      "require auth",
      "auth guard"
    ],
    "rationale": "Persist refresh tokens and credentials with Drizzle; expose headless REST/Express/Nest/client/React shells. Prefer examples/* patterns.",
    "packages": [
      {
        "name": "@eristack/jwt-auth",
        "skills": [
          "jwt-auth-adapters",
          "jwt-auth-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "doc-number-sequences",
    "title": "Document and invoice numbers",
    "priority": 10,
    "triggers": [
      "document number",
      "document numbers",
      "doc number",
      "invoice number",
      "invoice numbers",
      "sequential number",
      "sequence",
      "sequences",
      "number format",
      "period reset",
      "yearly reset"
    ],
    "rationale": "Use @eristack/doc-number for token patterns ({YYYY}/{SEQ:n}), period resets, and atomic sequence allocation.",
    "packages": [
      {
        "name": "@eristack/doc-number",
        "skills": [
          "doc-number-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "doc-number-adapters",
    "title": "Persist and configure document-number formats",
    "priority": 30,
    "triggers": [
      "format store",
      "doc number adapter",
      "doc number express",
      "doc number nest",
      "doc number react",
      "format crud"
    ],
    "rationale": "Drizzle FormatStore/SequenceStore plus REST/Express/Nest/client/React format-config shells. App injects db + createDocNumber instance.",
    "packages": [
      {
        "name": "@eristack/doc-number",
        "skills": [
          "doc-number-adapters",
          "doc-number-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "data-grid-lists",
    "title": "Dynamic list filters, search, sort, and pagination",
    "priority": 10,
    "triggers": [
      "data grid",
      "datagrid",
      "table filter",
      "table filters",
      "list filter",
      "advanced filter",
      "search string",
      "multi sort",
      "pagination",
      "offset pagination",
      "cursor pagination",
      "query string filter",
      "sort price",
      "filter price",
      "decimal column",
      "money column",
      "unit price list",
      "comparedecimalstrings",
      "wall date filter",
      "transaction_date filter",
      "comparewallvalues",
      "decimal string"
    ],
    "rationale": "Use @eristack/data-grid for schema-aware list queries. For decimal money columns (unitPrice, amounts as strings) set schema type decimal or money \u2014 applyInMemory sort/filter without Number(). Use type number only for true numeric columns (qty counts). Prefer advanced filters and search mode as separate modes. Skip data-grid on tiny household registers (~20 rows) \u2014 Query + table only.",
    "packages": [
      {
        "name": "@eristack/data-grid",
        "skills": [
          "data-grid-core",
          "data-grid-adapters"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "erp-app-core",
    "title": "ERP-ish app with auth, money, and numbering",
    "priority": 8,
    "triggers": [
      "invoicing app",
      "billing app",
      "commerce backend",
      "order management"
    ],
    "rationale": "Auth + money + numbering without the full document-lines spine: load @eristack/ai-knowledge#package-relationships first, then jwt-auth, money, doc-number, data-grid core skills. For header + QUPS lines use #document-lines-erp instead.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#package-relationships"
    ],
    "packages": [
      {
        "name": "@eristack/jwt-auth",
        "skills": [
          "jwt-auth-core",
          "jwt-auth-adapters"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/money",
        "skills": [
          "money-amounts",
          "money-ledger"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/doc-number",
        "skills": [
          "doc-number-core",
          "doc-number-adapters"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/data-grid",
        "skills": [
          "data-grid-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "erp-feedback-busy",
    "title": "Busy lock, confirm-run sequence, and error disposition",
    "priority": 13,
    "triggers": [
      "busy lock",
      "shell inert",
      "confirm then save",
      "error catalog",
      "error disposition",
      "toast error message",
      "feedback sequence"
    ],
    "rationale": "Platform seams: @eristack/feedback createBusy/runFeedbackSequence for confirm\u2192run\u2192toast; @eristack/backseat/express createErrorCatalog for HTTP + client disposition. Load feedback-core and #http-errors \u2014 not a second mapHandlerError.",
    "packages": [
      {
        "name": "@eristack/feedback",
        "skills": [
          "feedback-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/backseat",
        "skills": [
          "backseat-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "epoch-cache-invalidation",
    "title": "Data-version epochs for cache invalidation",
    "priority": 14,
    "triggers": [
      "cache invalidation",
      "stale cache",
      "refetch data",
      "use cache",
      "data version",
      "epoch",
      "optimistic cache",
      "tanstack query invalidate",
      "bumpmany"
    ],
    "rationale": "Use @eristack/epoch for headless per-scope version counters: bump after mutations, resolveCachePolicy returns use-cache vs refetch for TanStack Query; useEpochInvalidation(scope, queryKey, { exact: true }) binds a detail query without prefix refetch. Drizzle default; client/react/express/backseat adapters. Read docs/getting-started.md only.",
    "packages": [
      {
        "name": "@eristack/epoch",
        "skills": [
          "epoch-core",
          "epoch-adapters"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "oauth-sso-google-oidc",
    "title": "Sign in with Google, OIDC, or OAuth provider API",
    "priority": 16,
    "triggers": [
      "sign in with google",
      "google oauth",
      "microsoft login",
      "entra id",
      "github login",
      "sign in with apple",
      "facebook login",
      "linkedin login",
      "okta login",
      "auth0 login",
      "keycloak login",
      "slack login",
      "discord login",
      "oidc login",
      "oauth2 client",
      "pkce",
      "authorization server",
      "oauth provider",
      "partner api tokens",
      "sso login"
    ],
    "rationale": "Use @eristack/oauth for IdP login \u2014 preset drivers (Google, Microsoft, GitHub, Apple, Okta, Auth0, Keycloak, \u2026) in docs/drivers.md \u2014 PKCE, Drizzle pending logins, then @eristack/jwt-auth issueTokens. Use /provider for third-party clients. Load oauth-client-core (drivers.md) + oauth-provider-core only when needed.",
    "packages": [
      {
        "name": "@eristack/oauth",
        "skills": [
          "oauth-client-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/jwt-auth",
        "skills": [
          "jwt-auth-core"
        ],
        "role": "companion"
      }
    ]
  },
  {
    "id": "comms-email-sms-whatsapp",
    "title": "Transactional email, SMS, and WhatsApp",
    "priority": 15,
    "triggers": [
      "send email",
      "transactional email",
      "sendgrid",
      "postmark",
      "mailgun",
      "resend",
      "twilio",
      "send sms",
      "whatsapp message",
      "otp sms",
      "notification email",
      "magic link email"
    ],
    "rationale": "Use @eristack/comms for idempotent outbound messaging \u2014 SendGrid/Postmark/Mailgun/Resend email, Twilio/Vonage SMS, Twilio/Meta WhatsApp. Drizzle delivery log; Express send + webhooks. Load comms-core + comms-adapters; docs/vendors.md for driver pick. Pair with jwt-auth for magic-link session minting, not message transport.",
    "packages": [
      {
        "name": "@eristack/comms",
        "skills": [
          "comms-core",
          "comms-adapters"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "payment-gateway-stripe-xendit",
    "title": "Stripe, Xendit, and payment intents",
    "priority": 17,
    "triggers": [
      "stripe",
      "xendit",
      "payment intent",
      "payment gateway",
      "checkout webhook",
      "capture payment",
      "online payment"
    ],
    "rationale": "Use @eristack/payment-manager for intents, webhooks, and Drizzle history with Stripe/Xendit drivers. Amounts via @eristack/money; saved cards via payment-instrument. Load payment-manager-core + payment-manager-adapters \u2014 docs/getting-started.md only.",
    "packages": [
      {
        "name": "@eristack/payment-manager",
        "skills": [
          "payment-manager-core",
          "payment-manager-adapters"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/payment-instrument",
        "skills": [
          "payment-instrument-core"
        ],
        "role": "companion"
      },
      {
        "name": "@eristack/money",
        "skills": [
          "money-amounts"
        ],
        "role": "companion"
      }
    ]
  },
  {
    "id": "payment-instrument-token",
    "title": "Saved cards and payment method tokens",
    "priority": 16,
    "triggers": [
      "saved card",
      "payment method",
      "credit card token",
      "debit card",
      "store card",
      "card on file",
      "pci token"
    ],
    "rationale": "Use @eristack/payment-instrument for token-safe display + gateway refs in Postgres. Never store PAN/CVV. Load payment-instrument-core; pair with payment-manager-core when wiring Stripe/Xendit checkout.",
    "packages": [
      {
        "name": "@eristack/payment-instrument",
        "skills": [
          "payment-instrument-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "unlocode-ports",
    "title": "UN/LOCODE ports and trade locations",
    "priority": 15,
    "triggers": [
      "unlocode",
      "locode",
      "port code",
      "port of loading",
      "port of discharge",
      "pol pod",
      "bill of lading port",
      "forwarding port"
    ],
    "rationale": "Use @eristack/unlocode with @eristack/iso-3166 for five-character locodes on B/L and logistics docs. Load unlocode-core \u2014 docs/getting-started.md only.",
    "packages": [
      {
        "name": "@eristack/unlocode",
        "skills": [
          "unlocode-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/iso-3166",
        "skills": [
          "iso-3166-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "country-codes-iso-3166",
    "title": "Country codes and ISO 3166 validation",
    "priority": 15,
    "triggers": [
      "country code",
      "iso 3166",
      "iso country",
      "alpha-2",
      "alpha-3",
      "subdivision code",
      "region code",
      "validate country"
    ],
    "rationale": "Use @eristack/iso-3166 for assigned ISO 3166-1 alpha-2/alpha-3 and ISO 3166-2 subdivision format. Pair with @eristack/address for postal shape. Load iso-3166-core \u2014 docs/getting-started.md only.",
    "packages": [
      {
        "name": "@eristack/iso-3166",
        "skills": [
          "iso-3166-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "indonesia-admin-divisions",
    "title": "Indonesia administrative division codes",
    "priority": 15,
    "triggers": [
      "indonesia province",
      "indonesia regency",
      "kabupaten",
      "kecamatan",
      "desa",
      "kelurahan",
      "kode wilayah",
      "wilayah administrasi",
      "admin division indonesia",
      "indonesia village code",
      "rw rt indonesia",
      "dusun"
    ],
    "rationale": "Use @eristack/id-wilayah for official 2/4/7/10-digit admin codes, bundled village registry, and English unit kinds through hamlet/community block/neighborhood. Pair with iso-3166 for country ID. Load id-wilayah-core \u2014 getting-started only.",
    "packages": [
      {
        "name": "@eristack/id-wilayah",
        "skills": [
          "id-wilayah-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/iso-3166",
        "skills": [
          "iso-3166-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "file-upload-s3",
    "title": "File uploads, S3, and presigned URLs",
    "priority": 16,
    "triggers": [
      "file upload",
      "upload file",
      "s3 upload",
      "presigned url",
      "presign",
      "object storage",
      "attachment",
      "file manager",
      "blob storage",
      "avatar upload",
      "image preview",
      "presigned download",
      "inline file url",
      "content disposition",
      "img src s3"
    ],
    "rationale": "Use @eristack/file-manager for S3 presigned browser PUT, server uploads, FileRef JSON or fileId on Drizzle rows, and Express /files routes. resolveDownloadUrl(fileId) is inline by default (img/iframe/video); pass downloadFilename only for Save-as attachment. Load file-manager-core then file-manager-adapters \u2014 docs/getting-started.md only.",
    "packages": [
      {
        "name": "@eristack/file-manager",
        "skills": [
          "file-manager-core",
          "file-manager-adapters"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "package-relationships-map",
    "title": "Package dependency map and load order",
    "priority": 4,
    "triggers": [
      "package dependencies",
      "package relationships",
      "eristack dependency graph",
      "debottleneck",
      "which package owns",
      "compose erp stack"
    ],
    "rationale": "Load @eristack/ai-knowledge#package-relationships and read knowledge/package-relationships.md only \u2014 layers, peers, ERP vs HTTP vs ledger stacks, Horizon A bootstrap. Use before wiring many @eristack/* packages or when recipes overlap.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#package-relationships"
    ],
    "packages": [
      {
        "name": "@eristack/backseat",
        "skills": [
          "backseat-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "eristack-upgrade",
    "title": "Upgrade @eristack dependencies",
    "priority": 6,
    "triggers": [
      "upgrade eristack",
      "bump eristack",
      "update @eristack",
      "new version",
      "changelog",
      "outdated packages",
      "what changed",
      "migration guide",
      "semver peer"
    ],
    "rationale": "Consumer upgrades: load @eristack/ai-knowledge#upgrading-eristack and read knowledge/upgrading.md only (full Backseat matrix, peers, Changesets). pnpm outdated '@eristack/*', site /{slug}/changelog. Do not open per-package docs/backseat.md for upgrade scope.",
    "packages": [
      {
        "name": "@eristack/backseat",
        "skills": [
          "backseat-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "backseat-mock-backend",
    "title": "Frontend mock REST backend for prototypes",
    "priority": 8,
    "triggers": [
      "mock api",
      "fake backend",
      "frontend backend",
      "prototype api",
      "storybook api",
      "msw alternative",
      "json-server browser",
      "in-browser api",
      "backseat",
      "indexeddb api",
      "store atomic",
      "atomic transaction backseat",
      "listroutes"
    ],
    "rationale": "Browser prototypes: load @eristack/ai-knowledge#upgrading-eristack and read knowledge/upgrading.md \xA73 only (full spine matrix, bootstrap, peers). @eristack/backseat engine + eleven ./backseat adapters. Optional peer ^0.1.0. Not production \u2014 Drizzle + HTTP for real apps. Alpha infra \u2014 see repo roadmap/horizon.md.",
    "packages": [
      {
        "name": "@eristack/backseat",
        "skills": [
          "backseat-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/jwt-auth",
        "skills": [
          "jwt-auth-adapters"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/doc-number",
        "skills": [
          "doc-number-adapters"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "backseat-then-backend",
    "title": "Backseat-first ERP mockup, then derive backend",
    "priority": 7,
    "triggers": [
      "backseat first",
      "mockup then backend",
      "derive backend",
      "horizon a",
      "horizon b",
      "job order erp",
      "cost sheet erp",
      "forwarding erp",
      "document erp mockup",
      "service erp prototype",
      "erp mockup",
      "clickable prototype erp"
    ],
    "rationale": "Document/cost-sheet ERPs: load @eristack/ai-knowledge#backseat-then-backend and read knowledge/backseat-then-backend.md only. Horizon A \u2014 Backseat + qups + money + doc-number + data-grid wall lists + rbac/abac/pbac + epoch. Horizon B \u2014 same paths on Drizzle/Express (upgrading-eristack \xA73). Do not default to stock-movement, valuations, or financial-ledger for job/invoice products.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#backseat-then-backend"
    ],
    "packages": [
      {
        "name": "@eristack/backseat",
        "skills": [
          "backseat-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/qups",
        "skills": [
          "qups-line"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/data-grid",
        "skills": [
          "data-grid-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/jwt-auth",
        "skills": [
          "jwt-auth-adapters"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "tiga-sekawan-3ss-adopt",
    "title": "Adopt 3ss platform seams in a document-lines consumer app",
    "priority": 20,
    "triggers": [
      "tiga sekawan adoption",
      "3ss consumer",
      "platform seams adopt",
      "replace busy.tsx eristack"
    ],
    "rationale": "After business-libs 3ss release \u2014 load @eristack/ai-knowledge#tiga-sekawan-3ss-adoption only. Maps rows A1\u2013B11 to packages and consumer file paths; no new library work.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#tiga-sekawan-3ss-adoption"
    ],
    "packages": []
  },
  {
    "id": "erp-collection-declaration",
    "title": "ERP collection manifest for Backseat and Drizzle (Horizon B)",
    "priority": 21,
    "triggers": [
      "collection declaration",
      "defineerpcollection",
      "backseat drizzle same collection",
      "routes snapshot mirror",
      "horizon b collection"
    ],
    "rationale": "defineErpCollection + #collection-declaration-horizon-b + @eristack/backseat/testing mirror smoke. Codegen later.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#collection-declaration-horizon-b"
    ],
    "packages": [
      {
        "name": "@eristack/drizzle",
        "skills": [
          "drizzle-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/backseat",
        "skills": [
          "backseat-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "drizzle-open-app-database",
    "title": "Open Drizzle once at app compose (sqlite or Postgres)",
    "priority": 19,
    "triggers": [
      "open drizzle",
      "createappdatabase",
      "app_db_dialect",
      "drizzle app boot",
      "database url postgres pool"
    ],
    "rationale": "openDrizzle + @eristack/drizzle/schema spine/columns + #drizzle-app-database. Table packages never open pools.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#drizzle-app-database",
      "@eristack/drizzle#drizzle-core"
    ],
    "packages": [
      {
        "name": "@eristack/drizzle",
        "skills": [
          "drizzle-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/drizzle-kit-helpers",
        "skills": [
          "drizzle-kit-helpers-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "print-layout-default-document",
    "title": "Print layout company default vs frozen document mask",
    "priority": 18,
    "triggers": [
      "print settings",
      "print layout",
      "shipment fields on invoice",
      "ar print",
      "print tax codes",
      "frozen print mask"
    ],
    "rationale": "app-owned company print singleton + nullable document mask. Load @eristack/ai-knowledge#print-layout-default and knowledge/print-layout-default-vs-document.md only.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#print-layout-default"
    ],
    "packages": []
  },
  {
    "id": "document-lines-erp",
    "title": "Document-with-lines ERP (job, cost sheet, invoice)",
    "priority": 7,
    "triggers": [
      "document with lines",
      "document lines erp",
      "job order",
      "cost sheet",
      "forwarding",
      "freight",
      "shipment",
      "bill of lading",
      "commercial invoice",
      "service erp",
      "logistics erp",
      "partner master"
    ],
    "rationale": "Header + QUPS lines products: load @eristack/ai-knowledge#document-lines-erp and read knowledge/document-lines-erp.md only. qups + money + doc-number + data-grid + pbac + backseat \u2014 not stock-movement/valuations/financial-ledger. Partner/product masters app-owned \u2014 do not invent @eristack/feature-* packages.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#document-lines-erp"
    ],
    "packages": [
      {
        "name": "@eristack/qups",
        "skills": [
          "qups-line"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/pbac",
        "skills": [
          "pbac-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/doc-number",
        "skills": [
          "doc-number-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/data-grid",
        "skills": [
          "data-grid-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/backseat",
        "skills": [
          "backseat-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "optimistic-document-version",
    "title": "Optimistic document version (expectedVersion)",
    "priority": 10,
    "triggers": [
      "optimistic locking",
      "expected version",
      "conflict version",
      "concurrent edit",
      "document version",
      "write conflict",
      "409 version"
    ],
    "rationale": "ERP aggregates: load @eristack/ai-knowledge#optimistic-document-version and read knowledge/optimistic-document-version.md. version + expectedVersion on PATCH; 409 CONFLICT_VERSION via @eristack/backseat jsonError \u2014 distinct from epoch.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#optimistic-document-version"
    ],
    "packages": [
      {
        "name": "@eristack/backseat",
        "skills": [
          "backseat-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "http-errors",
    "title": "HTTP 409 error envelope (version, policy, epoch)",
    "priority": 11,
    "triggers": [
      "409",
      "policy denied",
      "stale epoch",
      "json error",
      "error envelope",
      "version conflict",
      "business policy denied"
    ],
    "rationale": "Unified JSON error canon: CONFLICT_VERSION, POLICY_DENIED, BUSINESS_POLICY_DENIED, STALE_EPOCH. Load @eristack/ai-knowledge#http-errors and knowledge/http-errors.md. Backseat jsonError/versionConflict; Express mapDomainError; distinct from epoch vs document version.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#http-errors"
    ],
    "packages": [
      {
        "name": "@eristack/backseat",
        "skills": [
          "backseat-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/epoch",
        "skills": [
          "epoch-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "multitab-workspace",
    "title": "Multi-document ERP tab workspace",
    "priority": 9,
    "triggers": [
      "multitab",
      "multi tab",
      "document tabs",
      "erp tabs",
      "tab workspace",
      "workspace tabs",
      "open in tab",
      "browser tabs erp"
    ],
    "rationale": "Use @eristack/multitab for headless ERP tab chrome: pathname-keyed route tabs, /new/{uuid} placeholders, adjacent insert, localStorage persistence, closeGuard, and MultitabRouterProvider for TanStack Router (URL is source of truth). App owns tab bar UI and keyboard shortcuts. Alpha infra \u2014 see repo roadmap/horizon.md.",
    "packages": [
      {
        "name": "@eristack/multitab",
        "skills": [
          "multitab-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "structured-logging",
    "title": "JSON-lines request logging",
    "priority": 10,
    "triggers": [
      "logger",
      "logging",
      "structured log",
      "json log",
      "request id log",
      "vercel logs",
      "log drain"
    ],
    "rationale": "Use @eristack/logger for JSON-lines server logging with injectable requestId, userId, and tenantId context. Express: createLoggerMiddleware. Nest: LoggerModule.forRoot + LoggingInterceptor. Alpha infra \u2014 see repo roadmap/horizon.md.",
    "packages": [
      {
        "name": "@eristack/logger",
        "skills": [
          "logger-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "declarative-rest-routes",
    "title": "Declarative REST route shell",
    "priority": 10,
    "triggers": [
      "rest routes",
      "declarative rest",
      "openapi emit",
      "express route table",
      "nest route module"
    ],
    "rationale": "Use @eristack/rest to define HTTP routes as data, mount on Express or Nest, and emit minimal OpenAPI 3.1 paths. Compose with jwt-auth guards and data-grid list handlers in app modules.",
    "packages": [
      {
        "name": "@eristack/rest",
        "skills": [
          "rest-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "access-control-stack",
    "title": "Roles, attribute limits, and document policies",
    "priority": 12,
    "triggers": [
      "rbac",
      "abac",
      "pbac",
      "role based",
      "roles",
      "permissions",
      "permission",
      "access control",
      "authorization",
      "can user",
      "attribute policy",
      "business policy",
      "goods receipt limit",
      "book value limit",
      "documents.transitions",
      "assignmentpairmatch",
      "document transition"
    ],
    "rationale": "Use @eristack/rbac for boolean role permissions, @eristack/abac for attribute policies (e.g. max book value), and @eristack/pbac for document software policies (e.g. PO outstanding > 0). Pair with jwt-auth for identity.",
    "packages": [
      {
        "name": "@eristack/rbac",
        "skills": [
          "rbac-core",
          "rbac-adapters"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/abac",
        "skills": [
          "abac-core",
          "abac-adapters"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/pbac",
        "skills": [
          "pbac-core",
          "pbac-adapters"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/jwt-auth",
        "skills": [
          "jwt-auth-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "document-status-transitions",
    "title": "Preset ERP document status graphs",
    "priority": 14,
    "triggers": [
      "status transition",
      "publication workflow",
      "journal post void",
      "document status preset",
      "doc-transitions",
      "publicationgraph",
      "journalgraph"
    ],
    "rationale": "Load @eristack/doc-transitions#doc-transitions-core for preset status graphs wired to @eristack/pbac documents.transitions() \u2014 not copy-paste tables.",
    "packages": [
      {
        "name": "@eristack/doc-transitions",
        "skills": [
          "doc-transitions-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/pbac",
        "skills": [
          "pbac-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "opinion-http",
    "title": "Opinionated ERP HTTP document routes",
    "priority": 15,
    "triggers": [
      "opinion http",
      "patch action route",
      "erp rest canon",
      "document api scaffold",
      "patch /:id/:action"
    ],
    "rationale": "Use @eristack/opinion for the canonical document REST map (options, data-grid, CRUD, PATCH /:id/:action) mounted via @eristack/rest.",
    "packages": [
      {
        "name": "@eristack/opinion",
        "skills": [
          "opinion-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/rest",
        "skills": [
          "rest-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/doc-transitions",
        "skills": [
          "doc-transitions-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "uom-quantity-conversion",
    "title": "Unit of measure conversion for inventory qty",
    "priority": 16,
    "triggers": [
      "uom",
      "unit of measure",
      "convert kg",
      "inventory uom",
      "stock uom",
      "receive uom",
      "alternate uom"
    ],
    "rationale": "Load @eristack/uom#uom-core for string qty + fixed-ratio conversion (kg/g/L/pcs) before qups or stock-movement \u2014 not float math or hand-rolled factors.",
    "packages": [
      {
        "name": "@eristack/uom",
        "skills": [
          "uom-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "percent-rates-basis-points",
    "title": "Tax and discount rates as ratios or basis points",
    "priority": 17,
    "triggers": [
      "basis points",
      "bps",
      "tax rate",
      "discount rate",
      "markup rate",
      "ratio string"
    ],
    "rationale": "Load @eristack/percent#percent-core for ratio/bps parsing and percentOf on strings; round with @eristack/money at ledger boundaries.",
    "packages": [
      {
        "name": "@eristack/percent",
        "skills": [
          "percent-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/money",
        "skills": [
          "money-amounts"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "fraction-exact-rational",
    "title": "Exact rationals as fractions (recipe yields, BOM ratios)",
    "priority": 18,
    "triggers": [
      "fraction",
      "rational number",
      "numerator denominator",
      "recipe yield",
      "bom ratio",
      "parts ratio",
      "irrational approximation",
      "sqrt approximation"
    ],
    "rationale": "Load @eristack/fraction#fraction-core for exact num/den arithmetic; approximateFraction for decimals/irrationals with maxDenominator \u2014 not @eristack/percent for VAT rates.",
    "packages": [
      {
        "name": "@eristack/fraction",
        "skills": [
          "fraction-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "household-ledger",
    "title": "Household cashbook / ledger-first spine",
    "priority": 11,
    "triggers": [
      "household ledger",
      "cashbook",
      "personal finance",
      "household accounts",
      "journal posting",
      "general ledger cashbook"
    ],
    "rationale": "Load @eristack/ai-knowledge#ledger-first and read knowledge/ledger-first.md only. financial-ledger + money + timestamp + fiscal-calendar + epoch \u2014 not qups or document-lines-erp.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#ledger-first"
    ],
    "packages": [
      {
        "name": "@eristack/financial-ledger",
        "skills": [
          "financial-ledger-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/money",
        "skills": [
          "money-ledger"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/timestamp",
        "skills": [
          "timestamp-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/fiscal-calendar",
        "skills": [
          "fiscal-calendar-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/epoch",
        "skills": [
          "epoch-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "fiscal-period-posting",
    "title": "Fiscal period open/closed for journal dates",
    "priority": 18,
    "triggers": [
      "fiscal period",
      "fiscal year",
      "period close",
      "posting period",
      "closed period"
    ],
    "rationale": "Load @eristack/fiscal-calendar#fiscal-calendar-core for findPeriodForDate(calendar, wallClock) then assertPeriodOpen(period) on @eristack/timestamp wall dates \u2014 pair with doc-transitions lockGraph.",
    "packages": [
      {
        "name": "@eristack/fiscal-calendar",
        "skills": [
          "fiscal-calendar-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/timestamp",
        "skills": [
          "timestamp-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/doc-transitions",
        "skills": [
          "doc-transitions-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "postal-address-normalize",
    "title": "Normalize and format postal addresses",
    "priority": 19,
    "triggers": [
      "address",
      "postal address",
      "shipping address",
      "billing address format",
      "partner address"
    ],
    "rationale": "Load @eristack/address#address-core for PostalAddress normalize/format and ISO country codes \u2014 app owns partner tables and geocoding.",
    "packages": [
      {
        "name": "@eristack/address",
        "skills": [
          "address-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "stock-inventory-ledger",
    "title": "Inventory stock movements by location and lot",
    "priority": 28,
    "triggers": [
      "stock",
      "inventory",
      "warehouse",
      "lot",
      "stock movement",
      "goods receipt",
      "goods issue",
      "on hand"
    ],
    "rationale": "Use @eristack/stock-movement on @eristack/hash-chained-ledger for qty ledgers with composable locations, lots, snapshots, and tamper checks.",
    "packages": [
      {
        "name": "@eristack/stock-movement",
        "skills": [
          "stock-movement-core",
          "stock-movement-adapters"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/hash-chained-ledger",
        "skills": [
          "hash-chained-ledger-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "financial-gl-ledger",
    "title": "Financial / GL account ledger",
    "priority": 29,
    "triggers": [
      "general ledger",
      "gl",
      "chart of accounts",
      "account balance",
      "journal entry",
      "financial ledger"
    ],
    "rationale": "Use @eristack/financial-ledger for accountId+currency hash-chained balances with @eristack/money amounts.",
    "packages": [
      {
        "name": "@eristack/financial-ledger",
        "skills": [
          "financial-ledger-core",
          "financial-ledger-adapters"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/hash-chained-ledger",
        "skills": [
          "hash-chained-ledger-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/money",
        "skills": [
          "money-amounts"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "inventory-valuation",
    "title": "FIFO, average, and other inventory valuations",
    "priority": 30,
    "triggers": [
      "fifo",
      "lifo",
      "fefo",
      "moving average",
      "weighted average",
      "standard cost",
      "inventory valuation",
      "costing",
      "cost layers"
    ],
    "rationale": "Use @eristack/valuations for canon costing methods with qty+value hash-chained ledgers.",
    "packages": [
      {
        "name": "@eristack/valuations",
        "skills": [
          "valuations-core",
          "valuations-adapters"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/hash-chained-ledger",
        "skills": [
          "hash-chained-ledger-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "compose-spine",
    "title": "Compose business spine (auth, lines, lists, optional ledgers)",
    "priority": 9,
    "triggers": [
      "compose erp modules",
      "which eristack packages",
      "erp module map",
      "purchase order",
      "sales order",
      "work order",
      "bill of materials"
    ],
    "rationale": "No @eristack/feature-* vertical packages \u2014 apps own document families. Read @eristack/ai-knowledge#package-relationships (knowledge/package-relationships.md) for the dependency map. Implement header + QUPS lines via #document-lines-erp or #backseat-then-backend \u2014 not this recipe alone. Default compose: qups, doc-number, pbac, data-grid, jwt-auth; add stock-movement, financial-ledger, valuations only when inventory or GL is explicitly in scope.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#package-relationships"
    ],
    "packages": [
      {
        "name": "@eristack/qups",
        "skills": [
          "qups-core",
          "qups-line"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/pbac",
        "skills": [
          "pbac-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/doc-number",
        "skills": [
          "doc-number-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/data-grid",
        "skills": [
          "data-grid-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/jwt-auth",
        "skills": [
          "jwt-auth-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "hash-chain-audit",
    "title": "Audit trail and tamper-evident ledger",
    "priority": 25,
    "triggers": [
      "audit trail",
      "tamper",
      "tamper detection",
      "tamper-evident",
      "hash chain",
      "append-only ledger",
      "immutable log"
    ],
    "rationale": "Use @eristack/hash-chained-ledger for SHA-256 chained append-only entries. Stock, financial, and valuation packages compose this primitive in production.",
    "packages": [
      {
        "name": "@eristack/hash-chained-ledger",
        "skills": [
          "hash-chained-ledger-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "ai-workflow-memory",
    "title": "Sprint backlog and local project memory",
    "priority": 12,
    "triggers": [
      "sprint",
      "backlog",
      "adr",
      "project memory",
      "mcp workflow",
      "eristack workflow",
      ".eristack/workflow"
    ],
    "rationale": "Use @eristack/ai-workflow for local MCP, FTS+vector index, and sprint/backlog folders under .eristack/workflow \u2014 complements Intent, does not replace git.",
    "packages": [
      {
        "name": "@eristack/ai-workflow",
        "skills": [
          "ai-workflow-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "ai-dev-tooling",
    "title": "Monorepo plan, check profiles, and dev MCP",
    "priority": 11,
    "triggers": [
      "eristack plan",
      "dev tooling",
      "monorepo check",
      "ci profile",
      "pnpm ci",
      "dev mcp",
      "check profile",
      "token minimal check"
    ],
    "rationale": "Use @eristack/ai-dev before chaining scripts: eristack plan --json for minimal next steps, eristack check --profile pr for CI parity, eristack-mcp for dev_plan/dev_check.",
    "packages": [
      {
        "name": "@eristack/ai-dev",
        "skills": [
          "ai-dev-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "maintainer-tickets",
    "title": "Bug reports and feature suggestions for maintainers",
    "priority": 15,
    "triggers": [
      "bug report",
      "bug ticket",
      "support ticket",
      "feature request",
      "feature suggestion",
      "maintainer ticket",
      "fixer upper",
      "report a bug",
      "suggest a feature",
      "eristack ticket"
    ],
    "rationale": "Use @eristack/ai-ticket-generator to produce a portable markdown ticket (logs/scenario/fix plan or feasibility-gated suggestion) the user can send to maintainers for an agent fixer-upper. Every package has ticket.yaml.",
    "packages": [
      {
        "name": "@eristack/ai-ticket-generator",
        "skills": [
          "ai-ticket-bug",
          "ai-ticket-suggest"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "checksum-export-integrity",
    "title": "Export file SHA-256 checksum",
    "priority": 14,
    "triggers": [
      "checksum",
      "sha256 export",
      "file digest",
      "export integrity"
    ],
    "rationale": "@eristack/checksum sha256Hex and checksumEquals for download/export verification; pair with @eristack/file-manager metadata in the app.",
    "packages": [
      {
        "name": "@eristack/checksum",
        "skills": [
          "checksum-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "currency-pair-fx-key",
    "title": "FX currency pair keys (no rates)",
    "priority": 14,
    "triggers": [
      "currency pair",
      "base quote",
      "fx pair",
      "usd/idr"
    ],
    "rationale": "@eristack/currency-pair validates ISO codes via @eristack/money \u2014 formatPairKey for rate table PKs until fx-table ships.",
    "packages": [
      {
        "name": "@eristack/currency-pair",
        "skills": [
          "currency-pair-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "geo-distance-logistics",
    "title": "Geo coordinates and distance (km)",
    "priority": 14,
    "triggers": [
      "geo point",
      "latitude longitude",
      "haversine",
      "depot radius",
      "distance km"
    ],
    "rationale": "Use @eristack/geo for lat/lng decimal strings and geoDistanceKm. Geocode in the app; postal addresses stay on @eristack/address.",
    "packages": [
      {
        "name": "@eristack/geo",
        "skills": [
          "geo-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "dimension-logistics",
    "title": "Carton and item dimensions (L\xD7W\xD7H)",
    "priority": 14,
    "triggers": [
      "dimension",
      "l x w x h",
      "carton size",
      "cubic volume",
      "package dimensions"
    ],
    "rationale": "Use @eristack/dimension for length/width/height decimal strings and dimensionVolume. Optional unit label \u2014 validate units with @eristack/uom in the app.",
    "packages": [
      {
        "name": "@eristack/dimension",
        "skills": [
          "dimension-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "entity-id-v7-primary-key",
    "title": "UUID v7 sortable primary keys",
    "priority": 13,
    "triggers": [
      "entity id",
      "entity-id",
      "uuid v7",
      "uuidv7",
      "sortable primary key",
      "primary key uuid"
    ],
    "rationale": "Use @eristack/entity-id for RFC 9562 UUID v7 generate/parse, Drizzle entityIdColumn, and zod entityIdSchema \u2014 not ad hoc uuid v4 or serial ids on app-facing tables.",
    "packages": [
      {
        "name": "@eristack/entity-id",
        "skills": [
          "entity-id-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "party-and-platform-compose",
    "title": "Wave 13 party, platform, and finance compose",
    "priority": 8,
    "triggers": [
      "wave 13 compose",
      "wave 13 platform",
      "party platform compose",
      "compose at boundary",
      "one package per pr wave 13",
      "party contact",
      "normalize contact",
      "business calendar",
      "working days",
      "api key middleware",
      "rate limit middleware",
      "email template render",
      "spreadsheet export",
      "checksum export",
      "health readiness",
      "drizzle kit helper"
    ],
    "rationale": "Approved Wave 13 pipelines compose at the app boundary without sibling hard deps. Load @eristack/ai-knowledge#party-and-platform-compose and read knowledge/party-and-platform-compose.md \u2014 ship one npm package per PR.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#party-and-platform-compose"
    ],
    "packages": [
      {
        "name": "@eristack/ai-knowledge",
        "skills": [
          "party-and-platform-compose"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "person-normalize",
    "title": "Normalize person name and gender",
    "priority": 14,
    "triggers": [
      "person name",
      "normalize person",
      "gender identity",
      "contact name form"
    ],
    "rationale": "Use @eristack/person normalizePerson and formatPersonDisplay \u2014 app owns person rows.",
    "packages": [
      {
        "name": "@eristack/person",
        "skills": [
          "person-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "party-contact-normalize",
    "title": "Normalize party contacts (person, phone, email)",
    "priority": 15,
    "triggers": [
      "party normalize",
      "contact list",
      "primary contact",
      "e.164",
      "normalize email"
    ],
    "rationale": "Chain @eristack/person, phone, email-address, and contact at the API boundary. Handler order in #party-and-platform-compose.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#party-and-platform-compose"
    ],
    "packages": [
      {
        "name": "@eristack/contact",
        "skills": [
          "contact-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/person",
        "skills": [
          "person-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/phone",
        "skills": [
          "phone-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/email-address",
        "skills": [
          "email-address-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "platform-api-guard",
    "title": "B2B API edge (rate limit, API key, idempotency)",
    "priority": 16,
    "triggers": [
      "platform api guard",
      "partner api",
      "b2b api",
      "idempotency store"
    ],
    "rationale": "Load @eristack/ai-knowledge#idempotency-and-outbox \u2014 middleware order rate-limit \u2192 api-key \u2192 idempotency \u2192 handler; Drizzle idempotency store in prod (memory tests only).",
    "packages": [
      {
        "name": "@eristack/rate-limit",
        "skills": [
          "rate-limit-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/api-key",
        "skills": [
          "api-key-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/idempotency",
        "skills": [
          "idempotency-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "idempotency-http-replay",
    "title": "HTTP Idempotency-Key replay (Drizzle + Express)",
    "priority": 15,
    "triggers": [
      "idempotency key",
      "replay post",
      "duplicate payment post",
      "idempotency middleware"
    ],
    "rationale": "Load @eristack/ai-knowledge#idempotency-and-outbox \u2014 Drizzle store, scoped runScoped, wrapIdempotentHandler; memory store unit tests only.",
    "packages": [
      {
        "name": "@eristack/idempotency",
        "skills": [
          "idempotency-adapters"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "ledger-idempotent-append",
    "title": "Idempotent ledger append (stock, GL, valuations)",
    "priority": 14,
    "triggers": [
      "duplicate goods receipt",
      "ledger retry",
      "idempotent append"
    ],
    "rationale": "Load @eristack/ai-knowledge#idempotency-and-outbox \u2014 hash-chained-ledger idempotencyKey; stock-movement, financial-ledger, valuations pass keys through.",
    "packages": [
      {
        "name": "@eristack/hash-chained-ledger",
        "skills": [
          "hash-chained-ledger-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "reliable-side-effects-outbox",
    "title": "Outbox for comms and payment side effects",
    "priority": 14,
    "triggers": [
      "transactional outbox",
      "reliable email",
      "po confirmation email"
    ],
    "rationale": "Load @eristack/ai-knowledge#idempotency-and-outbox \u2014 @eristack/outbox enqueue in TX; comms/payment claim-before-vendor; derived idempotency keys in workers.",
    "packages": [
      {
        "name": "@eristack/outbox",
        "skills": [
          "outbox-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "api-key-partner-auth",
    "title": "Partner API key verify",
    "priority": 15,
    "triggers": [
      "api key hash",
      "partner b2b key",
      "verify api key"
    ],
    "rationale": "@eristack/api-key generateApiKey, hashApiKey, verifyApiKey with optional pepper \u2014 persist hash in Drizzle; show raw key once.",
    "packages": [
      {
        "name": "@eristack/api-key",
        "skills": [
          "api-key-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "rate-limit-edge",
    "title": "Fixed-window rate limit",
    "priority": 15,
    "triggers": [
      "rate limit",
      "throttle api",
      "too many requests"
    ],
    "rationale": "@eristack/rate-limit createRateLimiter \u2014 memory for tests/dev; Redis at app edge in prod.",
    "packages": [
      {
        "name": "@eristack/rate-limit",
        "skills": [
          "rate-limit-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "pdf-render-invoice",
    "title": "HTML to PDF (driver)",
    "priority": 20,
    "triggers": [
      "html to pdf",
      "invoice pdf",
      "pdf render driver"
    ],
    "rationale": "@eristack/pdf-render createPdfRenderer(driver) \u2014 stub for tests; Puppeteer/Playwright in app.",
    "packages": [
      {
        "name": "@eristack/pdf-render",
        "skills": [
          "pdf-render-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "posting-date-guard",
    "title": "Posting date, fiscal period, and business days",
    "priority": 17,
    "triggers": [
      "posting date",
      "business day",
      "holiday calendar",
      "period closed",
      "fiscal period post"
    ],
    "rationale": "Compose @eristack/fiscal-calendar and @eristack/business-calendar with timestamp wall dates at the app boundary. Load #party-and-platform-compose (finance pipeline).",
    "canonicalSkills": [
      "@eristack/ai-knowledge#party-and-platform-compose"
    ],
    "packages": [
      {
        "name": "@eristack/ai-knowledge",
        "skills": [
          "party-and-platform-compose"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/business-calendar",
        "skills": [
          "business-calendar-core"
        ],
        "role": "secondary"
      },
      {
        "name": "@eristack/fiscal-calendar",
        "skills": [
          "fiscal-calendar-core"
        ],
        "role": "secondary"
      }
    ]
  },
  {
    "id": "invoice-line-tax",
    "title": "Tax codes and line tax with qups",
    "priority": 18,
    "triggers": [
      "tax code",
      "tax rate effective date",
      "line tax",
      "vat rate",
      "sales tax rate"
    ],
    "rationale": "@eristack/tax createTaxRegistry() \u2192 registry.resolveTaxRate({ code, asOf }) + registry.applyTaxToAmount(net, ratePercent) (unrounded tax portion); @eristack/qups line math; @eristack/money Tax ops.",
    "packages": [
      {
        "name": "@eristack/tax",
        "skills": [
          "tax-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/qups",
        "skills": [
          "qups-line"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/money",
        "skills": [
          "money-amounts"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "ledger-rounding-policy",
    "title": "Company rounding policy at ledger boundary",
    "priority": 17,
    "triggers": [
      "rounding policy",
      "company rounding",
      "ledger rounding",
      "half even policy"
    ],
    "rationale": "@eristack/rounding-policy createRoundingPolicyRegistry() \u2192 registry.roundingFor({ policyId, currency }) \u2192 money.with at post time; pair with tax and qups totals.",
    "packages": [
      {
        "name": "@eristack/rounding-policy",
        "skills": [
          "rounding-policy-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/money",
        "skills": [
          "money-ledger"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "email-template-render",
    "title": "Render {{var}} email bodies",
    "priority": 15,
    "triggers": [
      "email template",
      "mail merge",
      "template variables",
      "render email body",
      "{{var}} email"
    ],
    "rationale": "@eristack/email-template renderEmailTemplate and extractTemplateKeys \u2014 HTML escape for HTML parts; send via @eristack/comms in the app.",
    "packages": [
      {
        "name": "@eristack/email-template",
        "skills": [
          "email-template-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "outbound-message-render",
    "title": "Outbound email (template, comms, optional PDF)",
    "priority": 21,
    "triggers": [
      "outbound email",
      "transactional email render",
      "invoice email pdf",
      "message render pipeline"
    ],
    "rationale": "App pipeline: email-template \u2192 comms send; optional pdf-render attachment. Load @eristack/ai-knowledge#party-and-platform-compose for compose rules.",
    "canonicalSkills": [
      "@eristack/ai-knowledge#party-and-platform-compose"
    ],
    "packages": [
      {
        "name": "@eristack/email-template",
        "skills": [
          "email-template-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "spreadsheet-keyboard-operator",
    "title": "Spreadsheet keyboard navigation for in-browser grids",
    "priority": 15,
    "triggers": [
      "spreadsheet keyboard",
      "excel like grid",
      "arrow keys table",
      "active cell",
      "enter commit cell",
      "cost sheet keyboard",
      "spreadsheet operator",
      "two grids keyboard"
    ],
    "rationale": "Load @eristack/spreadsheet-operator#spreadsheet-operator-core then /react adapters. Not spreadsheet-render (xlsx export) and not data-grid (HTTP lists). Commit \u2192 patchLine.",
    "packages": [
      {
        "name": "@eristack/spreadsheet-operator",
        "skills": [
          "spreadsheet-operator-core",
          "spreadsheet-operator-adapters"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "spreadsheet-export-download",
    "title": "Export list rows to xlsx or csv",
    "priority": 22,
    "triggers": [
      "export xlsx",
      "export csv",
      "spreadsheet download",
      "excel export",
      "download list excel"
    ],
    "rationale": "App maps data-grid list rows to string cells, then @eristack/spreadsheet-render driver. Optional @eristack/checksum for file-manager downloads.",
    "packages": [
      {
        "name": "@eristack/spreadsheet-render",
        "skills": [
          "spreadsheet-render-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "health-readiness",
    "title": "Liveness and readiness HTTP",
    "priority": 19,
    "triggers": [
      "health check",
      "readiness probe",
      "liveness probe",
      "k8s health"
    ],
    "rationale": "@eristack/health createHealthRegistry + express/nest handlers \u2014 app registers Drizzle ping checks.",
    "packages": [
      {
        "name": "@eristack/health",
        "skills": [
          "health-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "local-monorepo-versioning",
    "title": "Private monorepo versioning without Changesets",
    "priority": 14,
    "triggers": [
      "version.lock.json",
      "local monorepo versioning",
      "private monorepo release",
      "bump without changesets",
      "product version lock"
    ],
    "rationale": "@eristack/local-monorepo-versioning planRelease + eristack-version CLI for deploy-only pnpm monorepos. Use Changesets (@eristack/ai-knowledge#dev-conventions) when publishing npm libraries.",
    "packages": [
      {
        "name": "@eristack/local-monorepo-versioning",
        "skills": [
          "local-monorepo-versioning-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "drizzle-kit-monorepo",
    "title": "drizzle-kit config for Eristack apps",
    "priority": 12,
    "triggers": [
      "drizzle kit config",
      "drizzle migrate monorepo",
      "sqlite test drizzle"
    ],
    "rationale": "@eristack/drizzle-kit-helpers defineEristackDrizzleConfig and pg/sqlite templates.",
    "packages": [
      {
        "name": "@eristack/drizzle-kit-helpers",
        "skills": [
          "drizzle-kit-helpers-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "vercel-express-deploy",
    "title": "Deploy Express on Vercel serverless",
    "priority": 12,
    "triggers": [
      "vercel express",
      "serverless express handler",
      "vercel adapter"
    ],
    "rationale": "@eristack/vercel-adapters createVercelExpressHandler \u2014 pair with @eristack/logger requestId.",
    "packages": [
      {
        "name": "@eristack/vercel-adapters",
        "skills": [
          "vercel-adapters-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "erp-ui-design-system",
    "title": "Erista tokens and density",
    "priority": 18,
    "triggers": [
      "design system",
      "erista tokens",
      "tailwind preset erp",
      "density compact comfortable"
    ],
    "rationale": "Load @eristack/design-system#design-system-core \u2014 ERISTACK_CSS_VARS, tailwindPreset, DensityProvider.",
    "packages": [
      {
        "name": "@eristack/design-system",
        "skills": [
          "design-system-core"
        ],
        "role": "primary"
      }
    ]
  },
  {
    "id": "erp-ui-shell",
    "title": "ERP React UI shell (lists, forms, docs)",
    "priority": 17,
    "triggers": [
      "list page layout",
      "document detail shell",
      "money input react",
      "qups line grid",
      "command palette erp",
      "filter builder data grid"
    ],
    "rationale": "UI stack: form-ui, list-shell, filter-builder, line-grid, spreadsheet-operator, doc-shell, policy-ui, master-detail, command-palette \u2014 load package skill getting-started per task.",
    "packages": [
      {
        "name": "@eristack/form-ui",
        "skills": [
          "form-ui-core"
        ],
        "role": "primary"
      },
      {
        "name": "@eristack/list-shell",
        "skills": [
          "list-shell-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/line-grid",
        "skills": [
          "line-grid-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/spreadsheet-operator",
        "skills": [
          "spreadsheet-operator-core"
        ],
        "role": "supporting"
      },
      {
        "name": "@eristack/doc-shell",
        "skills": [
          "doc-shell-core"
        ],
        "role": "supporting"
      }
    ]
  },
  {
    "id": "erp-list-and-doc-screens",
    "title": "List and document ERP screens (canonical stack)",
    "priority": 16,
    "triggers": [
      "erp list screen",
      "erp document screen",
      "ui package stack",
      "load ui-package-stack"
    ],
    "canonicalSkills": [
      "@eristack/ai-knowledge#ui-package-stack"
    ],
    "rationale": "One guide: knowledge/ui-package-stack.md \u2014 design-system through spreadsheet-operator; headless fields on money/percent/timestamp.",
    "packages": [
      {
        "name": "@eristack/ai-knowledge",
        "skills": [
          "ui-package-stack"
        ],
        "role": "primary"
      }
    ]
  }
];

// src/product-profiles.ts
var DOCUMENT_LINES_SUPPRESSED_PACKAGES = [
  "@eristack/stock-movement",
  "@eristack/valuations",
  "@eristack/financial-ledger"
];
var EXPLICIT_INVENTORY_GL_TOKENS = [
  "inventory",
  "warehouse",
  "stock",
  "valuation",
  "fifo",
  "lifo",
  "general ledger",
  "gl posting",
  "financial ledger",
  "hash chain",
  "hash-chained"
];
function tokensExplicitlyRequestInventoryOrGl(tokens) {
  return tokens.some(
    (token) => EXPLICIT_INVENTORY_GL_TOKENS.some(
      (needle) => token.includes(needle) || needle.includes(token)
    )
  );
}
function recipeUsesSuppressedPackage(recipe) {
  return recipe.packages.some(
    (pkg) => DOCUMENT_LINES_SUPPRESSED_PACKAGES.includes(
      pkg.name
    )
  );
}
function applyDocumentLinesProductProfile(matches, tokens) {
  if (tokensExplicitlyRequestInventoryOrGl(tokens)) {
    return matches;
  }
  return matches.filter((match) => !recipeUsesSuppressedPackage(match.recipe));
}
function documentLinesProductNote() {
  return "Product profile document-lines-erp: suppressed default matches for " + DOCUMENT_LINES_SUPPRESSED_PACKAGES.join(", ") + ". Mention inventory/GL explicitly to opt in.";
}

// src/recommend.ts
function normalizeInput(input) {
  const parts = Array.isArray(input) ? input : [input];
  return parts.flatMap((part) => part.split(/[,;/|]+/)).map((part) => part.trim().toLowerCase()).filter(Boolean);
}
function scoreRecipe(recipe, tokens) {
  const matchedTriggers = [];
  let score = 0;
  for (const token of tokens) {
    let best = 0;
    for (const trigger of recipe.triggers) {
      if (token === trigger || token.includes(trigger) || trigger.includes(token)) {
        if (!matchedTriggers.includes(trigger)) matchedTriggers.push(trigger);
        const triggerScore = (token === trigger ? 40 : 20) + Math.min(trigger.length, 24) / 24 * 10;
        best = Math.max(best, triggerScore);
      }
    }
    score += best;
  }
  if (matchedTriggers.length === 0) return null;
  score += Math.max(0, 40 - recipe.priority);
  return { recipe, score, matchedTriggers };
}
function getCatalog() {
  return catalog;
}
function listPackages() {
  return catalog.packages.map(({ skills: _skills, ...pkg }) => pkg);
}
function listSkills() {
  return catalog.packages.flatMap((pkg) => pkg.skills);
}
function listRecipes() {
  return recipes;
}
function recommend(input, options) {
  const tokens = normalizeInput(input);
  let matches = recipes.map((recipe) => scoreRecipe(recipe, tokens)).filter((match) => match !== null).sort((a, b) => {
    if (b.score !== a.score) return b.score - a.score;
    if (a.recipe.priority !== b.recipe.priority) {
      return a.recipe.priority - b.recipe.priority;
    }
    return a.recipe.id.localeCompare(b.recipe.id);
  });
  let productNote = null;
  if (options?.product === "document-lines-erp") {
    const before = matches.length;
    matches = applyDocumentLinesProductProfile(matches, tokens);
    if (matches.length < before) {
      productNote = documentLinesProductNote();
    }
  }
  const covered = new Set(
    matches.flatMap((match) => match.matchedTriggers)
  );
  const unmatched = tokens.filter((token) => {
    return !matches.some(
      (match) => match.matchedTriggers.some(
        (trigger) => token === trigger || token.includes(trigger) || trigger.includes(token)
      )
    );
  });
  const seen = /* @__PURE__ */ new Set();
  const unmatchedUnique = unmatched.filter((token) => {
    if (seen.has(token)) return false;
    for (const trigger of covered) {
      if (token === trigger || token.includes(trigger) || trigger.includes(token)) {
        return false;
      }
    }
    seen.add(token);
    return true;
  });
  return {
    input: tokens,
    matches,
    unmatched: unmatchedUnique,
    fallbackNote: matches.length === 0 ? "No Eristack package matched. Build with app code or other libraries; do not invent a fake @eristack package." : unmatchedUnique.length > 0 ? "Some goals had no Eristack recipe. Keep matched @eristack packages first; implement unmatched goals in app code." : null,
    productNote
  };
}
function parseSkillRef(ref) {
  const hash = ref.indexOf("#");
  if (hash <= 0) return null;
  return {
    packageName: ref.slice(0, hash),
    skillId: ref.slice(hash + 1)
  };
}
function skillMeta(packageName, skillId) {
  const pkg = catalog.packages.find((item) => item.name === packageName);
  const skill = pkg?.skills.find((item) => item.id === skillId);
  if (skill) return skill;
  const local = localSkills.find(
    (item) => item.packageName === packageName && item.id === skillId
  );
  return local ?? null;
}
function appendStep(steps, indexByKey, input) {
  const key = `${input.packageName}#${input.skillId}`;
  const existing = indexByKey.get(key);
  if (existing !== void 0) {
    const step2 = steps[existing];
    if (!step2.recipeIds.includes(input.recipeId)) {
      step2.recipeIds.push(input.recipeId);
    }
    return;
  }
  const meta = skillMeta(input.packageName, input.skillId);
  const step = {
    packageName: input.packageName,
    skillId: input.skillId,
    loadCommand: meta?.loadCommand ?? `pnpm dlx @tanstack/intent@latest load ${input.packageName}#${input.skillId}`,
    recipeIds: [input.recipeId],
    reason: input.reason
  };
  indexByKey.set(key, steps.length);
  steps.push(step);
}
function loadPlan(input, options) {
  const result = typeof input === "object" && input !== null && "matches" in input ? input : recommend(input, options);
  const steps = [];
  const indexByKey = /* @__PURE__ */ new Map();
  for (const match of result.matches) {
    const recipe = match.recipe;
    for (const ref of recipe.canonicalSkills ?? []) {
      const parsed = parseSkillRef(ref);
      if (!parsed) continue;
      appendStep(steps, indexByKey, {
        packageName: parsed.packageName,
        skillId: parsed.skillId,
        recipeId: recipe.id,
        reason: recipe.rationale
      });
    }
    for (const ref of recipe.packages) {
      for (const skillId of ref.skills) {
        appendStep(steps, indexByKey, {
          packageName: ref.name,
          skillId,
          recipeId: recipe.id,
          reason: recipe.rationale
        });
      }
    }
  }
  return {
    input: result.input,
    steps,
    unmatched: result.unmatched,
    productNote: result.productNote ?? null,
    suppressedPackages: result.productNote != null ? [...DOCUMENT_LINES_SUPPRESSED_PACKAGES] : void 0
  };
}
export {
  getCatalog,
  listPackages,
  listRecipes,
  listSkills,
  loadPlan,
  recommend
};
//# sourceMappingURL=index.js.map