diff --git a/.changeset/old-wolves-speak.md b/.changeset/old-wolves-speak.md deleted file mode 100644 index 3adf68da..00000000 --- a/.changeset/old-wolves-speak.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@gsa-tts/forms-cli": patch -"@gsa-tts/forms-e2e": patch -"@gsa-tts/forms-common": patch -"@gsa-tts/forms-design": patch ---- - -Change end-to-end tests to run against server rendered app diff --git a/.github/workflows/_docker-build-image.yml b/.github/workflows/_docker-build-image.yml index 1b4e1f87..3477ca71 100644 --- a/.github/workflows/_docker-build-image.yml +++ b/.github/workflows/_docker-build-image.yml @@ -27,8 +27,8 @@ env: AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: us-east-2 - ECR_REPOSITORY: tts-10x-forms-${{ inputs.deploy-key }}-image-${{ inputs.app-name }} + AWS_REGION: us-east-1 + ECR_REPOSITORY: flexion-forms-sandbox-${{ inputs.deploy-key }} jobs: setup: @@ -57,16 +57,16 @@ jobs: run: | docker push --all-tags ${REGISTRY_PATH} - # - name: Log in to AWS ECR - # run: | - # aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com + - name: Log in to AWS ECR + run: | + aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com - # - name: Tag Docker image for ECR - # run: | - # docker tag ${REGISTRY_PATH}:${COMMIT_SHA} ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:${COMMIT_SHA} - # docker tag ${REGISTRY_PATH}:${TAG_NAME} ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:${TAG_NAME} + - name: Tag Docker image for ECR + run: | + docker tag ${REGISTRY_PATH}:${COMMIT_SHA} ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:${COMMIT_SHA} + docker tag ${REGISTRY_PATH}:${COMMIT_SHA} ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:latest - # - name: Push Docker image to ECR - # run: | - # docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:${COMMIT_SHA} - # docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:${TAG_NAME} + - name: Push Docker image to ECR + run: | + docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:${COMMIT_SHA} + docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:latest diff --git a/.github/workflows/_terraform-apply.yml b/.github/workflows/_terraform-apply.yml index 2f6bfb0d..e7cdff58 100644 --- a/.github/workflows/_terraform-apply.yml +++ b/.github/workflows/_terraform-apply.yml @@ -57,11 +57,11 @@ jobs: - name: Generate Terraform CDK provider constructs shell: bash - run: pnpm --filter @gsa-tts/forms-infra-cdktf build:get + run: pnpm --filter @flexion/forms-infra-cdktf build:get - name: Initialize Terraform CDK configuration shell: bash - run: pnpm turbo run --filter @gsa-tts/forms-infra-cdktf build + run: pnpm turbo run --filter @flexion/forms-infra-cdktf build - name: Install CloudFoundry CLI run: | diff --git a/.github/workflows/_terraform-plan-pr-comment.yml b/.github/workflows/_terraform-plan-pr-comment.yml index 160b920c..36f0e036 100644 --- a/.github/workflows/_terraform-plan-pr-comment.yml +++ b/.github/workflows/_terraform-plan-pr-comment.yml @@ -68,11 +68,11 @@ jobs: - name: Generate Terraform CDK provider constructs shell: bash - run: pnpm --filter @gsa-tts/forms-infra-cdktf build:get + run: pnpm --filter @flexion/forms-infra-cdktf build:get - name: Build Terraform configuration shell: bash - run: pnpm turbo run --filter @gsa-tts/forms-infra-cdktf build + run: NODE_OPTIONS=--max-old-space-size=6144 pnpm turbo run --filter @flexion/forms-infra-cdktf build - name: Get Terraform stack name id: get_stack_name diff --git a/.github/workflows/_validate.yml b/.github/workflows/_validate.yml index cc6331ec..a7c95325 100644 --- a/.github/workflows/_validate.yml +++ b/.github/workflows/_validate.yml @@ -62,26 +62,26 @@ jobs: if: steps.playwright-cache.outputs.cache-hit != 'true' # While most of the test suite is self-contained, the tests for the demo - # servers require a prod build of @atj/server. + # servers require a prod build of @flexion/forms-server. - name: Build run: pnpm build - - name: Make directory for build artifacts - run: mkdir -p output/build-artifacts + # - name: Make directory for build artifacts + # run: mkdir -p output/build-artifacts - - name: Spotlight app performance budget - run: pnpm --filter @atj/spotlight size:ci > output/build-artifacts/spotlight-size-output.txt + # - name: Spotlight app performance budget + # run: pnpm --filter @flexion/forms-spotlight size:ci > output/build-artifacts/spotlight-size-output.txt - - name: Design package performance budget - run: pnpm --filter @atj/design size:ci > output/build-artifacts/design-size-output.txt + # - name: Design package performance budget + # run: pnpm --filter @flexion/forms-design size:ci > output/build-artifacts/design-size-output.txt - - name: Upload size:ci results - uses: actions/upload-artifact@v4 - with: - name: size-limit-results - path: | - output/build-artifacts/spotlight-size-output.txt - output/build-artifacts/design-size-output.txt + # - name: Upload size:ci results + # uses: actions/upload-artifact@v4 + # with: + # name: size-limit-results + # path: | + # output/build-artifacts/spotlight-size-output.txt + # output/build-artifacts/design-size-output.txt - name: Lint source code shell: bash @@ -99,6 +99,8 @@ jobs: - name: Initialize Terraform CDK configuration shell: bash working-directory: infra/cdktf + env: + NODE_OPTIONS: --max-old-space-size=4096 run: | pnpm cdktf get pnpm build:tsc diff --git a/.github/workflows/add-terraform-plan-to-pr.yml b/.github/workflows/add-terraform-plan-to-pr.yml index 818df188..18daf9bd 100644 --- a/.github/workflows/add-terraform-plan-to-pr.yml +++ b/.github/workflows/add-terraform-plan-to-pr.yml @@ -1,14 +1,15 @@ name: Post Terraform plan to PR comment on: - pull_request: - branches: - - demo - - main - types: - - opened - - synchronize - - reopened + workflow_dispatch: + # pull_request: + # branches: + # - demo + # - main + # types: + # - opened + # - synchronize + # - reopened jobs: add-terraform-plan-to-demo-pr: diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 00000000..1602a784 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,56 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + + Please review this pull request and provide feedback on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options + claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 00000000..b1a3201d --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options + # claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)' + diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9094443f..499d53b8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,5 +1,26 @@ name: 'Deploy' +# Triggers on pushes to main or demo branches +# Builds Docker image and pushes to: +# - GitHub Container Registry (ghcr.io) +# - AWS ECR (requires ECR repositories to exist first) +# +# PREREQUISITES: +# 1. Deploy infrastructure first to create ECR repositories: +# cd infra/cdktf +# pnpm deploy:flexion-sandbox-main # for main branch +# pnpm deploy:flexion-sandbox-demo # for demo branch +# +# 2. Configure GitHub secrets: +# - AWS_ACCOUNT_ID +# - AWS_ACCESS_KEY_ID +# - AWS_SECRET_ACCESS_KEY +# +# DEPLOYMENT FLOW: +# - Push to main branch → builds image → pushes to flexion-forms-sandbox-main ECR +# - Push to demo branch → builds image → pushes to flexion-forms-sandbox-demo ECR +# - App Runner auto-deploys when new images are detected (autoDeploymentsEnabled: true) + on: push: branches: @@ -8,7 +29,7 @@ on: workflow_dispatch: jobs: - build-image: + build-and-push-image: uses: ./.github/workflows/_docker-build-image.yml secrets: inherit with: @@ -16,10 +37,15 @@ jobs: tag-name: ${{ github.ref_name }} deploy-key: ${{ github.ref_name }} - deploy: - needs: [build-image] - uses: ./.github/workflows/_terraform-apply.yml - secrets: inherit - with: - deploy-env: ${{ github.ref_name }} - #deploy-env: main + # Terraform deployment is handled separately/manually for now + # To deploy infrastructure changes: + # cd infra/cdktf + # DEPLOY_ENV=flexion-sandbox-main pnpm deploy # or flexion-sandbox-demo + # + # Future: Automate Terraform deployment for AWS + # deploy-infrastructure: + # needs: [build-and-push-image] + # uses: ./.github/workflows/_terraform-apply.yml + # secrets: inherit + # with: + # deploy-env: aws-${{ github.ref_name }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0601b85c..62e8f983 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,10 +4,6 @@ on: push: branches: - main - inputs: - playwright-version: - description: "Set the Playwright version" - default: "1.51.1" concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -33,17 +29,6 @@ jobs: - name: Install dependencies run: pnpm install - - name: Cache Playwright binaries - uses: actions/cache@v4 - id: playwright-cache - with: - path: ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ inputs.playwright_version }} - - - name: Install Playwright - run: pnpm dlx playwright@${{ inputs.playwright_version }} install --with-deps - if: steps.playwright-cache.outputs.cache-hit != 'true' - - name: Build packages run: pnpm run build @@ -54,4 +39,4 @@ jobs: publish: pnpm release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + #NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 63742921..f982e064 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,6 +9,6 @@ on: jobs: run-tests: uses: ./.github/workflows/_validate.yml - e2e: - uses: ./.github/workflows/_end-to-end.yml - secrets: inherit \ No newline at end of file + # e2e: + # uses: ./.github/workflows/_end-to-end.yml + # secrets: inherit diff --git a/.husky/pre-commit b/.husky/pre-commit index 8db69672..bb7884ce 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,5 @@ #!/bin/sh pnpm lint pnpm format -pnpm test:ci +echo "*** NOTE: Running tests is temporarily disabled ***" +#pnpm test:ci diff --git a/.nvmrc b/.nvmrc index 517f3866..2c6984e9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v22.14.0 +v22.19.0 diff --git a/AGENTS.md b/AGENTS.md index b1b7b18d..97306736 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,17 @@ # Repository Guidelines -This guide helps contributors work effectively in the 10x Forms Platform monorepo. +This guide helps AI agents and contributors work effectively in the Forms Platform monorepo. + +## Documentation Index + +For comprehensive documentation, see [DOCS.md](./DOCS.md). + +**Quick links:** +- [Quick Reference](./documents/quick-reference.md) - Common commands and workflows +- [Patterns and Conventions](./documents/patterns-and-conventions.md) - Coding standards +- [Architecture Overview](./documents/architecture.md) - System design +- [Terminology](./documents/terminology.md) - Domain language +- [ADRs](./documents/adr/) - Architectural decisions ## Project Structure & Module Organization @@ -42,6 +53,17 @@ Tip: Tests that hit the database require Docker or Podman. Install Playwright br - Commits: follow Conventional Commits (e.g., `feat:`, `fix:`, `refactor:`). Include scope and ticket/issue (`TCKT-123`, `#123`) when relevant. - PRs: clear description, linked issues, screenshots for UI, tests updated, docs updated, and passing CI. One logical change per PR. +## Documentation Maintenance + +When making code changes, update relevant documentation in the same PR: +- Update package READMEs when public APIs change +- Create ADR for significant architectural decisions (use next number in sequence) +- Update [DOCS.md](./DOCS.md) when adding new documentation files +- Keep [Quick Reference](./documents/quick-reference.md) current with command changes +- Update [Patterns and Conventions](./documents/patterns-and-conventions.md) for new patterns + +See [ADR 0018: Documentation Strategy](./documents/adr/0018-documentation-strategy.md) for complete guidelines. + ## Security & Configuration Tips - Never commit secrets. Use `.env` files (see examples like `e2e/.env.sample`). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..5920dab4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,184 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Forms Platform is a forms-as-a-service platform for government organizations, enabling non-technical staff to create user-friendly "guided interview" web experiences. The platform serves two primary personas: +- **Form Builders**: Create and publish forms via a no-code browser interface +- **Form Fillers**: Complete forms created by form builders + +## Core Concepts + +- **Blueprint**: Defines the structure of an interactive session between government and user +- **Conversation**: A single instance of a blueprint (one interactive session) +- **Pattern/Template**: Building blocks of a blueprint, implementing UX best-practices +- **Prompt**: Produced by a pattern, defines what is presented to the user at a single point in a conversation +- **Component**: UI building block of prompts + +## Common Commands + +### Setup +```bash +pnpm install # Install dependencies +pnpm dlx playwright@1.51.1 install --with-deps # One-time: Install browsers for Vitest +``` + +### Development +```bash +pnpm build # Build all packages (required before dev) +pnpm dev # Start dev servers (Astro at :4321, Storybook at :61610) +``` + +### Testing +```bash +pnpm test # Run all tests (requires Docker/Podman for PostgreSQL) +pnpm vitest # Run tests in watch mode +pnpm test:ci # Run tests in CI mode +pnpm test:e2e:dev # Run E2E tests in dev mode +pnpm test:e2e:ci # Run E2E tests in CI mode +``` + +### Testing Individual Packages +```bash +pnpm --filter @flexion/forms-design test:watch # Watch mode for specific package +``` + +### Code Quality +```bash +pnpm lint # Lint all packages +pnpm format # Format code with Prettier +pnpm typecheck # Type-check all packages +``` + +### Cleanup +```bash +pnpm clean:dist # Remove all build artifacts recursively +pnpm clean:modules # Remove all node_modules recursively +``` + +### CLI Tool +```bash +./manage.sh --help # Access command-line operations +``` + +## Architecture + +### Monorepo Structure + +This is a pnpm workspace managed with Turborepo for efficient builds. The codebase is organized into packages and apps: + +**Packages** (in `/packages/`): +- `forms`: Core business logic, services, patterns, repository, and document handling + - `/src/services`: Public interface of Forms Platform + - `/src/patterns`: Form building blocks ("patterns") + - `/src/repository`: Database routines + - `/src/documents`: Document ingest and creation + - `/src/context`: Runtime contexts (testing, browser, server-side) +- `design`: User-facing React components, USWDS theme, Storybook stories +- `server`: Node.js web server built on Astro with Express adapter +- `auth`: Authentication and authorization (uses deprecated Lucia Auth with Arctic for Login.gov) +- `database`: PostgreSQL (production) and SQLite (testing) support with Knex migrations and Kysely queries +- `common`: Shared utilities + +**Apps** (in `/apps/`): +- `spotlight`: Main Astro website (http://localhost:4321/) +- `sandbox`: Testing/demo application +- `server-doj`: Department of Justice specific server instance +- `cli`: Command-line interface (accessed via `./manage.sh`) + +**Infrastructure** (in `/infra/`): +- `aws-cdk`: AWS CDK infrastructure code +- `cdktf`: Terraform CDK infrastructure code +- `core`: Shared infrastructure utilities + +### Dependency Flow +``` +server → auth, common, database, design, forms +forms → common, database +design → common, forms +auth → common, database +database → common +common → (no dependencies) +``` + +### Key Technologies + +- **Build System**: pnpm workspaces + Turborepo for efficient caching and builds +- **Frontend**: Astro (static site framework), React components, USWDS design system +- **Backend**: Node.js with Express +- **Database**: PostgreSQL (production), SQLite (testing) + - Query builders: Kysely (type-safe queries) and Knex.js (migrations) + - Testing: Testcontainers for Postgres unit tests, in-memory SQLite for integration tests +- **Auth**: Lucia Auth (deprecated) with Arctic for Login.gov OIDC/PKCE +- **Testing**: Vitest (unit/integration), Playwright (E2E), @vitest/browser (Storybook) +- **Language**: TypeScript throughout + +### Development Workflow with Conditional Exports + +This monorepo uses **conditional exports** for zero-build development workflow: + +**In Development:** +- Library packages (common, database, forms-core, auth, design) are consumed directly from TypeScript source files +- No build step required when editing library code - changes are immediately reflected in consuming apps +- Hot module replacement (HMR) works instantly across package boundaries +- Consumer apps (server, spotlight, etc.) are configured with `customConditions: ["development"]` in tsconfig.json +- Vite/Astro resolve the `development` export condition to use `./src/**/*.ts` files + +**In Production:** +- Library packages are built and published from `dist/` folders +- Production builds use optimized, transpiled artifacts +- The `development` export condition is not used + +**How it Works:** +Each library package.json has exports like: +```json +{ + "exports": { + ".": { + "development": { + "types": "./src/index.ts", + "import": "./src/index.ts" + }, + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + } +} +``` + +**Build Scripts:** +- **Important:** The `design` package requires CSS/SASS compilation: + - First time setup: Run `pnpm --filter @flexion/forms-design build:styles` (one-time) + - Or use `pnpm dev` which includes `dev:styles` (gulp watch) for the design package +- Production builds (`pnpm build`) are still required before publishing + +### Pattern System + +Patterns are the platform's primary building blocks. Each pattern has: +- `type`: String identifier for the pattern type +- `id`: Unique identifier for the pattern instance +- `data`: Configuration data specific to the pattern type + +Patterns can be constructed manually or via `PatternBuilder` helper classes. They are stored on the form `Blueprint`'s pattern attribute. + +## Testing Strategy + +- **Unit tests**: Service-level with in-memory SQLite via `createInMemoryDatabaseContext()` +- **Integration tests**: Database gateway logic tested against PostgreSQL Testcontainers +- **E2E tests**: Playwright tests for full user flows +- **Component tests**: Storybook + @vitest/browser + +Use `describeDatabase` helper for testing database routines against both SQLite and PostgreSQL. + +## Important Notes + +- Node version is specified in `.nvmrc` - use `nvm install` to ensure correct version +- Requires Docker or Podman for running tests (PostgreSQL container) +- Playwright version must match exactly (1.51.1) across local and CI environments +- **Development**: + - No TypeScript build required - packages are consumed from source via conditional exports + - CSS/styles must be built once: `pnpm --filter @flexion/forms-design build:styles` + - Or run `pnpm dev` which includes style watching +- **Production/Publishing**: Run `pnpm build` to create optimized artifacts before publishing +- Pre-commit hook runs `pnpm format` automatically diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 00000000..accbac40 --- /dev/null +++ b/DOCS.md @@ -0,0 +1,110 @@ +# Documentation Index + +This index helps you navigate all Forms Platform documentation. + +## Quick Start + +**New to the project?** Start here: +1. [README.md](./README.md) - Project overview and setup +2. [AGENTS.md](./AGENTS.md) - AI agent guidelines and repository structure +3. [Quick Reference](./documents/quick-reference.md) - Common commands and workflows +4. [Architecture Overview](./documents/architecture.md) - System design and component relationships + +**Using Claude Code?** See [CLAUDE.md](./CLAUDE.md) for Claude-specific guidance. + +## Documentation by Purpose + +### Understanding the System + +**Core Concepts** +- [Architecture Overview](./documents/architecture.md) - System design, packages, data flows +- [Terminology](./documents/terminology.md) - Domain language (blueprints, patterns, prompts, components) +- [DOJ Deployment Diagram](./documents/doj-diagram.md) - Department of Justice specific architecture + +**Patterns & Conventions** +- [Patterns and Conventions](./documents/patterns-and-conventions.md) - Coding standards, naming, architecture patterns +- [Pattern System](./packages/forms/src/patterns/README.md) - Form building blocks and pattern usage + +### Building and Developing + +**Getting Started** +- [Quick Reference](./documents/quick-reference.md) - Common commands, workflows, troubleshooting +- [Podman Integration](./documents/podman-integration.md) - Development environment setup with Podman/Docker + +**Package Documentation** +- [Forms Package](./packages/forms/README.md) - Core business logic, services, patterns +- [Design Package](./packages/design/README.md) - UI components and Storybook +- [Server Package](./packages/server/README.md) - Node.js web server (Astro + Express) +- [Auth Package](./packages/auth/README.md) - Authentication and authorization +- [Database Package](./packages/database/README.md) - PostgreSQL/SQLite with Kysely and Knex +- [Common Package](./packages/common/README.md) - Shared utilities + +**Application Documentation** +- [Spotlight App](./apps/spotlight/README.md) - Main Astro website +- [CLI App](./apps/cli/README.md) - Command-line interface +- [Sandbox App](./apps/sandbox/README.md) - Testing and demo application +- [Server DOJ App](./apps/server-doj/README.md) - Department of Justice server instance + +**Testing** +- [E2E Testing](./e2e/README.md) - Playwright end-to-end tests +- [ADR 0010: End-to-End Testing](./documents/adr/0010-end-to-end-testing.md) - E2E testing strategy + +### Operations and Deployment + +**Release and Deployment** +- [Release Process](./documents/release-process.md) - How to release new versions +- [ADR 0003: Initial Deployment Choices](./documents/adr/0003-initial-deployment-choices.md) +- [ADR 0004: Infrastructure as Code](./documents/adr/0004-infrastructure-as-code.md) + +**Infrastructure** +- [AWS CDK Infrastructure](./infra/aws-cdk/README.md) - AWS deployment +- [Terraform CDK Infrastructure](./infra/cdktf/README.md) - Terraform deployment +- [Core Infrastructure](./infra/core/README.md) - Shared infrastructure utilities + +**Security** +- [ADR 0011: Secrets Management](./documents/adr/0011-secrets-management.md) +- [ADR 0014: Authentication](./documents/adr/0014-authentication.md) + +### Architectural Decisions + +All architectural decisions are documented as ADRs in [documents/adr/](./documents/adr/). Key decisions: + +**System Architecture** +- [ADR 0001: Record Architecture Decisions](./documents/adr/0001-record-architecture-decisions.md) +- [ADR 0005: Build System](./documents/adr/0005-build-system.md) - Turborepo + pnpm workspaces +- [ADR 0013: Database Strategy](./documents/adr/0013-database-strategy.md) - PostgreSQL, SQLite, Kysely, Knex +- [ADR 0015: REST API](./documents/adr/0015-rest-api.md) +- [ADR 0018: Documentation Strategy](./documents/adr/0018-documentation-strategy.md) + +**Frontend & Design** +- [ADR 0006: Spotlight Frontend](./documents/adr/0006-spotlight-frontend.md) - Astro framework +- [ADR 0007: Initial CSS Strategy](./documents/adr/0007-initial-css-strategy.md) +- [ADR 0009: Design Assets Workflow](./documents/adr/0009-design-assets-workflow.md) +- [ADR 0012: Rich Text Editor](./documents/adr/0012-rich-text-editor.md) +- [ADR 0016: Unused CSS](./documents/adr/0016-unused-css.md) + +**Code Quality** +- [ADR 0017: Use Named Exports](./documents/adr/0017-use-named-exports.md) +- [ADR 0002: Generate Dependency Diagram](./documents/adr/0002-generate-dependency-diagram.md) + +**Domain Logic** +- [ADR 0008: Initial Form Handling Strategy](./documents/adr/0008-initial-form-handling-strategy.md) + +### Reference + +**Sample Documents** +- [California Unlawful Detainer](./packages/forms/sample-documents/ca-unlawful-detainer/README.md) +- [DOJ Pardon Marijuana](./packages/forms/sample-documents/doj-pardon-marijuana/README.md) + +**Work in Progress** +- [Pending Loose Ends](./documents/pending-loose-ends.md) - Known gaps and future work + +## Documentation Maintenance + +When making code changes: +1. Update relevant documentation in the same PR +2. Create ADR for significant architectural decisions +3. Update package READMEs when public APIs change +4. Keep this index current when adding new documentation + +See [ADR 0018: Documentation Strategy](./documents/adr/0018-documentation-strategy.md) for complete guidelines. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md index 1a7edeea..a35d3c96 100644 --- a/apps/cli/CHANGELOG.md +++ b/apps/cli/CHANGELOG.md @@ -1,5 +1,27 @@ # @gsa-tts/forms-cli +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-infra-core@0.2.0 + - @flexion/forms-auth@0.2.0 + - @flexion/forms-database@0.2.0 + +## 0.1.5 + +### Patch Changes + +- bbd065d: Change end-to-end tests to run against server rendered app + - @flexion/forms-infra-core@0.1.5 + - @flexion/forms-auth@0.1.3 + - @flexion/forms-database@0.1.3 + ## 0.1.4 ### Patch Changes diff --git a/apps/cli/README.md b/apps/cli/README.md index b4e56d04..31a24d44 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -1,4 +1,4 @@ -# @gsa-tts/forms-cli-app +# @flexion/forms-cli-app This package defines the platform's command-line interface. diff --git a/apps/cli/package.json b/apps/cli/package.json index 08a60bf9..18749786 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,10 +1,13 @@ { - "name": "@gsa-tts/forms-cli", - "version": "0.1.4", + "name": "@flexion/forms-cli", + "version": "0.2.0", "description": "10x Forms Platform command-line interface", "type": "module", - "license": "CC0", + "license": "Apache-2.0", "main": "src/index.ts", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "scripts": { "build": "tsup src/* --format esm", "clean": "rimraf dist tsconfig.tsbuildinfo coverage", @@ -13,9 +16,10 @@ "test": "vitest run --coverage" }, "dependencies": { - "@gsa-tts/forms-infra-core": "workspace:*", - "@gsa-tts/forms-auth": "workspace:^", - "@gsa-tts/forms-database": "workspace:*", + "@flexion/forms-infra-core": "workspace:*", + "@flexion/forms-auth": "workspace:^", + "@flexion/forms-database": "workspace:*", + "@flexion/forms-core": "workspace:*", "commander": "^11.1.0" } } diff --git a/apps/cli/src/cli-controller/e2e.ts b/apps/cli/src/cli-controller/e2e.ts index 30123aa5..1e52987c 100644 --- a/apps/cli/src/cli-controller/e2e.ts +++ b/apps/cli/src/cli-controller/e2e.ts @@ -2,7 +2,7 @@ import { promises as fs } from 'fs'; import { Command } from 'commander'; import { type Context } from './types.js'; -import { createTestDbSession, createE2eAuthContext } from '@gsa-tts/forms-auth'; +import { createTestDbSession, createE2eAuthContext } from '@flexion/forms-auth'; export const addE2eCommands = (ctx: Context, cli: Command) => { const cmd = cli @@ -41,4 +41,4 @@ export const addE2eCommands = (ctx: Context, cli: Command) => { process.exit(); } }); -}; \ No newline at end of file +}; diff --git a/apps/cli/src/cli-controller/forms.ts b/apps/cli/src/cli-controller/forms.ts new file mode 100644 index 00000000..c470727d --- /dev/null +++ b/apps/cli/src/cli-controller/forms.ts @@ -0,0 +1,60 @@ +import { promises as fs } from 'fs'; +import { Command } from 'commander'; + +import { type Context } from './types.js'; +import { createFormService, defaultFormConfig, parsePdf as parsePdfCore } from '@flexion/forms-core'; +import { createFormsRepository } from '@flexion/forms-core/repository'; +import { createTestPdfParser } from '@flexion/forms-core/documents/pdf/context'; +import { createFilesystemDatabaseContext } from '@flexion/forms-database/context'; + +export const addFormCommands = (ctx: Context, cli: Command) => { + const cmd = cli + .command('forms') + .description('form management commands') + .option('-d, --database ', 'Path to the dev sqlite3 database file. (Postgres currently not wired up.)', async databasePath => { + ctx.db = await createFilesystemDatabaseContext(databasePath); + const repository = createFormsRepository({ db: ctx.db, formConfig: defaultFormConfig }); + ctx.forms = createFormService({ + repository, + isUserLoggedIn: () => true, + config: defaultFormConfig, + parser: createTestPdfParser(), // Use test parser with filesystem cache for CLI + }); + }); + + cmd + .command('import-pdf') + .description('Intialize a new form by importing a PDF file') + .argument('', 'Source PDF file for form.') + .action(async inputFile => { + // For standalone import-pdf command, use test parser with caching + const parser = createTestPdfParser(); + const pdfBytes = await fs.readFile(inputFile); + const maybeForm = await parsePdfCore({ parser, formConfig: defaultFormConfig }, pdfBytes); + if (maybeForm === undefined) { + console.error('Error parsing PDF file:', inputFile); + return; + } + console.log(JSON.stringify(maybeForm, null, 2)); + }); + + cmd + .command('add') + .description('add a form') + .argument('', 'Source JSON file for form.') + .action(async inputFile => { + const fileContents = await fs.readFile(inputFile); + const fileName = inputFile.split(/[\\/]/).pop() ?? inputFile; + const result = await ctx.forms?.initializeForm({ + summary: { + title: `Imported Form: ${fileName}`, + description: 'Form imported from PDF', + }, + document: { + fileName: inputFile, + data: fileContents.toString('base64') + }, + }); + console.log(result); + }); +}; diff --git a/apps/cli/src/cli-controller/index.ts b/apps/cli/src/cli-controller/index.ts index 89d8a763..ddbb183c 100644 --- a/apps/cli/src/cli-controller/index.ts +++ b/apps/cli/src/cli-controller/index.ts @@ -1,8 +1,9 @@ import { Command } from 'commander'; -import type { Context } from './types.js'; -import { addSecretCommands } from './secrets.js'; import { addE2eCommands } from './e2e.js'; +import { addFormCommands } from './forms.js'; +import { addSecretCommands } from './secrets.js'; +import type { Context } from './types.js'; export const CliController = (ctx: Context) => { const cli = new Command().description( @@ -16,6 +17,7 @@ export const CliController = (ctx: Context) => { ctx.console.log('Hello!'); }); + addFormCommands(ctx, cli); addSecretCommands(ctx, cli); addE2eCommands(ctx, cli); diff --git a/apps/cli/src/cli-controller/secrets.ts b/apps/cli/src/cli-controller/secrets.ts index e1d9f884..94e715ad 100644 --- a/apps/cli/src/cli-controller/secrets.ts +++ b/apps/cli/src/cli-controller/secrets.ts @@ -1,14 +1,13 @@ import { promises as fs } from 'fs'; import path from 'path'; +import { fileURLToPath } from 'url'; import { Command } from 'commander'; -import { - type DeployEnv, - commands, - getSecretsVault, -} from '@gsa-tts/forms-infra-core'; +import { commands, getSecretsVault } from '@flexion/forms-infra-core'; import { type Context } from './types.js'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + export const addSecretCommands = (ctx: Context, cli: Command) => { const cmd = cli .command('secrets') @@ -79,17 +78,20 @@ export const addSecretCommands = (ctx: Context, cli: Command) => { .command('set-login-gov-keys') .description( 'generate and save login.gov keypair; if it already exists, it is not ' + - 'updated (future work might include adding key rotation)', + 'updated (future work might include adding key rotation)' + ) + .argument( + '', + 'root key for secrets (e.g., flexion-forms-demo, tts-10x-forms-dev)' ) - .argument('', 'deployment environment (dev, demo)') - .argument('', 'application key') - .action(async (env: DeployEnv, appKey: string) => { + .argument('', 'application key (e.g., server-doj, server-kansas)') + .action(async (rootKey: string, appKey: string) => { const vault = await getSecretsVault(ctx.file); - const secretsDir = path.resolve(__dirname, '../../../infra/secrets'); + const secretsDir = path.resolve(__dirname, '../../../infra/core'); const loginResult = await commands.setLoginGovSecrets( { vault, secretsDir }, - env, - appKey, + rootKey, + appKey ); if (loginResult.preexisting) { console.log('Keypair already exists.'); @@ -97,4 +99,4 @@ export const addSecretCommands = (ctx: Context, cli: Command) => { console.log('New keypair added'); } }); -}; \ No newline at end of file +}; diff --git a/apps/cli/src/cli-controller/types.ts b/apps/cli/src/cli-controller/types.ts index 90b95261..686058b3 100644 --- a/apps/cli/src/cli-controller/types.ts +++ b/apps/cli/src/cli-controller/types.ts @@ -1,5 +1,10 @@ +import type { FormService } from "@flexion/forms-core"; +import type { DatabaseContext } from "@flexion/forms-database"; + export type Context = { console: Console; workspaceRoot: string; file?: string; + db?: DatabaseContext; + forms?: FormService; }; diff --git a/apps/sandbox/CHANGELOG.md b/apps/sandbox/CHANGELOG.md index 19adc05c..a9d34fec 100644 --- a/apps/sandbox/CHANGELOG.md +++ b/apps/sandbox/CHANGELOG.md @@ -1,5 +1,44 @@ # @gsa-tts/forms-server-doj +## 0.2.3 + +### Patch Changes + +- @flexion/forms-server@0.2.3 + +## 0.2.2 + +### Patch Changes + +- @flexion/forms-server@0.2.2 + +## 0.2.1 + +### Patch Changes + +- @flexion/forms-server@0.2.1 + +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-infra-core@0.2.0 + - @flexion/forms-database@0.2.0 + - @flexion/forms-server@0.2.0 + +## 0.1.5 + +### Patch Changes + +- @flexion/forms-infra-core@0.1.5 +- @flexion/forms-database@0.1.3 +- @flexion/forms-server@0.1.3 + ## 0.1.4 ### Patch Changes diff --git a/apps/sandbox/README.md b/apps/sandbox/README.md index cb573592..a4569e94 100644 --- a/apps/sandbox/README.md +++ b/apps/sandbox/README.md @@ -1,3 +1,3 @@ -# @gsa-tts/forms-sandbox +# @flexion/forms-sandbox Sandbox application to evaluate platform functionality. diff --git a/apps/sandbox/package.json b/apps/sandbox/package.json index a9c20d4e..06a7faab 100644 --- a/apps/sandbox/package.json +++ b/apps/sandbox/package.json @@ -1,22 +1,25 @@ { - "name": "@gsa-tts/forms-sandbox", - "version": "0.1.4", + "name": "@flexion/forms-sandbox", + "version": "0.2.3", "description": "Form server sandbox for evaluating functionality.", "type": "module", - "license": "CC0", + "license": "Apache-2.0", "main": "src/index.ts", "private": true, + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "scripts": { "build": "tsup src/* --format esm", "clean": "rimraf dist tsconfig.tsbuildinfo coverage", "dev": "tsup src/* --watch --format esm", - "start": "VCAP_SERVICES='{\"aws-rds\": [{ \"credentials\": { \"uri\": \"\" }}]}' node dist/index.js", + "start": "node dist/index.js", "test": "vitest run --coverage" }, "dependencies": { - "@gsa-tts/forms-database": "workspace:*", - "@gsa-tts/forms-infra-core": "workspace:*", - "@gsa-tts/forms-server": "workspace:*" + "@flexion/forms-database": "workspace:*", + "@flexion/forms-infra-core": "workspace:*", + "@flexion/forms-server": "workspace:*" }, "devDependencies": { "@types/supertest": "^6.0.2", diff --git a/apps/sandbox/src/index.ts b/apps/sandbox/src/index.ts index b4874e4b..7dea5798 100644 --- a/apps/sandbox/src/index.ts +++ b/apps/sandbox/src/index.ts @@ -1,45 +1,61 @@ -import { createPostgresDatabaseContext } from '@gsa-tts/forms-database/context'; -import { getAWSSecretsManagerVault } from '@gsa-tts/forms-infra-core'; +import { createPostgresDatabaseContext } from '@flexion/forms-database/context'; +import { getAWSSecretsManagerVault } from '@flexion/forms-infra-core'; import { createCustomServer } from './server.js'; const port = process.env.PORT || 4321; -const getCloudGovServerSecrets = () => { - if (process.env.VCAP_SERVICES === undefined) { +const getAppRunnerSecrets = async () => { + const dbSecretEnv = process.env.DB_SECRET; + const dbHost = process.env.DB_HOST; + const dbPort = process.env.DB_PORT; + const dbName = process.env.DB_NAME; + + if (!dbSecretEnv || !dbHost || !dbPort || !dbName) { + console.error( + 'Missing required environment variables: DB_SECRET, DB_HOST, DB_PORT, DB_NAME' + ); return; } - const services = JSON.parse(process.env.VCAP_SERVICES || '{}'); - return { - //loginGovClientSecret: services['user-provided']?.credentials?.SECRET_LOGIN_GOV_PRIVATE_KEY, - dbUri: services['aws-rds'][0].credentials.uri as string, - }; -}; -const getAppRunnerSecrets = async () => { - const secrets = { - dbHost: process.env.DB_HOST, - dbPort: process.env.DB_PORT, - dbName: process.env.DB_NAME, - dbSecretArn: process.env.DB_SECRET_ARN, - } - if (secrets.dbHost === undefined || secrets.dbPort === undefined || secrets.dbName === undefined || secrets.dbSecretArn === undefined) { - return; + let dbSecretStr: string; + + // Check if DB_SECRET is already JSON (injected by App Runner) or an ARN + if (dbSecretEnv.startsWith('{')) { + // Already JSON - App Runner injected the secret value directly + console.log('Using secret value from environment variable'); + dbSecretStr = dbSecretEnv; + } else { + // It's an ARN - fetch from Secrets Manager + console.log('Fetching secret from Secrets Manager using ARN'); + const vault = getAWSSecretsManagerVault(); + const fetchedSecret = await vault.getSecret(dbSecretEnv); + + if (!fetchedSecret) { + console.error('Failed to retrieve secret from Secrets Manager'); + return; + } + dbSecretStr = fetchedSecret; } - const vault = getAWSSecretsManagerVault(); - const dbSecret = await vault.getSecret(secrets.dbSecretArn); - if (dbSecret === undefined) { - console.error('Error getting secret:', secrets.dbSecretArn); + const dbSecret = JSON.parse(dbSecretStr); + if (!dbSecret.username || !dbSecret.password) { + console.error( + 'Secret from Secrets Manager is missing username or password' + ); return; } - const secret = JSON.parse(dbSecret); + + // URL-encode credentials to handle special characters + const encodedUsername = encodeURIComponent(dbSecret.username); + const encodedPassword = encodeURIComponent(dbSecret.password); + return { - dbUri: `postgresql://${secret.username}:${secret.password}@${secret.dbHost}:${secret.dbPort}/${secret.dbName}` + dbUri: `postgresql://${encodedUsername}:${encodedPassword}@${dbHost}:${dbPort}/${dbName}`, }; }; -const secrets = getCloudGovServerSecrets() || (await getAppRunnerSecrets()); +const secrets = await getAppRunnerSecrets(); if (secrets === undefined) { console.error('Error getting secrets'); process.exit(1); diff --git a/apps/sandbox/src/server.ts b/apps/sandbox/src/server.ts index eb2ebbc0..840d1c00 100644 --- a/apps/sandbox/src/server.ts +++ b/apps/sandbox/src/server.ts @@ -1,5 +1,5 @@ -import { type DatabaseContext } from '@gsa-tts/forms-database'; -import { createServer } from '@gsa-tts/forms-server'; +import { type DatabaseContext } from '@flexion/forms-database'; +import { createServer } from '@flexion/forms-server'; export const createCustomServer = async (db: DatabaseContext): Promise => { return createServer({ @@ -13,7 +13,41 @@ export const createCustomServer = async (db: DatabaseContext): Promise => { //clientSecret: '', // secrets.loginGovClientSecret, }, isUserAuthorized: async (email: string) => { - return email.endsWith('.gov'); + // Flexion addresses + if (email.endsWith('@flexion.us')) { + return true; + } + + // Other authorized users + return [ + // Digital Public Ventures + 'jim@digitalpublic.ventures', + 'mike@digitalpublic.ventures', + + // Maryland Digital Service + 'syed.azeem@maryland.gov', + 'lauren.george@maryland.gov', + 'emil.leong@maryland.gov', + 'paul.roberts@maryland.gov', + + // GSA/TTS people + 'amber.vanamburg@gsa.gov', + 'bret.mogilefsky@gsa.gov', + 'chris.bisom@gsa.gov', + 'daniel.naab@gsa.gov', + 'daniela.aburto@gsa.gov', + 'elizabeth.ayer@gsa.gov', + 'john.jediny@gsa.gov', + 'nicholas.papafil@gsa.gov', + 'samantha.noor@gsa.gov', + 'tyler.burton@gsa.gov', + + // CEQ + 'david.y.yi@ceq.eop.gov', + 'Jordan.K.Eccles@ceq.eop.gov', + 'michael.r.drummond@ceq.eop.gov', + 'sophie.r.godfrey-mckee@ceq.eop.gov' + ].includes(email.toLowerCase()); }, }); }; diff --git a/apps/sandbox/tests/integration.test.ts b/apps/sandbox/tests/integration.test.ts index 64d2e083..ae6df6b9 100644 --- a/apps/sandbox/tests/integration.test.ts +++ b/apps/sandbox/tests/integration.test.ts @@ -1,7 +1,7 @@ import request from 'supertest'; import { describe, expect, test } from 'vitest'; -import { createInMemoryDatabaseContext } from '@gsa-tts/forms-database/context'; +import { createInMemoryDatabaseContext } from '@flexion/forms-database/context'; import { createCustomServer } from '../src/server'; diff --git a/apps/sandbox/tsconfig.json b/apps/sandbox/tsconfig.json index 11cf6557..09185aaf 100644 --- a/apps/sandbox/tsconfig.json +++ b/apps/sandbox/tsconfig.json @@ -4,7 +4,8 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", - "emitDeclarationOnly": true + "emitDeclarationOnly": true, + "customConditions": ["development"] }, "include": ["./src"], "references": [] diff --git a/apps/server-doj/CHANGELOG.md b/apps/server-doj/CHANGELOG.md index 19adc05c..a9d34fec 100644 --- a/apps/server-doj/CHANGELOG.md +++ b/apps/server-doj/CHANGELOG.md @@ -1,5 +1,44 @@ # @gsa-tts/forms-server-doj +## 0.2.3 + +### Patch Changes + +- @flexion/forms-server@0.2.3 + +## 0.2.2 + +### Patch Changes + +- @flexion/forms-server@0.2.2 + +## 0.2.1 + +### Patch Changes + +- @flexion/forms-server@0.2.1 + +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-infra-core@0.2.0 + - @flexion/forms-database@0.2.0 + - @flexion/forms-server@0.2.0 + +## 0.1.5 + +### Patch Changes + +- @flexion/forms-infra-core@0.1.5 +- @flexion/forms-database@0.1.3 +- @flexion/forms-server@0.1.3 + ## 0.1.4 ### Patch Changes diff --git a/apps/server-doj/README.md b/apps/server-doj/README.md index a673c36b..e671eb9a 100644 --- a/apps/server-doj/README.md +++ b/apps/server-doj/README.md @@ -1,3 +1,3 @@ -# @gsa-tts/forms-server-doj +# @flexion/forms-server-doj Web server to demonstrate forms for DOJ's Office of the Pardon Attorney. diff --git a/apps/server-doj/package.json b/apps/server-doj/package.json index d685542c..987b08fa 100644 --- a/apps/server-doj/package.json +++ b/apps/server-doj/package.json @@ -1,11 +1,14 @@ { - "name": "@gsa-tts/forms-server-doj", - "version": "0.1.4", + "name": "@flexion/forms-server-doj", + "version": "0.2.3", "description": "Form server instance for DOJ", "type": "module", - "license": "CC0", + "license": "Apache-2.0", "main": "src/index.ts", "private": true, + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "scripts": { "build": "tsup src/* --format esm", "clean": "rimraf dist tsconfig.tsbuildinfo coverage", @@ -14,9 +17,9 @@ "test": "vitest run --coverage" }, "dependencies": { - "@gsa-tts/forms-database": "workspace:*", - "@gsa-tts/forms-infra-core": "workspace:*", - "@gsa-tts/forms-server": "workspace:*" + "@flexion/forms-database": "workspace:*", + "@flexion/forms-infra-core": "workspace:*", + "@flexion/forms-server": "workspace:*" }, "devDependencies": { "@types/supertest": "^6.0.2", diff --git a/apps/server-doj/src/index.ts b/apps/server-doj/src/index.ts index b4874e4b..60809d15 100644 --- a/apps/server-doj/src/index.ts +++ b/apps/server-doj/src/index.ts @@ -1,5 +1,5 @@ -import { createPostgresDatabaseContext } from '@gsa-tts/forms-database/context'; -import { getAWSSecretsManagerVault } from '@gsa-tts/forms-infra-core'; +import { createPostgresDatabaseContext } from '@flexion/forms-database/context'; +import { getAWSSecretsManagerVault } from '@flexion/forms-infra-core'; import { createCustomServer } from './server.js'; diff --git a/apps/server-doj/src/server.ts b/apps/server-doj/src/server.ts index de8dae4f..cf6f3fd6 100644 --- a/apps/server-doj/src/server.ts +++ b/apps/server-doj/src/server.ts @@ -1,5 +1,5 @@ -import { type DatabaseContext } from '@gsa-tts/forms-database'; -import { createServer } from '@gsa-tts/forms-server'; +import { type DatabaseContext } from '@flexion/forms-database'; +import { createServer } from '@flexion/forms-server'; export const createCustomServer = async (db: DatabaseContext): Promise => { return createServer({ diff --git a/apps/server-doj/tests/integration.test.ts b/apps/server-doj/tests/integration.test.ts index 15b31955..a3566d57 100644 --- a/apps/server-doj/tests/integration.test.ts +++ b/apps/server-doj/tests/integration.test.ts @@ -1,7 +1,7 @@ import request from 'supertest'; import { describe, expect, test } from 'vitest'; -import { createInMemoryDatabaseContext } from '@gsa-tts/forms-database/context'; +import { createInMemoryDatabaseContext } from '@flexion/forms-database/context'; import { createCustomServer } from '../src/server'; diff --git a/apps/server-doj/tsconfig.json b/apps/server-doj/tsconfig.json index 11cf6557..09185aaf 100644 --- a/apps/server-doj/tsconfig.json +++ b/apps/server-doj/tsconfig.json @@ -4,7 +4,8 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", - "emitDeclarationOnly": true + "emitDeclarationOnly": true, + "customConditions": ["development"] }, "include": ["./src"], "references": [] diff --git a/apps/spotlight/CHANGELOG.md b/apps/spotlight/CHANGELOG.md index 5b6323fb..8aab6fc8 100644 --- a/apps/spotlight/CHANGELOG.md +++ b/apps/spotlight/CHANGELOG.md @@ -1,5 +1,49 @@ # @gsa-tts/forms-spotlight +## 0.2.3 + +### Patch Changes + +- Updated dependencies [82bb94d] +- Updated dependencies [f3bc441] + - @flexion/forms-design@0.2.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies + - @flexion/forms-design@0.2.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies + - @flexion/forms-design@0.2.1 + +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-common@0.2.0 + - @flexion/forms-design@0.2.0 + - @flexion/forms-core@0.2.0 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [bbd065d] + - @flexion/forms-common@0.1.3 + - @flexion/forms-design@0.1.3 + - @flexion/forms-core@0.1.3 + ## 0.1.3 ### Patch Changes diff --git a/apps/spotlight/astro.config.mjs b/apps/spotlight/astro.config.mjs index 01f796f2..50895063 100644 --- a/apps/spotlight/astro.config.mjs +++ b/apps/spotlight/astro.config.mjs @@ -21,6 +21,11 @@ export default defineConfig({ define: { 'import.meta.env.GITHUB': JSON.stringify(githubRepository), }, + resolve: { + conditions: process.env.NODE_ENV === 'production' + ? ['production', 'import', 'module', 'browser', 'default'] + : ['development', 'import', 'module', 'browser', 'default'], + }, }, }); diff --git a/apps/spotlight/package.json b/apps/spotlight/package.json index f11b3af8..1dbbac37 100644 --- a/apps/spotlight/package.json +++ b/apps/spotlight/package.json @@ -1,8 +1,11 @@ { - "name": "@gsa-tts/forms-spotlight", + "name": "@flexion/forms-spotlight", "type": "module", - "version": "0.1.3", + "version": "0.2.3", "private": true, + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "scripts": { "astro": "astro", "build": "astro build", @@ -24,9 +27,9 @@ ], "dependencies": { "@astrojs/react": "^3.6.1", - "@gsa-tts/forms-common": "workspace:*", - "@gsa-tts/forms-design": "workspace:*", - "@gsa-tts/forms-core": "workspace:*", + "@flexion/forms-common": "workspace:*", + "@flexion/forms-design": "workspace:*", + "@flexion/forms-core": "workspace:*", "astro": "^4.16.18", "qs": "^6.13.0", "react": "^18.3.1", diff --git a/apps/spotlight/src/components/AppAvailableFormList.tsx b/apps/spotlight/src/components/AppAvailableFormList.tsx index a85f48aa..af7d8892 100644 --- a/apps/spotlight/src/components/AppAvailableFormList.tsx +++ b/apps/spotlight/src/components/AppAvailableFormList.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import { AvailableFormList } from '@gsa-tts/forms-design'; +import { AvailableFormList } from '@flexion/forms-design'; import { getAppContext } from '../context.js'; import { getFormManagerUrlById, getFormUrl } from '../routes.js'; diff --git a/apps/spotlight/src/components/AppFormManager.tsx b/apps/spotlight/src/components/AppFormManager.tsx index ef1831e2..55b744d4 100644 --- a/apps/spotlight/src/components/AppFormManager.tsx +++ b/apps/spotlight/src/components/AppFormManager.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { FormManager } from '@gsa-tts/forms-design'; +import { FormManager } from '@flexion/forms-design'; import { getAppContext } from '../context.js'; import { getFormManagerUrlById, getFormUrl } from '../routes.js'; diff --git a/apps/spotlight/src/components/DemoHeader.astro b/apps/spotlight/src/components/DemoHeader.astro index 1c1d12a4..35a50b67 100644 --- a/apps/spotlight/src/components/DemoHeader.astro +++ b/apps/spotlight/src/components/DemoHeader.astro @@ -1,5 +1,5 @@ --- -import closeSvg from '@gsa-tts/forms-design/static/uswds/img/usa-icons/close.svg'; +import closeSvg from '@flexion/forms-design/static/uswds/img/usa-icons/close.svg'; import * as routes from '../routes'; const getNavLinkClasses = (url: string) => { diff --git a/apps/spotlight/src/components/Header.astro b/apps/spotlight/src/components/Header.astro index e1234dbd..ad2d8137 100644 --- a/apps/spotlight/src/components/Header.astro +++ b/apps/spotlight/src/components/Header.astro @@ -1,5 +1,5 @@ --- -import closeSvg from '@gsa-tts/forms-design/static/uswds/img/usa-icons/close.svg'; +import closeSvg from '@flexion/forms-design/static/uswds/img/usa-icons/close.svg'; import * as routes from '../routes'; import { Image } from 'astro:assets'; import { getPublicDirUrl } from '../routes'; diff --git a/apps/spotlight/src/components/UsaBanner.astro b/apps/spotlight/src/components/UsaBanner.astro index 6fbfd61e..2dd3f656 100644 --- a/apps/spotlight/src/components/UsaBanner.astro +++ b/apps/spotlight/src/components/UsaBanner.astro @@ -1,7 +1,7 @@ --- -import iconDotGov from '@gsa-tts/forms-design/static/uswds/img/icon-dot-gov.svg'; -import iconHttps from '@gsa-tts/forms-design/static/uswds/img/icon-https.svg'; -import usFlagSmall from '@gsa-tts/forms-design/static/uswds/img/us_flag_small.png'; +import iconDotGov from '@flexion/forms-design/static/uswds/img/icon-dot-gov.svg'; +import iconHttps from '@flexion/forms-design/static/uswds/img/icon-https.svg'; +import usFlagSmall from '@flexion/forms-design/static/uswds/img/us_flag_small.png'; ---
diff --git a/apps/spotlight/src/context.ts b/apps/spotlight/src/context.ts index 5ca2c938..2f06ee11 100644 --- a/apps/spotlight/src/context.ts +++ b/apps/spotlight/src/context.ts @@ -2,13 +2,13 @@ import { type FormConfig, type FormService, createFormService, - parsePdf, -} from '@gsa-tts/forms-core'; -import { defaultFormConfig } from '@gsa-tts/forms-core'; -import { BrowserFormRepository } from '@gsa-tts/forms-core/context'; + createSimpleFakeParser, +} from '@flexion/forms-core'; +import { defaultFormConfig } from '@flexion/forms-core'; +import { BrowserFormRepository } from '@flexion/forms-core/context'; import { type GithubRepository } from './lib/github.js'; -import { createTestBrowserFormService } from '@gsa-tts/forms-core/context'; +import { createTestBrowserFormService } from '@flexion/forms-core/context'; export type AppContext = { baseUrl: `${string}/`; @@ -44,7 +44,7 @@ const createAppFormService = () => { repository, config: defaultFormConfig, isUserLoggedIn: () => true, - parsePdf, + parser: createSimpleFakeParser(), }); } else { return createTestBrowserFormService(); diff --git a/apps/spotlight/src/features/form-page/components/AppFormPage.tsx b/apps/spotlight/src/features/form-page/components/AppFormPage.tsx index 4b0ae183..3ba7f697 100644 --- a/apps/spotlight/src/features/form-page/components/AppFormPage.tsx +++ b/apps/spotlight/src/features/form-page/components/AppFormPage.tsx @@ -8,11 +8,11 @@ import { useParams, } from 'react-router-dom'; -import { defaultPatternComponents, Form } from '@gsa-tts/forms-design'; +import { defaultPatternComponents, Form } from '@flexion/forms-design'; import { defaultFormConfig, getRouteDataFromQueryString, -} from '@gsa-tts/forms-core'; +} from '@flexion/forms-core'; import { getAppContext } from '../../../context.js'; import { useFormPageStore } from '../store/index.js'; diff --git a/apps/spotlight/src/features/form-page/store/actions/get-form-session.ts b/apps/spotlight/src/features/form-page/store/actions/get-form-session.ts index fdf83e59..1638d646 100644 --- a/apps/spotlight/src/features/form-page/store/actions/get-form-session.ts +++ b/apps/spotlight/src/features/form-page/store/actions/get-form-session.ts @@ -1,4 +1,4 @@ -import { type FormSession, type RouteData } from '@gsa-tts/forms-core'; +import { type FormSession, type RouteData } from '@flexion/forms-core'; import { type FormPageContext } from './index.js'; export type FormSessionResponse = diff --git a/apps/spotlight/src/features/form-page/store/actions/index.ts b/apps/spotlight/src/features/form-page/store/actions/index.ts index b5a6e33c..5eadc35a 100644 --- a/apps/spotlight/src/features/form-page/store/actions/index.ts +++ b/apps/spotlight/src/features/form-page/store/actions/index.ts @@ -1,4 +1,4 @@ -import { type ServiceMethod, createService } from '@gsa-tts/forms-common'; +import { type ServiceMethod, createService } from '@flexion/forms-common'; import { type AppContext, getAppContext } from '../../../../context.js'; import { type GetFormSession, getFormSession } from './get-form-session.js'; diff --git a/apps/spotlight/src/features/form-page/store/actions/initialize.ts b/apps/spotlight/src/features/form-page/store/actions/initialize.ts index 06d17edf..06e26213 100644 --- a/apps/spotlight/src/features/form-page/store/actions/initialize.ts +++ b/apps/spotlight/src/features/form-page/store/actions/initialize.ts @@ -1,4 +1,4 @@ -import { type FormRoute } from '@gsa-tts/forms-core'; +import { type FormRoute } from '@flexion/forms-core'; import { type FormPageContext } from './index.js'; import { getFormSession } from './get-form-session.js'; @@ -9,7 +9,7 @@ export type Initialize = ( export const initialize: Initialize = (ctx, opts) => { // Get the session ID from local storage so we can use it on page reload. - const sessionId = window.localStorage.getItem('form_session_id') || undefined; + const sessionId = window.localStorage.getItem(`form_session_id_${opts.formId}`) || undefined; getFormSession(ctx, { formId: opts.formId, route: opts.route, diff --git a/apps/spotlight/src/features/form-page/store/actions/on-submit-form.ts b/apps/spotlight/src/features/form-page/store/actions/on-submit-form.ts index 7a78214c..65d7422c 100644 --- a/apps/spotlight/src/features/form-page/store/actions/on-submit-form.ts +++ b/apps/spotlight/src/features/form-page/store/actions/on-submit-form.ts @@ -37,7 +37,7 @@ export const onSubmitForm: OnSubmitForm = async (ctx, opts) => { sessionId: submission.data.sessionId, }, }); - window.localStorage.setItem('form_session_id', submission.data.sessionId); + window.localStorage.setItem(`form_session_id_${opts.formId}`, submission.data.sessionId); } else { console.error(submission.error); } diff --git a/apps/spotlight/src/lib/initialize.ts b/apps/spotlight/src/lib/initialize.ts index 2c9f2d73..ddb01758 100644 --- a/apps/spotlight/src/lib/initialize.ts +++ b/apps/spotlight/src/lib/initialize.ts @@ -1,4 +1,4 @@ /** * Global initialization script. */ -import '@gsa-tts/forms-design'; +import '@flexion/forms-design'; diff --git a/apps/spotlight/src/styles.css b/apps/spotlight/src/styles.css index 40f8ac4f..26f6dc29 100644 --- a/apps/spotlight/src/styles.css +++ b/apps/spotlight/src/styles.css @@ -1 +1 @@ -@import '@gsa-tts/forms-design/static/uswds/styles/styles.css'; +@import '@flexion/forms-design/static/uswds/styles/styles.css'; diff --git a/apps/spotlight/tsconfig.json b/apps/spotlight/tsconfig.json index 7ea57e6d..70c020ab 100644 --- a/apps/spotlight/tsconfig.json +++ b/apps/spotlight/tsconfig.json @@ -6,7 +6,8 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "jsx": "react", - "resolveJsonModule": true + "resolveJsonModule": true, + "customConditions": ["development"] }, "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.astro"], "exclude": [".astro", "dist", "node_modules"] diff --git a/documents/adr/0007-initial-css-strategy.md b/documents/adr/0007-initial-css-strategy.md index ef725f9f..948c1648 100644 --- a/documents/adr/0007-initial-css-strategy.md +++ b/documents/adr/0007-initial-css-strategy.md @@ -14,12 +14,12 @@ The project team desires a method of managing CSS using a method that maximizes ## Decision -The project team will theme USWDS via an encapsulated build (`@gsa-tts/forms-design`). Any USWDS-related configuration or initialization will reside in this package. +The project team will theme USWDS via an encapsulated build (`@flexion/forms-design`). Any USWDS-related configuration or initialization will reside in this package. The Spotlight frontend will leverage this package via CSS imports. Where necessary, the Spotlight frontend application will use straight CSS. ## Consequences -There is a bit more pomp and circumstance required to leverage styles that are in a separate project (`@gsa-tts/forms-design`) than there is when importing SASS directly via Astro. +There is a bit more pomp and circumstance required to leverage styles that are in a separate project (`@flexion/forms-design`) than there is when importing SASS directly via Astro. This decision is easily reversed if there proves to not be benefit from the extra modularization. diff --git a/documents/adr/0009-design-assets-workflow.md b/documents/adr/0009-design-assets-workflow.md index f0a1f1de..45af9c0c 100644 --- a/documents/adr/0009-design-assets-workflow.md +++ b/documents/adr/0009-design-assets-workflow.md @@ -14,7 +14,7 @@ The project team requires a method of organizing frontend components that facili ## Decision -The project team will use [Storybook](https://storybook.js.org/) as development aid, component documentation, and collaboration tool. Storybook and corresponding React components will be located in the @gsa-tts/forms-design namespace. The Storybook build will be bundled with the Spotlight build and deployed to Cloud.gov Pages. +The project team will use [Storybook](https://storybook.js.org/) as development aid, component documentation, and collaboration tool. Storybook and corresponding React components will be located in the @flexion/forms-design namespace. The Storybook build will be bundled with the Spotlight build and deployed to Cloud.gov Pages. The Spotlight frontend will leverage this package via CSS imports. Where necessary, the Spotlight frontend application will use straight CSS. diff --git a/documents/adr/0018-documentation-strategy.md b/documents/adr/0018-documentation-strategy.md new file mode 100644 index 00000000..769f2648 --- /dev/null +++ b/documents/adr/0018-documentation-strategy.md @@ -0,0 +1,107 @@ +# 18. Documentation Strategy for AI Agents and Developers + +Date: 2025-10-05 + +## Status + +Accepted + +## Context + +Forms Platform requires documentation that serves two audiences: human developers and AI coding agents. Existing documentation includes READMEs, ADRs, AGENTS.md, CLAUDE.md, and various technical guides. However, the documentation lacks: + +1. A clear navigation structure for discovery +2. Consistent organization across documents +3. Optimization for AI agent context windows +4. Clear maintenance guidelines + +Research shows AI agents work best with: +- Progressive disclosure (index → summary → details) +- Context-efficient, modular documentation +- Specific, descriptive titles (not generic "Overview") +- Plain language with direct, unambiguous phrasing +- Clear cross-references between related concepts + +The AGENTS.md standard has emerged as best practice for AI coding agents, adopted by 20,000+ repositories. + +## Decision + +We implement a six-layer documentation architecture: + +### Layer 1: Navigation (Discovery) +- `DOCS.md` - Master documentation index with categorized links +- `AGENTS.md` - AI agent quick start and repository guidelines +- `CLAUDE.md` - Claude Code-specific configuration and patterns +- `README.md` - Project overview and getting started + +### Layer 2: Quick Reference (Common Tasks) +- `documents/quick-reference.md` - Commands, workflows, troubleshooting +- Organized by: Setup, Development, Testing, Deployment, Common Issues + +### Layer 3: Concepts & Patterns (How We Build) +- `documents/patterns-and-conventions.md` - Coding standards, architecture patterns +- `documents/terminology.md` - Domain language (ubiquitous language) +- `documents/architecture.md` - System architecture and component relationships + +### Layer 4: Decisions & History (Why We Build This Way) +- `documents/adr/` - Architecture Decision Records (numbered NNNN-title.md) +- Standard format: Status, Context, Decision, Consequences + +### Layer 5: Operations (Running the System) +- `documents/release-process.md` - Release workflow +- `documents/podman-integration.md` - Development environment setup +- Other operational guides as needed + +### Layer 6: Package-Specific (Deep Dives) +- Package-level READMEs in each workspace package +- Detailed API documentation and implementation notes + +### Documentation Standards + +**File Naming** +- Use descriptive, specific names: `authentication-flow.md` not `overview.md` +- Use kebab-case for filenames +- ADRs follow pattern: `NNNN-descriptive-title.md` + +**Content Structure** +- Start each document with one-sentence purpose statement +- Use consistent heading hierarchy (##, ###) +- Include "See also" sections for related documents +- Keep sentences short and direct +- Use bullet lists for scannability +- Provide code examples where helpful +- Avoid duplication - link to authoritative source + +**Maintenance Process** +- Documentation changes in same PR as related code changes +- AI agents must update relevant docs when implementing features +- Create new ADR for any significant architectural decision +- Regular documentation review to identify gaps and outdated content + +**AI Agent Optimization** +- Keep documents focused and modular (< 500 lines preferred) +- Use specific, searchable titles +- Include clear summary at document start +- Cross-reference related documents +- Avoid verbose explanations - prefer direct statements + +## Consequences + +### Positive +- AI agents can quickly discover relevant documentation via DOCS.md index +- Modular structure reduces context window usage +- Clear maintenance guidelines ensure documentation stays current +- Progressive disclosure serves both quick lookups and deep research +- Consistent structure reduces cognitive load +- Single source of truth reduces contradictions + +### Negative +- Requires initial effort to create new documentation +- Developers must maintain documentation alongside code +- Additional files to track in version control + +### Mitigation +- AI agents can generate initial documentation from existing code +- Documentation updates are mandatory part of code review +- Clear templates reduce friction for creating new docs +- Index structure makes it easy to find and update docs diff --git a/documents/patterns-and-conventions.md b/documents/patterns-and-conventions.md new file mode 100644 index 00000000..0026e813 --- /dev/null +++ b/documents/patterns-and-conventions.md @@ -0,0 +1,483 @@ +# Patterns and Conventions + +Coding standards and architectural patterns for Forms Platform. + +## TypeScript Conventions + +### Type System +- Strict TypeScript mode enabled (`"strict": true`) +- Use NodeNext module resolution +- Prefer type inference when possible +- Explicitly type function parameters and return values +- Use `type` for object types, `interface` for extensible contracts + +```typescript +// Good - explicit parameter and return types +export function getForm(ctx: Context, id: string): Promise> { + // implementation +} + +// Good - type alias for object shape +export type InputPattern = { + type: 'input'; + id: string; + data: InputData; +}; + +// Good - interface for extensible contract +export interface PatternConfig { + displayName: string; + initial: P['data']; + parseUserInput: (input: string) => Result; +} +``` + +### Imports and Exports + +**Use named exports** (see ADR 0017) +```typescript +// Good +export function FormManager() { ... } +export type FormData = { ... }; + +// Avoid (except for single-purpose files) +export default function FormManager() { ... } +``` + +**Import style** +```typescript +// Good - named imports +import { getForm, saveForm } from './repository'; +import { type Blueprint, type Pattern } from './types'; + +// Use `type` modifier for type-only imports +import { type Context } from './context'; +``` + +**File organization** +- Avoid `index.ts` barrel files - use descriptive filenames +- Exception: Package entry points (`packages/*/src/index.ts`) +- Co-locate related files (tests, types, utilities) + +``` +patterns/input/ + ├── config.ts # Pattern configuration types + ├── prompt.ts # Prompt generation logic + ├── response.ts # Response parsing logic + ├── builder.ts # Builder utility + └── index.ts # Pattern export (PatternConfig) +``` + +### Naming Conventions + +| Item | Convention | Example | +|------|------------|---------| +| Variables | camelCase | `const userName = 'alice'` | +| Functions | camelCase | `function parseInput() {}` | +| Classes | PascalCase | `class FormManager {}` | +| Types/Interfaces | PascalCase | `type UserData = {}` | +| Components | PascalCase | `function FormInput() {}` | +| Constants | UPPER_SNAKE_CASE | `const MAX_FILE_SIZE = 1024` | +| Files | kebab-case | `form-manager.ts` | +| Folders | kebab-case | `user-authentication/` | +| Packages | kebab-case | `@flexion/forms-design` | +| Test files | `*.test.ts(x)` | `form-manager.test.ts` | + +## Architecture Patterns + +### Pattern System + +Patterns are the building blocks of forms. Each pattern follows this structure: + +```typescript +// Pattern type definition +export type InputPattern = { + type: 'input'; // Pattern type identifier + id: string; // Unique instance ID + data: { // Pattern-specific configuration + label: string; + hint: string; + initial: string; + required: boolean; + }; +}; +``` + +**Pattern configuration** exports a `PatternConfig` object: +```typescript +export const inputConfig: PatternConfig = { + displayName: 'Short answer', + iconPath: 'short-answer-icon.svg', + initial: { + label: 'Question text', + hint: '', + initial: '', + required: false, + }, + parseUserInput, // Parse user response + parseConfigData, // Validate pattern data + getChildren, // Return child patterns + createPrompt, // Generate UI prompt +}; +``` + +**Pattern file structure:** +- `config.ts` - Type definitions and validation +- `prompt.ts` - UI prompt generation +- `response.ts` - User input parsing +- `index.ts` - PatternConfig export + +### Service Layer Pattern + +Services encapsulate business logic and coordinate between layers: + +```typescript +// Service function signature +export async function getForm( + ctx: FormServiceContext, + formId: string +): Promise> { + // 1. Validate input + // 2. Call repository layer + // 3. Transform data + // 4. Return result +} +``` + +**Context pattern** for dependency injection: +```typescript +export type FormServiceContext = { + db: DatabaseContext; + formConfig: FormConfig; +}; + +// Usage +const result = await getForm( + { db: dbContext, formConfig: defaultFormConfig }, + 'form-id' +); +``` + +### Repository Pattern + +Repository layer handles database operations: + +```typescript +export async function getForm( + ctx: DatabaseContext, + id: string +): Promise> { + const kysely = await ctx.getKysely(); + const row = await kysely + .selectFrom('forms') + .selectAll() + .where('id', '=', id) + .executeTakeFirst(); + + return { success: true, data: row ? parseForm(row) : null }; +} +``` + +### Result Pattern + +Use Result type for operations that can fail: + +```typescript +// Success result +return { success: true, data: form }; + +// Error result +return { + success: false, + error: { + type: 'validation-error', + message: 'Invalid form data' + } +}; + +// Handling results +const result = await getForm(ctx, id); +if (!result.success) { + console.error(result.error); + return; +} +const form = result.data; +``` + +## Testing Patterns + +### Unit Tests + +Co-locate tests with source code using `*.test.ts` suffix: + +```typescript +import { expect, it, describe } from 'vitest'; +import { parseInput } from './input'; + +describe('parseInput', () => { + it('parses valid input', () => { + const result = parseInput('hello'); + expect(result).toEqual({ success: true, data: 'hello' }); + }); + + it('rejects empty input', () => { + const result = parseInput(''); + expect(result.success).toBe(false); + }); +}); +``` + +### Database Tests + +Use `describeDatabase` for testing database operations against both SQLite and PostgreSQL: + +```typescript +import { expect, it } from 'vitest'; +import { type DbTestContext, describeDatabase } from '@flexion/forms-database/testing'; + +describeDatabase('getForm', () => { + it('retrieves form successfully', async ({ db }) => { + const kysely = await db.ctx.getKysely(); + + // Setup test data + await kysely.insertInto('forms').values({ + id: 'test-id', + data: JSON.stringify(testForm), + }).execute(); + + // Test the function + const result = await getForm({ db: db.ctx }, 'test-id'); + expect(result.success).toBe(true); + }); +}); +``` + +### Integration Tests + +Use in-memory database for business logic integration tests: + +```typescript +import { createInMemoryDatabaseContext } from '@flexion/forms-database/context'; + +it('creates and retrieves form', async () => { + const db = await createInMemoryDatabaseContext(); + const ctx = { db, formConfig: defaultFormConfig }; + + await saveForm(ctx, testForm); + const result = await getForm(ctx, testForm.id); + + expect(result.success).toBe(true); + expect(result.data).toEqual(testForm); +}); +``` + +### Component Tests + +Use Storybook stories for component development and testing: + +```typescript +// Component.stories.tsx +import type { Meta, StoryObj } from '@storybook/react'; +import { FormInput } from './FormInput'; + +const meta: Meta = { + component: FormInput, + title: 'Components/FormInput', +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Your name', + hint: 'Enter your full name', + }, +}; +``` + +### E2E Tests + +Use Playwright for end-to-end testing: + +```typescript +import { test, expect } from '@playwright/test'; + +test('user can create form', async ({ page }) => { + await page.goto('http://localhost:4321'); + await page.click('text=New Form'); + await page.fill('[name="title"]', 'Test Form'); + await page.click('button:has-text("Create")'); + + await expect(page).toHaveURL(/\/forms\/.+/); +}); +``` + +## Code Organization + +### Package Structure + +``` +packages/forms/ +├── src/ +│ ├── patterns/ # Form patterns +│ │ ├── input/ +│ │ │ ├── config.ts +│ │ │ ├── prompt.ts +│ │ │ ├── response.ts +│ │ │ └── index.ts +│ │ └── index.ts +│ ├── services/ # Business logic +│ ├── repository/ # Database operations +│ ├── documents/ # Document handling +│ ├── context/ # Runtime contexts +│ └── index.ts # Package exports +├── package.json +└── README.md +``` + +### Dependency Rules + +Packages follow strict dependency hierarchy (see architecture.md): + +``` +common ← database ← auth + ↑ ↑ + forms ← design + ↑ ↑ + └─ server +``` + +**Never introduce circular dependencies.** + +## Code Style + +### Formatting + +- Prettier is the source of truth +- Pre-commit hook runs `pnpm format` automatically +- 2 space indentation +- Single quotes for strings +- Semicolons required +- Trailing commas in multi-line structures + +### Linting + +- ESLint configured per package +- Run `pnpm lint` before committing +- Fix issues with `pnpm lint --fix` + +### Comments + +```typescript +// Good - explain why, not what +// Retry logic needed because Login.gov occasionally times out +const result = await retryWithBackoff(() => loginGov.authenticate()); + +// Avoid - redundant comments +// Get the user's name +const name = user.name; + +// Good - document complex types +/** + * Pattern configuration defining behavior and UI generation. + * + * @template P - Pattern type + * @template O - Output type from user input + */ +export interface PatternConfig { ... } +``` + +## Git Conventions + +### Commit Messages + +Follow Conventional Commits: + +``` +feat(forms): add email validation pattern +fix(design): correct button styling in dark mode +refactor(database): simplify migration logic +docs(patterns): update pattern creation guide +test(repository): add tests for form deletion +chore(deps): update dependencies +``` + +Format: `type(scope): description` + +Types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `style` + +### Pull Requests + +- One logical change per PR +- Link related issues +- Include tests for new features +- Update documentation +- Ensure CI passes +- Request review from relevant code owners + +## Error Handling + +### Validation Errors + +```typescript +// Return validation errors as Results +if (!email.includes('@')) { + return { + success: false, + error: { + type: 'validation-error', + message: 'Invalid email address', + }, + }; +} +``` + +### Exceptions + +```typescript +// Throw for programmer errors +if (!ctx.db) { + throw new Error('Database context required'); +} + +// Catch and convert to Result for runtime errors +try { + const data = await externalApi.fetch(); + return { success: true, data }; +} catch (error) { + return { + success: false, + error: { + type: 'api-error', + message: error instanceof Error ? error.message : 'Unknown error', + }, + }; +} +``` + +## Performance Considerations + +- Use database indexes for frequently queried fields +- Implement pagination for large result sets +- Lazy load components when appropriate +- Cache expensive computations +- Profile before optimizing + +## Security Practices + +- Never commit secrets (use `.env` files) +- Validate all user input +- Sanitize data before rendering +- Use parameterized queries (Kysely/Knex handles this) +- Follow principle of least privilege +- See ADR 0011 for secrets management strategy + +## See Also + +- [Architecture Overview](./architecture.md) - System design +- [Terminology](./terminology.md) - Domain language +- [Quick Reference](./quick-reference.md) - Common commands +- [ADR 0017: Use Named Exports](./adr/0017-use-named-exports.md) +- [ADR 0013: Database Strategy](./adr/0013-database-strategy.md) +- [ADR 0005: Build System](./adr/0005-build-system.md) diff --git a/documents/quick-reference.md b/documents/quick-reference.md new file mode 100644 index 00000000..1749be23 --- /dev/null +++ b/documents/quick-reference.md @@ -0,0 +1,216 @@ +# Quick Reference + +Common commands and workflows for Forms Platform development. + +## Initial Setup + +```bash +# Use correct Node version +nvm install # Install Node version from .nvmrc + +# Install dependencies +pnpm install # Install all workspace dependencies + +# One-time: Install Playwright browsers (version must match exactly) +pnpm dlx playwright@1.51.1 install --with-deps + +# Install Docker or Podman +# See: documents/podman-integration.md for Podman setup +``` + +## Development Workflow + +```bash +# Build all packages (required before dev) +pnpm build + +# Start development servers +pnpm dev # Astro (localhost:4321) + Storybook (localhost:61610) + +# Type checking +pnpm typecheck # Check all packages + +# Code quality +pnpm lint # Lint all packages +pnpm format # Format with Prettier (runs automatically on commit) +``` + +## Testing + +```bash +# Run all tests +pnpm test # Full test suite (requires Docker/Podman) + +# Watch mode +pnpm vitest # Watch mode for all packages + +# Test specific package +pnpm --filter @flexion/forms test # Test forms package +pnpm --filter @flexion/forms-design test:watch # Watch mode for design package + +# End-to-end tests +pnpm test:e2e:dev # E2E tests in dev mode +pnpm test:e2e:ci # E2E tests in CI mode +``` + +## Package Management + +```bash +# Add dependency to specific package +pnpm --filter @flexion/forms add lodash + +# Add dev dependency to workspace root +pnpm add -Dw prettier + +# Remove dependency +pnpm --filter @flexion/forms remove lodash + +# Update dependencies +pnpm update # Update all dependencies +``` + +## Build and Cleanup + +```bash +# Build commands +pnpm build # Build all packages via Turborepo +pnpm --filter @flexion/forms build # Build specific package + +# Cleanup commands +pnpm clean:dist # Remove all build artifacts recursively +pnpm clean:modules # Remove all node_modules recursively + +# Full reset workflow +pnpm clean:modules # Step 1: Remove node_modules +pnpm install # Step 2: Reinstall +pnpm build # Step 3: Rebuild all +``` + +## CLI Tools + +```bash +# Access command-line operations +./manage.sh --help # View available CLI commands +``` + +## Common Development Tasks + +### Adding a New Pattern + +1. Create pattern file in `packages/forms/src/patterns/` +2. Implement pattern interface with `type`, `id`, and `data` +3. Add pattern builder if needed +4. Export from `packages/forms/src/patterns/index.ts` +5. Add tests in `*.test.ts` file +6. Update pattern README + +### Creating a New Component + +1. Create component in `packages/design/src/components/` +2. Add Storybook story in `*.stories.tsx` +3. Add component tests +4. Export from `packages/design/src/index.ts` +5. Document props and usage + +### Adding a Database Migration + +1. Create migration in `packages/database/src/migrations/` +2. Use Knex migration format +3. Test against both PostgreSQL and SQLite +4. Update database schema types if needed + +### Running Specific App + +```bash +# Run specific application +pnpm --filter spotlight dev # Run Spotlight app +pnpm --filter sandbox dev # Run Sandbox app +pnpm --filter server-doj dev # Run DOJ server +``` + +## Troubleshooting + +### Build Errors + +**Symptom**: Unexplained build failures +```bash +# Solution: Clean and rebuild +pnpm clean:dist +pnpm clean:modules +pnpm install +pnpm build +``` + +### Test Failures + +**Symptom**: Database tests failing +- Ensure Docker/Podman is running +- Check PostgreSQL container is accessible +- Verify `.env` files are configured + +**Symptom**: Playwright tests failing +- Ensure browsers installed: `pnpm dlx playwright@1.51.1 install --with-deps` +- Verify version matches exactly (1.51.1) +- Check local and CI environments match + +### Development Server Issues + +**Symptom**: Dev server won't start +- Run `pnpm build` first (required before `pnpm dev`) +- Check ports 4321 and 61610 are available +- Clear build artifacts: `pnpm clean:dist` + +**Symptom**: Hot reload not working +- Restart dev server +- Check file watchers limit (Linux): `sudo sysctl fs.inotify.max_user_watches=524288` + +### Type Errors + +**Symptom**: TypeScript errors in IDE but not in CLI +- Restart TypeScript server in IDE +- Run `pnpm typecheck` to verify +- Check `tsconfig.json` is correctly configured + +### Dependency Issues + +**Symptom**: Module not found errors +- Verify package is in `dependencies` or `devDependencies` +- Run `pnpm install` again +- Check workspace protocol versions in `package.json` + +**Symptom**: Version conflicts +- Use `pnpm why ` to trace dependency tree +- Use `pnpm update` to resolve +- Check peer dependency requirements + +## Environment Variables + +Key environment variables (see `.env.sample` files in each app): + +- `DATABASE_URL` - PostgreSQL connection string +- `BASEURL` - Base URL for static builds +- `NODE_ENV` - Environment (development, production, test) + +## Important Files + +| File | Purpose | +|------|---------| +| `.nvmrc` | Node version specification | +| `pnpm-workspace.yaml` | Workspace configuration | +| `turbo.json` | Turborepo build configuration | +| `vitest.workspace.ts` | Vitest workspace configuration | +| `manage.sh` | CLI tool wrapper | + +## Getting Help + +- [DOCS.md](../DOCS.md) - Full documentation index +- [Architecture](./architecture.md) - System design +- [ADRs](./adr/) - Architectural decisions +- Package READMEs - Package-specific documentation + +## See Also + +- [AGENTS.md](../AGENTS.md) - Repository guidelines for AI agents +- [CLAUDE.md](../CLAUDE.md) - Claude Code specific guidance +- [Patterns and Conventions](./patterns-and-conventions.md) - Coding standards +- [Podman Integration](./podman-integration.md) - Container setup diff --git a/e2e/CHANGELOG.md b/e2e/CHANGELOG.md index bfe2a953..4d752e85 100644 --- a/e2e/CHANGELOG.md +++ b/e2e/CHANGELOG.md @@ -1,5 +1,24 @@ # @gsa-tts/forms-e2e +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-common@0.2.0 + +## 0.1.4 + +### Patch Changes + +- bbd065d: Change end-to-end tests to run against server rendered app +- Updated dependencies [bbd065d] + - @flexion/forms-common@0.1.3 + ## 0.1.3 ### Patch Changes diff --git a/e2e/README.md b/e2e/README.md index 4657dba0..056085b9 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -8,7 +8,7 @@ To run the tests, run `pnpm test` in this directory. In order to test the authenticated state, you can generate an auth session and the associated cookie. To authenticate for these tests, you can run the following command: ```bash -pnpm --filter=@gsa-tts/forms-cli cli e2e create-test-session -p ../../packages/server/src/main.db -o ../../e2e/.env +pnpm --filter=@flexion/forms-cli cli e2e create-test-session -p ../../packages/server/src/main.db -o ../../e2e/.env ``` ## Developing tests @@ -18,4 +18,4 @@ The easiest way to develop tests is to run Playwright in UI mode. This is availa pnpm test:dev ``` -When Playwright starts in UI mode, the UI will open automatically at `http://localhost:8080`. \ No newline at end of file +When Playwright starts in UI mode, the UI will open automatically at `http://localhost:8080`. diff --git a/e2e/package.json b/e2e/package.json index d0323bc8..82e617e2 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,9 +1,10 @@ { - "name": "@gsa-tts/forms-e2e", - "version": "0.1.3", + "name": "@flexion/forms-e2e", + "version": "0.2.0", "private": true, "scripts": { - "auth": "pnpm --filter=@gsa-tts/forms-cli cli e2e create-test-session -p ../../packages/server/src/main.db -o ../../e2e/.env", + "auth": "pnpm --filter=@flexion/forms-cli cli e2e create-test-session -p ../../packages/server/src/main.db -o ../../e2e/.env", + "clean": "rimraf coverage test-results playwright-report", "dev": "tsc -w", "test:e2e:ci": "pnpm auth && pnpm playwright test --headed", "test:e2e:dev": "pnpm auth && pnpm playwright test --ui-port=8080 --ui-host=0.0.0.0" @@ -14,6 +15,6 @@ "pdf-lib": "^1.17.1" }, "dependencies": { - "@gsa-tts/forms-common": "workspace:*" + "@flexion/forms-common": "workspace:*" } } diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index cfd515ba..658d019c 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -58,7 +58,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'pnpm --filter @gsa-tts/forms-server dev', + command: 'pnpm --filter @flexion/forms-server dev', url: process.env.E2E_ENDPOINT || 'http://localhost:4321', reuseExistingServer: !process.env.CI, }, diff --git a/e2e/src/fixtures/add-download.fixture.ts b/e2e/src/fixtures/add-download.fixture.ts index 98bb8f9a..d81aae33 100644 --- a/e2e/src/fixtures/add-download.fixture.ts +++ b/e2e/src/fixtures/add-download.fixture.ts @@ -1,5 +1,5 @@ import { test as base, expect } from './import-file.fixture'; -import { enLocale as message } from '@gsa-tts/forms-common'; +import { enLocale as message } from '@flexion/forms-common'; export type AddPackageDownloadFixture = { formUrl: string; @@ -39,4 +39,4 @@ const test = base.extend({ }, }); -export { test, expect }; \ No newline at end of file +export { test, expect }; diff --git a/e2e/src/models/form-create-page.ts b/e2e/src/models/form-create-page.ts index 383accbc..84404741 100644 --- a/e2e/src/models/form-create-page.ts +++ b/e2e/src/models/form-create-page.ts @@ -1,6 +1,6 @@ import { Locator, Page } from '@playwright/test'; import { expect } from '../fixtures/import-file.fixture.js'; -import { enLocale as message } from '@gsa-tts/forms-common'; +import { enLocale as message } from '@flexion/forms-common'; export class FormCreatePage { private readonly page: Page; @@ -74,4 +74,4 @@ export class FormCreatePage { await expect(pattern).not.toBeVisible(); } -} \ No newline at end of file +} diff --git a/infra/aws-cdk/CHANGELOG.md b/infra/aws-cdk/CHANGELOG.md index 0f684ffa..eafd1b9f 100644 --- a/infra/aws-cdk/CHANGELOG.md +++ b/infra/aws-cdk/CHANGELOG.md @@ -1,5 +1,22 @@ # @gsa-tts/forms-infra-aws-cdk +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-infra-core@0.2.0 + +## 0.1.13 + +### Patch Changes + +- @flexion/forms-infra-core@0.1.5 + ## 0.1.12 ### Patch Changes diff --git a/infra/aws-cdk/README.md b/infra/aws-cdk/README.md index 2cae929a..2c005c95 100644 --- a/infra/aws-cdk/README.md +++ b/infra/aws-cdk/README.md @@ -16,7 +16,7 @@ pnpm build ```bash #forms-apply-stack -r -e -cd node_modules/@gsa-tts/forms-infra-aws-cdk +cd node_modules/@flexion/forms-infra-aws-cdk pnpm cdk deploy \ --ci FormsPlatformStack \ --parameters "tagOrDigest=${TAG_OR_DIGEST}" \ diff --git a/infra/aws-cdk/lib/pipeline-stack/index.ts b/infra/aws-cdk/lib/pipeline-stack/index.ts index 3e887c50..4df16266 100644 --- a/infra/aws-cdk/lib/pipeline-stack/index.ts +++ b/infra/aws-cdk/lib/pipeline-stack/index.ts @@ -143,7 +143,7 @@ export class FormsPipelineStack extends cdk.Stack { }, build: { commands: [ - 'cd node_modules/@gsa-tts/forms-infra-aws-cdk', + 'cd node_modules/@flexion/forms-infra-aws-cdk', 'pnpm cdk deploy --ci FormsPlatformStack --parameters "tagOrDigest=${TAG_OR_DIGEST}" --parameters "environment=${ENVIRONMENT}" --parameters "repositoryName=${REPO_NAME}"', ], }, diff --git a/infra/aws-cdk/lib/platform-stack/index.ts b/infra/aws-cdk/lib/platform-stack/index.ts index 55c8b9b6..a8b4694a 100644 --- a/infra/aws-cdk/lib/platform-stack/index.ts +++ b/infra/aws-cdk/lib/platform-stack/index.ts @@ -9,7 +9,7 @@ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as changeCase from 'change-case'; import { Duration } from 'aws-cdk-lib'; -import { getDatabaseSecretKey } from '@gsa-tts/forms-infra-core'; +import { getDatabaseSecretKey } from '@flexion/forms-infra-core'; export class FormsPlatformStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { diff --git a/infra/aws-cdk/package.json b/infra/aws-cdk/package.json index 9db6cf44..f156ff31 100644 --- a/infra/aws-cdk/package.json +++ b/infra/aws-cdk/package.json @@ -1,6 +1,9 @@ { - "name": "@gsa-tts/forms-infra-aws-cdk", - "version": "0.1.12", + "name": "@flexion/forms-infra-aws-cdk", + "version": "0.2.0", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "bin": { "aws-cdk": "bin/aws-cdk.js", "forms-apply-stack": "bin/forms-apply-stack", @@ -19,12 +22,13 @@ "build:typescript": "tsc", "build:synth": "cdk synth", "cdk": "cdk", + "clean": "rimraf dist cdk.out tsconfig.tsbuildinfo coverage", "test": "echo 'no tests'", "watch": "tsc -w" }, "dependencies": { "@aws-cdk/aws-apprunner-alpha": "2.184.1-alpha.0", - "@gsa-tts/forms-infra-core": "workspace:*", + "@flexion/forms-infra-core": "workspace:*", "aws-cdk": "2.1004.0", "aws-cdk-lib": "2.184.1", "change-case": "^5.4.4", diff --git a/infra/cdktf/CHANGELOG.md b/infra/cdktf/CHANGELOG.md index fb4563d6..7f1121b5 100644 --- a/infra/cdktf/CHANGELOG.md +++ b/infra/cdktf/CHANGELOG.md @@ -1,5 +1,22 @@ # @gsa-tts/forms-infra-cdktf +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-infra-aws-cdk@0.2.0 + +## 0.1.13 + +### Patch Changes + +- @flexion/forms-infra-aws-cdk@0.1.13 + ## 0.1.12 ### Patch Changes diff --git a/infra/cdktf/README.md b/infra/cdktf/README.md index 2aad19fb..32e7a407 100644 --- a/infra/cdktf/README.md +++ b/infra/cdktf/README.md @@ -1,4 +1,4 @@ -# @gsa-tts/forms-infra +# @flexion/forms-infra Infrastructure-as-code (IaC) for the project, implemented with [Terraform CDK](https://github.com/hashicorp/terraform-cdk). @@ -16,11 +16,101 @@ To perform a deployment, ensure the current environment is configured with crede pnpm deploy ``` +## Deployment environments + +This project supports multiple deployment targets organized by platform: + +### Cloud.gov +- `cloud-gov-main`: Production deployment to Cloud.gov +- `cloud-gov-demo`: Demo deployment to Cloud.gov + +### AWS +- `flexion-sandbox-main`: Production deployment to AWS (App Runner + RDS) +- `flexion-sandbox-demo`: Demo deployment to AWS (App Runner + RDS) + ## Cloud services ### AWS -The Terraform state is maintained in an AWS S3 bucket. Also, some experimental integrations have at times been deployed to AWS. In order to apply the Terraform, you must have appropriate AWS credentials in your current environment. +The Terraform state is maintained in an AWS S3 bucket. AWS deployments use: +- **App Runner** for the containerized application +- **RDS PostgreSQL** for the database +- **VPC** with public subnets +- **Secrets Manager** for database credentials +- **ECR** for container images + +To deploy to AWS, you must have appropriate AWS credentials configured: + +```bash +export AWS_ACCESS_KEY_ID= +export AWS_SECRET_ACCESS_KEY= +export AWS_DEFAULT_REGION=us-east-1 +``` + +#### Deploying to AWS + +**Deployment Order:** + +1. **First: Deploy infrastructure** (creates ECR repositories, VPC, RDS, App Runner service) +2. **Second: Push Docker image** to ECR +3. **Auto-deploy: App Runner** detects new image and redeploys automatically + +##### Initial Infrastructure Deployment + +```bash +# Deploy infrastructure for demo environment +pnpm deploy:flexion-sandbox-demo + +# Deploy infrastructure for main/production environment +pnpm deploy:flexion-sandbox-main +``` + +This creates: +- ECR repository (`flexion-forms-sandbox-demo` or `flexion-forms-sandbox-main`) +- VPC with subnets and security groups +- RDS PostgreSQL database +- App Runner service (will fail to start until image is pushed) + +##### Manual Image Push + +After infrastructure is deployed, push your first image: + +```bash +# Get your AWS account ID +AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + +# Build the sandbox app Docker image +docker build --build-arg APP_DIR=sandbox -t sandbox:latest -f Dockerfile . + +# Authenticate to ECR +aws ecr get-login-password --region us-east-1 | \ + docker login --username AWS --password-stdin \ + ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com + +# For demo environment: +docker tag sandbox:latest ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/flexion-forms-sandbox-demo:latest +docker push ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/flexion-forms-sandbox-demo:latest + +# For main/production environment: +docker tag sandbox:latest ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/flexion-forms-sandbox-main:latest +docker push ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/flexion-forms-sandbox-main:latest +``` + +##### Automated Deployment via GitHub Actions + +Once the initial infrastructure and image are in place, GitHub Actions automatically builds and pushes new images on every commit to `main` or `demo` branches. + +**Prerequisites:** +Configure these secrets in GitHub repository settings: +- `AWS_ACCOUNT_ID` +- `AWS_ACCESS_KEY_ID` +- `AWS_SECRET_ACCESS_KEY` + +**Workflow:** +- Push to `main` branch → Builds image → Pushes to `flexion-forms-sandbox-main` ECR → App Runner auto-deploys +- Push to `demo` branch → Builds image → Pushes to `flexion-forms-sandbox-demo` ECR → App Runner auto-deploys + +See `.github/workflows/deploy.yml` for workflow configuration. ### Cloud.gov diff --git a/infra/cdktf/package.json b/infra/cdktf/package.json index 6435c231..fd1976ec 100644 --- a/infra/cdktf/package.json +++ b/infra/cdktf/package.json @@ -1,27 +1,35 @@ { - "name": "@gsa-tts/forms-infra-cdktf", - "version": "0.1.12", + "name": "@flexion/forms-infra-cdktf", + "version": "0.2.0", "description": "10x Forms Platform Terraform CDK", "main": "src/index.js", "types": "src/index.ts", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "scripts": { "build": "echo $PATH && pnpm build:tsc && pnpm build:synth", "build:get": "cdktf get", - "build:synth": "pnpm build:synth:main && pnpm build:synth:demo", - "build:synth:local": "DEPLOY_GIT_REF=main DEPLOY_ENV=main cdktf synth", - "build:synth:main": "DEPLOY_ENV=main cdktf synth", - "build:synth:demo": "DEPLOY_ENV=demo cdktf synth", + "build:synth": "pnpm build:synth:cloud-gov-main && pnpm build:synth:cloud-gov-demo && pnpm build:synth:flexion-sandbox-main && pnpm build:synth:flexion-sandbox-demo", + "build:synth:local": "DEPLOY_GIT_REF=main DEPLOY_ENV=cloud-gov-main cdktf synth", + "build:synth:cloud-gov-main": "DEPLOY_ENV=cloud-gov-main cdktf synth", + "build:synth:cloud-gov-demo": "DEPLOY_ENV=cloud-gov-demo cdktf synth", + "build:synth:flexion-sandbox-main": "DEPLOY_ENV=flexion-sandbox-main cdktf synth", + "build:synth:flexion-sandbox-demo": "DEPLOY_ENV=flexion-sandbox-demo cdktf synth", "build:tsc": "tsc --pretty", "clean": "rimraf cdktf.out dist tsconfig.tsbuildinfo", "clean:gen": "rimraf .gen", - "deploy:main": "DEPLOY_ENV=main cdktf deploy", - "deploy:demo": "DEPLOY_ENV=demo cdktf deploy", - "deploy:main:local": "DEPLOY_GIT_REF=main DEPLOY_ENV=main cdktf deploy", + "deploy:cloud-gov-main": "DEPLOY_ENV=cloud-gov-main cdktf deploy", + "deploy:cloud-gov-demo": "DEPLOY_ENV=cloud-gov-demo cdktf deploy", + "deploy:flexion-sandbox-main": "DEPLOY_ENV=flexion-sandbox-main cdktf deploy", + "deploy:flexion-sandbox-demo": "DEPLOY_ENV=flexion-sandbox-demo cdktf deploy", + "deploy:main:local": "DEPLOY_GIT_REF=main DEPLOY_ENV=cloud-gov-main cdktf deploy", "dev": "tsc -w", "test": "echo 'no tests'" }, "dependencies": { - "@gsa-tts/forms-infra-aws-cdk": "workspace:*", + "@flexion/forms-infra-aws-cdk": "workspace:*", + "@flexion/forms-infra-core": "workspace:*", "@aws-sdk/client-ssm": "^3.750.0", "cdktf": "^0.20.11", "cdktf-cli": "^0.20.11", diff --git a/infra/cdktf/src/index.ts b/infra/cdktf/src/index.ts index f7e01283..58d284cf 100644 --- a/infra/cdktf/src/index.ts +++ b/infra/cdktf/src/index.ts @@ -5,11 +5,17 @@ const app = new App(); const deployEnv = process.env.DEPLOY_ENV; switch (deployEnv) { - case 'main': - import('./spaces/main'); + case 'cloud-gov-main': + import('./spaces/cloud-gov/main'); break; - case 'demo': - import('./spaces/demo'); + case 'cloud-gov-demo': + import('./spaces/cloud-gov/demo'); + break; + case 'flexion-sandbox-main': + import('./spaces/aws/main'); + break; + case 'flexion-sandbox-demo': + import('./spaces/aws/demo'); break; default: throw new Error(`Please specify a valid environment (got: "${deployEnv}")`); diff --git a/infra/cdktf/src/lib/aws/sandbox-stack.ts b/infra/cdktf/src/lib/aws/sandbox-stack.ts new file mode 100644 index 00000000..0427c763 --- /dev/null +++ b/infra/cdktf/src/lib/aws/sandbox-stack.ts @@ -0,0 +1,419 @@ +import { Construct } from 'constructs'; +import { Fn } from 'cdktf'; + +import { Vpc } from '../../../.gen/providers/aws/vpc'; +import { Subnet } from '../../../.gen/providers/aws/subnet'; +import { InternetGateway } from '../../../.gen/providers/aws/internet-gateway'; +import { RouteTable } from '../../../.gen/providers/aws/route-table'; +import { RouteTableAssociation } from '../../../.gen/providers/aws/route-table-association'; +import { Route } from '../../../.gen/providers/aws/route'; +import { Eip } from '../../../.gen/providers/aws/eip'; +import { NatGateway } from '../../../.gen/providers/aws/nat-gateway'; +import { SecurityGroup } from '../../../.gen/providers/aws/security-group'; +import { DbSubnetGroup } from '../../../.gen/providers/aws/db-subnet-group'; +import { DbInstance } from '../../../.gen/providers/aws/db-instance'; +import { EcrRepository } from '../../../.gen/providers/aws/ecr-repository'; +import { ApprunnerVpcConnector } from '../../../.gen/providers/aws/apprunner-vpc-connector'; +import { ApprunnerService } from '../../../.gen/providers/aws/apprunner-service'; +import { IamRole } from '../../../.gen/providers/aws/iam-role'; +import { IamRolePolicy } from '../../../.gen/providers/aws/iam-role-policy'; +import { IamRolePolicyAttachment } from '../../../.gen/providers/aws/iam-role-policy-attachment'; +import { DataAwsAvailabilityZones } from '../../../.gen/providers/aws/data-aws-availability-zones'; + +interface SandboxStackConfig { + environment: string; +} + +export class SandboxStack extends Construct { + constructor(scope: Construct, id: string, config: SandboxStackConfig) { + super(scope, id); + + const { environment } = config; + + // Get availability zones + const azs = new DataAwsAvailabilityZones(this, `${id}-azs`, { + state: 'available', + }); + + // VPC + const vpc = new Vpc(this, `${id}-vpc`, { + cidrBlock: '10.0.0.0/16', + enableDnsHostnames: true, + enableDnsSupport: true, + tags: { + Name: `${id}-vpc`, + Environment: environment, + }, + }); + + // Internet Gateway + const igw = new InternetGateway(this, `${id}-igw`, { + vpcId: vpc.id, + tags: { + Name: `${id}-igw`, + Environment: environment, + }, + }); + + // Public Subnets (for App Runner VPC connector and RDS) + const publicSubnet1 = new Subnet(this, `${id}-public-subnet-1`, { + vpcId: vpc.id, + cidrBlock: '10.0.1.0/24', + availabilityZone: Fn.element(azs.names, 0), + mapPublicIpOnLaunch: true, + tags: { + Name: `${id}-public-subnet-1`, + Environment: environment, + }, + }); + + const publicSubnet2 = new Subnet(this, `${id}-public-subnet-2`, { + vpcId: vpc.id, + cidrBlock: '10.0.2.0/24', + availabilityZone: Fn.element(azs.names, 1), + mapPublicIpOnLaunch: true, + tags: { + Name: `${id}-public-subnet-2`, + Environment: environment, + }, + }); + + // Route table for public subnets + const publicRouteTable = new RouteTable(this, `${id}-public-rt`, { + vpcId: vpc.id, + tags: { + Name: `${id}-public-rt`, + Environment: environment, + }, + }); + + new Route(this, `${id}-public-route`, { + routeTableId: publicRouteTable.id, + destinationCidrBlock: '0.0.0.0/0', + gatewayId: igw.id, + }); + + new RouteTableAssociation(this, `${id}-public-rta-1`, { + subnetId: publicSubnet1.id, + routeTableId: publicRouteTable.id, + }); + + new RouteTableAssociation(this, `${id}-public-rta-2`, { + subnetId: publicSubnet2.id, + routeTableId: publicRouteTable.id, + }); + + // Private subnets for App Runner VPC connector + const privateSubnet1 = new Subnet(this, `${id}-private-subnet-1`, { + vpcId: vpc.id, + cidrBlock: '10.0.11.0/24', + availabilityZone: Fn.element(azs.names, 0), + tags: { + Name: `${id}-private-subnet-1`, + Environment: environment, + }, + }); + + const privateSubnet2 = new Subnet(this, `${id}-private-subnet-2`, { + vpcId: vpc.id, + cidrBlock: '10.0.12.0/24', + availabilityZone: Fn.element(azs.names, 1), + tags: { + Name: `${id}-private-subnet-2`, + Environment: environment, + }, + }); + + // Elastic IP for NAT Gateway + const natEip = new Eip(this, `${id}-nat-eip`, { + domain: 'vpc', + tags: { + Name: `${id}-nat-eip`, + Environment: environment, + }, + }); + + // NAT Gateway in public subnet + const natGateway = new NatGateway(this, `${id}-nat-gw`, { + allocationId: natEip.id, + subnetId: publicSubnet1.id, + tags: { + Name: `${id}-nat-gw`, + Environment: environment, + }, + }); + + // Route table for private subnets + const privateRouteTable = new RouteTable(this, `${id}-private-rt`, { + vpcId: vpc.id, + tags: { + Name: `${id}-private-rt`, + Environment: environment, + }, + }); + + new Route(this, `${id}-private-route`, { + routeTableId: privateRouteTable.id, + destinationCidrBlock: '0.0.0.0/0', + natGatewayId: natGateway.id, + }); + + new RouteTableAssociation(this, `${id}-private-rta-1`, { + subnetId: privateSubnet1.id, + routeTableId: privateRouteTable.id, + }); + + new RouteTableAssociation(this, `${id}-private-rta-2`, { + subnetId: privateSubnet2.id, + routeTableId: privateRouteTable.id, + }); + + // Security Groups + const appRunnerSecurityGroup = new SecurityGroup( + this, + `${id}-apprunner-sg`, + { + name: `${id}-apprunner-sg`, + description: 'Security group for App Runner service', + vpcId: vpc.id, + egress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + cidrBlocks: ['0.0.0.0/0'], + description: 'Allow all outbound traffic', + }, + ], + tags: { + Name: `${id}-apprunner-sg`, + Environment: environment, + }, + } + ); + + const rdsSecurityGroup = new SecurityGroup(this, `${id}-rds-sg`, { + name: `${id}-rds-sg`, + description: 'Allow postgres access from App Runner', + vpcId: vpc.id, + ingress: [ + { + fromPort: 5432, + toPort: 5432, + protocol: 'tcp', + securityGroups: [appRunnerSecurityGroup.id], + cidrBlocks: [], + ipv6CidrBlocks: [], + prefixListIds: [], + description: 'Allow postgres access from App Runner', + }, + ], + tags: { + Name: `${id}-rds-sg`, + Environment: environment, + }, + }); + + // Database username (password will be managed by RDS in Secrets Manager) + const dbUsername = 'postgres'; + + // RDS Subnet Group + const dbSubnetGroup = new DbSubnetGroup(this, `${id}-db-subnet-group`, { + name: `${id}-db-subnet-group`, + subnetIds: [publicSubnet1.id, publicSubnet2.id], + tags: { + Name: `${id}-db-subnet-group`, + Environment: environment, + }, + }); + + // RDS Instance with AWS-managed password in Secrets Manager + const rdsInstance = new DbInstance(this, `${id}-db`, { + identifier: `${id}-db`, + engine: 'postgres', + engineVersion: '15', + instanceClass: 'db.t3.micro', + allocatedStorage: 20, + maxAllocatedStorage: 100, + dbName: 'postgres', + username: dbUsername, + manageMasterUserPassword: true, + dbSubnetGroupName: dbSubnetGroup.name, + vpcSecurityGroupIds: [rdsSecurityGroup.id], + publiclyAccessible: false, + skipFinalSnapshot: true, + tags: { + Name: `${id}-db`, + Environment: environment, + }, + }); + + // ECR Repository + const ecrRepo = new EcrRepository(this, `${id}-ecr`, { + name: `${id}`, + imageTagMutability: 'MUTABLE', + imageScanningConfiguration: { + scanOnPush: true, + }, + tags: { + Environment: environment, + }, + }); + + // IAM Role for App Runner instance + const appRunnerInstanceRole = new IamRole( + this, + `${id}-apprunner-instance-role`, + { + name: `${id}-apprunner-instance-role`, + assumeRolePolicy: Fn.jsonencode({ + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'tasks.apprunner.amazonaws.com', + }, + Action: 'sts:AssumeRole', + }, + ], + }), + tags: { + Environment: environment, + }, + } + ); + + // Attach policy to read secrets + new IamRolePolicyAttachment( + this, + `${id}-apprunner-secrets-policy`, + { + role: appRunnerInstanceRole.name, + policyArn: + 'arn:aws:iam::aws:policy/SecretsManagerReadWrite', + } + ); + + // Attach inline policy for Bedrock model invocation + new IamRolePolicy(this, `${id}-apprunner-bedrock-policy`, { + name: `${id}-bedrock-invoke`, + role: appRunnerInstanceRole.name, + policy: Fn.jsonencode({ + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + Resource: '*', + }, + ], + }), + }); + + // IAM Role for App Runner access to ECR + const appRunnerAccessRole = new IamRole( + this, + `${id}-apprunner-access-role`, + { + name: `${id}-apprunner-access-role`, + assumeRolePolicy: Fn.jsonencode({ + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'build.apprunner.amazonaws.com', + }, + Action: 'sts:AssumeRole', + }, + ], + }), + tags: { + Environment: environment, + }, + } + ); + + // Attach ECR read policy + new IamRolePolicyAttachment( + this, + `${id}-apprunner-ecr-policy`, + { + role: appRunnerAccessRole.name, + policyArn: + 'arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess', + } + ); + + // App Runner VPC Connector + const vpcConnector = new ApprunnerVpcConnector( + this, + `${id}-vpc-connector`, + { + vpcConnectorName: `${id}-vpc-connector`, + subnets: [privateSubnet1.id, privateSubnet2.id], + securityGroups: [appRunnerSecurityGroup.id], + tags: { + Name: `${id}-vpc-connector`, + Environment: environment, + }, + } + ); + + // App Runner Service + new ApprunnerService(this, `${id}-apprunner-service`, { + serviceName: `${id}`, + sourceConfiguration: { + autoDeploymentsEnabled: true, + authenticationConfiguration: { + accessRoleArn: appRunnerAccessRole.arn, + }, + imageRepository: { + imageIdentifier: `${ecrRepo.repositoryUrl}:latest`, + imageRepositoryType: 'ECR', + imageConfiguration: { + port: '4321', + runtimeEnvironmentVariables: { + DB_HOST: rdsInstance.address, + DB_PORT: '5432', + DB_NAME: 'postgres', + }, + runtimeEnvironmentSecrets: { + DB_SECRET: `\${try(aws_db_instance.${rdsInstance.friendlyUniqueId}.master_user_secret[0].secret_arn, "")}`, + }, + }, + }, + }, + instanceConfiguration: { + instanceRoleArn: appRunnerInstanceRole.arn, + cpu: '1024', + memory: '2048', + }, + healthCheckConfiguration: { + protocol: 'HTTP', + path: '/', + interval: 10, + timeout: 10, + healthyThreshold: 1, + unhealthyThreshold: 5, + }, + networkConfiguration: { + egressConfiguration: { + egressType: 'VPC', + vpcConnectorArn: vpcConnector.arn, + }, + }, + tags: { + Name: `${id}-apprunner-service`, + Environment: environment, + }, + lifecycle: { + createBeforeDestroy: true, + }, + }); + } +} diff --git a/infra/cdktf/src/lib/backend.ts b/infra/cdktf/src/lib/backend.ts index 8e826150..5e4058eb 100644 --- a/infra/cdktf/src/lib/backend.ts +++ b/infra/cdktf/src/lib/backend.ts @@ -6,7 +6,7 @@ import { S3Backend, TerraformStack } from 'cdktf'; */ export const withBackend = (stack: TerraformStack, stackPrefix: string) => new S3Backend(stack, { - bucket: '10x-atj-tfstate', + bucket: 'flexion-forms-demo-sandbox-tfstate', key: `${stackPrefix}.tfstate`, - region: 'us-east-2', + region: 'us-east-1', }); diff --git a/infra/cdktf/src/spaces/aws/demo.ts b/infra/cdktf/src/spaces/aws/demo.ts new file mode 100644 index 00000000..4a4cdd50 --- /dev/null +++ b/infra/cdktf/src/spaces/aws/demo.ts @@ -0,0 +1,29 @@ +import { App, TerraformStack } from 'cdktf'; +import { Construct } from 'constructs'; + +import { AwsProvider } from '../../../.gen/providers/aws/provider'; +import { withBackend } from '../../lib/backend'; +import { SandboxStack } from '../../lib/aws/sandbox-stack'; + +const stackName = 'flexion-forms-sandbox-demo'; + +class AwsDemoStack extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + // Configure AWS provider + new AwsProvider(this, 'AWS', { + region: 'us-east-1', + }); + + // Create the sandbox infrastructure + new SandboxStack(this, stackName, { + environment: 'flexion-forms-demo', + }); + } +} + +const app = new App(); +const stack = new AwsDemoStack(app, stackName); +withBackend(stack, stackName); +app.synth(); diff --git a/infra/cdktf/src/spaces/aws/main.ts b/infra/cdktf/src/spaces/aws/main.ts new file mode 100644 index 00000000..b6d173f3 --- /dev/null +++ b/infra/cdktf/src/spaces/aws/main.ts @@ -0,0 +1,29 @@ +import { App, TerraformStack } from 'cdktf'; +import { Construct } from 'constructs'; + +import { AwsProvider } from '../../../.gen/providers/aws/provider'; +import { withBackend } from '../../lib/backend'; +import { SandboxStack } from '../../lib/aws/sandbox-stack'; + +const stackName = 'flexion-forms-sandbox-main'; + +class AwsMainStack extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + // Configure AWS provider + new AwsProvider(this, 'AWS', { + region: 'us-east-1', + }); + + // Create the sandbox infrastructure + new SandboxStack(this, stackName, { + environment: 'flexion-forms-main', + }); + } +} + +const app = new App(); +const stack = new AwsMainStack(app, stackName); +withBackend(stack, stackName); +app.synth(); diff --git a/infra/cdktf/src/spaces/demo.ts b/infra/cdktf/src/spaces/cloud-gov/demo.ts similarity index 77% rename from infra/cdktf/src/spaces/demo.ts rename to infra/cdktf/src/spaces/cloud-gov/demo.ts index fb0d5b04..7b70145e 100644 --- a/infra/cdktf/src/spaces/demo.ts +++ b/infra/cdktf/src/spaces/cloud-gov/demo.ts @@ -1,6 +1,6 @@ import { execSync } from 'child_process'; -import { registerAppStack } from '../lib/app-stack'; +import { registerAppStack } from '../../lib/app-stack'; const gitRef = process.env.DEPLOY_GIT_REF || diff --git a/infra/cdktf/src/spaces/main.ts b/infra/cdktf/src/spaces/cloud-gov/main.ts similarity index 77% rename from infra/cdktf/src/spaces/main.ts rename to infra/cdktf/src/spaces/cloud-gov/main.ts index 8e5f6638..0a400926 100644 --- a/infra/cdktf/src/spaces/main.ts +++ b/infra/cdktf/src/spaces/cloud-gov/main.ts @@ -1,6 +1,6 @@ import { execSync } from 'child_process'; -import { registerAppStack } from '../lib/app-stack'; +import { registerAppStack } from '../../lib/app-stack'; const gitRef = process.env.DEPLOY_GIT_REF || diff --git a/infra/core/CHANGELOG.md b/infra/core/CHANGELOG.md index bcc8da0c..3c63ea3b 100644 --- a/infra/core/CHANGELOG.md +++ b/infra/core/CHANGELOG.md @@ -1,5 +1,25 @@ # @gsa-tts/forms-infra-core +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-common@0.2.0 + - @flexion/forms-core@0.2.0 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies [bbd065d] + - @flexion/forms-common@0.1.3 + - @flexion/forms-core@0.1.3 + ## 0.1.4 ### Patch Changes diff --git a/infra/core/package.json b/infra/core/package.json index 7e36d39d..0b2f105b 100644 --- a/infra/core/package.json +++ b/infra/core/package.json @@ -1,11 +1,14 @@ { - "name": "@gsa-tts/forms-infra-core", - "version": "0.1.4", + "name": "@flexion/forms-infra-core", + "version": "0.2.0", "description": "10x Forms Platform core infrastructure management", "type": "module", - "license": "CC0", + "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.js", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "scripts": { "build": "tsc", "clean": "rimraf dist tsconfig.tsbuildinfo coverage", @@ -15,8 +18,8 @@ "dependencies": { "@aws-sdk/client-secrets-manager": "^3.758.0", "@aws-sdk/client-ssm": "^3.624.0", - "@gsa-tts/forms-common": "workspace:*", - "@gsa-tts/forms-core": "workspace:*", - "zod": "^3.23.8" + "@flexion/forms-common": "workspace:*", + "@flexion/forms-core": "workspace:*", + "zod": "^4.1.11" } } diff --git a/infra/core/src/commands/set-login-gov-secrets.test.ts b/infra/core/src/commands/set-login-gov-secrets.test.ts index 4534336b..d8cbfded 100644 --- a/infra/core/src/commands/set-login-gov-secrets.test.ts +++ b/infra/core/src/commands/set-login-gov-secrets.test.ts @@ -18,27 +18,32 @@ describe('set-login-gov-secrets command', () => { it('sets app secrets when uninitialized', async () => { const context = { vault: getTestVault({}), - secretsDir: path.resolve(__dirname, '../../../../infra/secrets'), + secretsDir: path.resolve(__dirname, '../../../../infra/core'), generateLoginGovKey: async () => ({ publicKey: 'mock public key', privateKey: 'mock private key', }), }; const appKey = randomUUID(); - const result = await setLoginGovSecrets(context, 'dev', appKey); + const result = await setLoginGovSecrets( + context, + 'flexion-forms-dev', + appKey + ); expect(result.preexisting).toEqual(false); expect( await context.vault.getSecrets(await context.vault.getSecretKeys()) ).toEqual({ - [`/tts-10x-forms-dev/${appKey}/login.gov/public-key`]: 'mock public key', - [`/tts-10x-forms-dev/${appKey}/login.gov/private-key`]: 'mock private key', + [`/flexion-forms-dev/${appKey}/login.gov/public-key`]: 'mock public key', + [`/flexion-forms-dev/${appKey}/login.gov/private-key`]: + 'mock private key', }); }); it('leaves initialized secrets as-is', async () => { const context = { vault: getTestVault({}), - secretsDir: path.resolve(__dirname, '../../../../infra/secrets'), + secretsDir: path.resolve(__dirname, '../../../../infra/core'), }; const appKey = randomUUID(); @@ -50,7 +55,7 @@ describe('set-login-gov-secrets command', () => { privateKey: 'mock private key - 1', }), }, - 'dev', + 'flexion-forms-dev', appKey ); const secondResult = await setLoginGovSecrets( @@ -61,7 +66,7 @@ describe('set-login-gov-secrets command', () => { privateKey: 'mock private key - 2', }), }, - 'dev', + 'flexion-forms-dev', appKey ); @@ -69,9 +74,9 @@ describe('set-login-gov-secrets command', () => { expect( await context.vault.getSecrets(await context.vault.getSecretKeys()) ).toEqual({ - [`/tts-10x-forms-dev/${appKey}/login.gov/public-key`]: + [`/flexion-forms-dev/${appKey}/login.gov/public-key`]: 'mock public key - 1', - [`/tts-10x-forms-dev/${appKey}/login.gov/private-key`]: + [`/flexion-forms-dev/${appKey}/login.gov/private-key`]: 'mock private key - 1', }); }); diff --git a/infra/core/src/commands/set-login-gov-secrets.ts b/infra/core/src/commands/set-login-gov-secrets.ts index afd95237..7cd3e7f1 100644 --- a/infra/core/src/commands/set-login-gov-secrets.ts +++ b/infra/core/src/commands/set-login-gov-secrets.ts @@ -3,7 +3,7 @@ import { promises as fs } from 'fs'; import { promisify } from 'util'; import { type SecretsVault } from '../lib/types.js'; -import { type DeployEnv, getAppLoginGovKeys } from '../values.js'; +import { getAppLoginGovKeys } from '../values.js'; const execPromise = promisify(exec); @@ -24,13 +24,17 @@ type Context = { /** * Sets or retrieves Login.gov secrets for the given application key. It retrieves and returns the * existing key pair or generates, stores, and returns new key pair if one didn't exist previously. + * + * @param ctx Context with vault and secrets directory + * @param rootKey The root key for secrets (e.g., 'flexion-forms-demo', 'tts-10x-forms-dev') + * @param appKey The application key (e.g., 'server-doj', 'server-kansas') */ export const setLoginGovSecrets = async ( ctx: Context, - env: DeployEnv, + rootKey: string, appKey: string ) => { - const loginKeys = getAppLoginGovKeys(env, appKey); + const loginKeys = getAppLoginGovKeys(rootKey, appKey); // If the keypair is already set, do nothing and return it. const existingPublicKey = await ctx.vault.getSecret(loginKeys.publicKey); diff --git a/infra/core/src/lib/adapters/index.ts b/infra/core/src/lib/adapters/index.ts index b0fbd6c7..7d49a9f7 100644 --- a/infra/core/src/lib/adapters/index.ts +++ b/infra/core/src/lib/adapters/index.ts @@ -1,6 +1,6 @@ import { promises as fs } from 'fs'; -import * as r from '@gsa-tts/forms-common'; +import * as r from '@flexion/forms-common'; import { AWSParameterStoreSecretsVault } from './aws-param-store.js'; import { getSecretMapFromJsonString, type SecretsVault } from '../types.js'; diff --git a/infra/core/src/lib/index.ts b/infra/core/src/lib/index.ts index 8702eb7a..00946a3a 100644 --- a/infra/core/src/lib/index.ts +++ b/infra/core/src/lib/index.ts @@ -2,6 +2,7 @@ import { type SecretMap, type SecretsVault } from './types.js'; export { getSecretMapFromJsonString } from './types.js'; export * from './adapters/index.js'; +export * from './secrets/index.js'; export const getSecretMap = async (vault: SecretsVault): Promise => { const secretKeys = await vault.getSecretKeys(); diff --git a/infra/core/src/lib/secrets/index.ts b/infra/core/src/lib/secrets/index.ts index 48a80085..7a7b8981 100644 --- a/infra/core/src/lib/secrets/index.ts +++ b/infra/core/src/lib/secrets/index.ts @@ -1,15 +1,13 @@ -export const getSecretKeys = (env: string) => [ - `/tts-10x-forms-${env}/cloudfoundry/password`, - `/tts-10x-forms-${env}/cloudfoundry/username`, - `/tts-10x-forms-${env}/server-doj/leidos-intranet-quorum/password`, - `/tts-10x-forms-${env}/server-doj/leidos-intranet-quorum/username`, - `/tts-10x-forms-${env}/server-doj/login.gov/private-key`, - `/tts-10x-forms-${env}/server-doj/login.gov/public-key`, - `/tts-10x-forms-${env}/server-kansas/login.gov/private-key`, - `/tts-10x-forms-${env}/server-kansas/login.gov/public-key`, +export const getSecretKeys = (rootKey: string) => [ + `/${rootKey}/cloudfoundry/password`, + `/${rootKey}/cloudfoundry/username`, + `/${rootKey}/server-doj/leidos-intranet-quorum/password`, + `/${rootKey}/server-doj/leidos-intranet-quorum/username`, + `/${rootKey}/server-doj/login.gov/private-key`, + `/${rootKey}/server-doj/login.gov/public-key`, + `/${rootKey}/server-kansas/login.gov/private-key`, + `/${rootKey}/server-kansas/login.gov/public-key`, + `/${rootKey}/database`, ]; -const secretPrefix = (env: string) => `/tts-10x-forms-${env}`; - -export const getDatabaseSecretKey = (env: string) => - `${secretPrefix(env)}/database`; +export const getDatabaseSecretKey = (rootKey: string) => `/${rootKey}/database`; diff --git a/infra/core/src/lib/types.ts b/infra/core/src/lib/types.ts index db474b84..bd16ce94 100644 --- a/infra/core/src/lib/types.ts +++ b/infra/core/src/lib/types.ts @@ -1,11 +1,11 @@ import * as z from 'zod'; -import { type Result } from '@gsa-tts/forms-common'; +import { type Result } from '@flexion/forms-common'; export type SecretKey = string; export type SecretValue = string | undefined; export type SecretMap = Record; -const secretMap = z.record(z.string()); +const secretMap = z.record(z.string(), z.string().optional()); export const getSecretMapFromJsonString = ( jsonString?: string diff --git a/infra/core/src/values.ts b/infra/core/src/values.ts index 725203e8..b86fe934 100644 --- a/infra/core/src/values.ts +++ b/infra/core/src/values.ts @@ -1,16 +1,16 @@ export type DeployEnv = 'dev' | 'demo'; -const getPathPrefix = (env: DeployEnv) => `/tts-10x-forms-${env}`; - /** * Generates an object containing the paths for private/public keys pairs * associated with login.gov for an application in the specified * deployment environment. + * + * @param rootKey The root key for secrets (e.g., 'flexion-forms-demo', 'tts-10x-forms-dev') + * @param appKey The application key (e.g., 'server-doj', 'server-kansas') */ -export const getAppLoginGovKeys = (env: DeployEnv, appKey: string) => { - const prefix = getPathPrefix(env); +export const getAppLoginGovKeys = (rootKey: string, appKey: string) => { return { - privateKey: `${prefix}/${appKey}/login.gov/private-key`, - publicKey: `${prefix}/${appKey}/login.gov/public-key`, + privateKey: `/${rootKey}/${appKey}/login.gov/private-key`, + publicKey: `/${rootKey}/${appKey}/login.gov/public-key`, }; }; diff --git a/manage.sh b/manage.sh index 6cebc247..85dffdef 100755 --- a/manage.sh +++ b/manage.sh @@ -1,3 +1,3 @@ #!/bin/sh -pnpm --filter @gsa-tts/forms-cli cli $@ +pnpm --filter @flexion/forms-cli cli "$@" diff --git a/package.json b/package.json index b2442a75..52d8a748 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "@gsa-tts/forms", + "name": "@flexion/forms", "version": "1.0.0-beta.1", "description": "10x Forms Platform", "type": "module", "main": "index.js", - "license": "CC0", + "license": "Apache-2.0", "packageManager": "pnpm@9.8.0", "scripts": { - "build": "turbo run build --filter=!@gsa-tts/forms-infra-cdktf --filter=!@gsa-tts/forms-infra-aws-cdk", + "build": "turbo run build --filter=!@flexion/forms-infra-cdktf --filter=!@flexion/forms-infra-aws-cdk", "clean": "turbo run clean", "clean:modules": "find $(git rev-parse --show-toplevel) -name 'node_modules' -type d -prune -exec rm -rf '{}' +", "clean:dist": "find $(git rev-parse --show-toplevel) -name 'dist' -type d -prune -exec rm -rf '{}' +", @@ -19,8 +19,8 @@ "release": "pnpm run build && changeset publish", "test": "vitest run", "test:ci": "CI=true vitest run # --coverage.enabled --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=json-summary --coverage.reporter=json --coverage.reportOnFailure", - "test:e2e:ci": "pnpm --filter @gsa-tts/forms-e2e test:e2e:ci", - "test:e2e:dev": "pnpm --filter @gsa-tts/forms-e2e test:e2e:dev", + "test:e2e:ci": "pnpm --filter @flexion/forms-e2e test:e2e:ci", + "test:e2e:dev": "pnpm --filter @flexion/forms-e2e test:e2e:dev", "test:infra": "turbo run --filter=infra-cdktf test", "typecheck": "tsc --build --noEmit" }, @@ -47,6 +47,7 @@ "rollup-plugin-typescript2": "^0.36.0", "ts-node": "^10.9.2", "tsup": "^8.3.0", + "tsx": "^4.20.6", "turbo": "^2.1.3", "typescript": "^5.7.3", "vitest": "^3.0.5", diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md index 847fd578..16e5263a 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -1,5 +1,25 @@ # @gsa-tts/forms-auth +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-common@0.2.0 + - @flexion/forms-database@0.2.0 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [bbd065d] + - @flexion/forms-common@0.1.3 + - @flexion/forms-database@0.1.3 + ## 0.1.2 ### Patch Changes diff --git a/packages/auth/package.json b/packages/auth/package.json index ea2a5adb..23d7efc3 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,11 +1,25 @@ { - "name": "@gsa-tts/forms-auth", - "version": "0.1.2", + "name": "@flexion/forms-auth", + "version": "0.2.0", "description": "10x Forms Platform auth module", "type": "module", - "license": "CC0", + "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.js", + "exports": { + ".": { + "development": { + "types": "./src/index.ts", + "import": "./src/index.ts" + }, + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "scripts": { "build": "tsc", "clean": "rimraf dist tsconfig.tsbuildinfo coverage", @@ -13,8 +27,8 @@ "test": "vitest run --coverage" }, "dependencies": { - "@gsa-tts/forms-common": "workspace:^", - "@gsa-tts/forms-database": "workspace:*", + "@flexion/forms-common": "workspace:^", + "@flexion/forms-database": "workspace:*", "@lucia-auth/adapter-postgresql": "^3.1.2", "@lucia-auth/adapter-sqlite": "^3.0.2", "arctic": "^1.9.2", diff --git a/packages/auth/src/context/e2e.test.ts b/packages/auth/src/context/e2e.test.ts index f601fcc0..c5270d0d 100644 --- a/packages/auth/src/context/e2e.test.ts +++ b/packages/auth/src/context/e2e.test.ts @@ -3,7 +3,7 @@ import { createE2eAuthContext } from './e2e.js'; import { BaseAuthContext } from './base.js'; // Mock imports -vi.mock('@gsa-tts/forms-database/context', () => ({ +vi.mock('@flexion/forms-database/context', () => ({ createFilesystemDatabaseContext: vi.fn().mockResolvedValue({}), })); diff --git a/packages/auth/src/context/e2e.ts b/packages/auth/src/context/e2e.ts index 68d7cbfa..501e37d5 100644 --- a/packages/auth/src/context/e2e.ts +++ b/packages/auth/src/context/e2e.ts @@ -4,7 +4,7 @@ import { createAuthRepository } from '../repository/index.js'; export const createE2eAuthContext = async (dbPath: string) => { // The login flow is to create fs db context -> feed into base auth context constructor -> feed it into the auth service const { createFilesystemDatabaseContext } = await import( - '@gsa-tts/forms-database/context' + '@flexion/forms-database/context' ); const dbContext = await createFilesystemDatabaseContext(dbPath); const authRepository = createAuthRepository(dbContext); diff --git a/packages/auth/src/context/test.ts b/packages/auth/src/context/test.ts index 5bbe262d..24beeebc 100644 --- a/packages/auth/src/context/test.ts +++ b/packages/auth/src/context/test.ts @@ -1,7 +1,7 @@ import { Cookie, Lucia } from 'lucia'; import { vi } from 'vitest'; -import { createInMemoryDatabaseContext } from '@gsa-tts/forms-database/context'; +import { createInMemoryDatabaseContext } from '@flexion/forms-database/context'; import { AuthServiceContext, UserSession } from '../index.js'; import { createSqliteLuciaAdapter } from '../lucia.js'; diff --git a/packages/auth/src/lucia.ts b/packages/auth/src/lucia.ts index 11b3cc52..5da98d30 100644 --- a/packages/auth/src/lucia.ts +++ b/packages/auth/src/lucia.ts @@ -3,7 +3,7 @@ import { BetterSqlite3Adapter } from '@lucia-auth/adapter-sqlite'; import { type Database as Sqlite3Database } from 'better-sqlite3'; import { Lucia } from 'lucia'; -import { type Database } from '@gsa-tts/forms-database'; +import { type Database } from '@flexion/forms-database'; /** * Factory function to create a SQLite Lucia adapter. diff --git a/packages/auth/src/repository/create-session.test.ts b/packages/auth/src/repository/create-session.test.ts index f34a0eec..7a069e76 100644 --- a/packages/auth/src/repository/create-session.test.ts +++ b/packages/auth/src/repository/create-session.test.ts @@ -3,7 +3,7 @@ import { expect, it } from 'vitest'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { createUser } from './create-user.js'; import { createSession } from './create-session.js'; diff --git a/packages/auth/src/repository/create-session.ts b/packages/auth/src/repository/create-session.ts index 085c1150..8903bc52 100644 --- a/packages/auth/src/repository/create-session.ts +++ b/packages/auth/src/repository/create-session.ts @@ -1,4 +1,4 @@ -import { type DatabaseContext, dateValue } from '@gsa-tts/forms-database'; +import { type DatabaseContext, dateValue } from '@flexion/forms-database'; type Session = { id: string; diff --git a/packages/auth/src/repository/create-user.test.ts b/packages/auth/src/repository/create-user.test.ts index adf9e635..b8afab57 100644 --- a/packages/auth/src/repository/create-user.test.ts +++ b/packages/auth/src/repository/create-user.test.ts @@ -3,7 +3,7 @@ import { expect, it } from 'vitest'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { createUser } from './create-user.js'; diff --git a/packages/auth/src/repository/create-user.ts b/packages/auth/src/repository/create-user.ts index d1a4cd20..123e4303 100644 --- a/packages/auth/src/repository/create-user.ts +++ b/packages/auth/src/repository/create-user.ts @@ -1,6 +1,6 @@ import { randomUUID } from 'crypto'; -import { type DatabaseContext } from '@gsa-tts/forms-database'; +import { type DatabaseContext } from '@flexion/forms-database'; /** * Asynchronously creates a new user record in the database. diff --git a/packages/auth/src/repository/get-user-id.test.ts b/packages/auth/src/repository/get-user-id.test.ts index ba659c64..262268b9 100644 --- a/packages/auth/src/repository/get-user-id.test.ts +++ b/packages/auth/src/repository/get-user-id.test.ts @@ -4,7 +4,7 @@ import { expect, it } from 'vitest'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { getUserId } from './get-user-id.js'; diff --git a/packages/auth/src/repository/get-user-id.ts b/packages/auth/src/repository/get-user-id.ts index 21bc2ebe..1c94f2d9 100644 --- a/packages/auth/src/repository/get-user-id.ts +++ b/packages/auth/src/repository/get-user-id.ts @@ -1,4 +1,4 @@ -import { type DatabaseContext } from '@gsa-tts/forms-database'; +import { type DatabaseContext } from '@flexion/forms-database'; /** * Retrieves the unique identifier (ID) of a user based on their email address. diff --git a/packages/auth/src/repository/index.ts b/packages/auth/src/repository/index.ts index 9b7c875e..afc6526a 100644 --- a/packages/auth/src/repository/index.ts +++ b/packages/auth/src/repository/index.ts @@ -1,5 +1,5 @@ -import { createService } from '@gsa-tts/forms-common'; -import { type DatabaseContext } from '@gsa-tts/forms-database'; +import { createService } from '@flexion/forms-common'; +import { type DatabaseContext } from '@flexion/forms-database'; import { createSession } from './create-session.js'; import { createUser } from './create-user.js'; diff --git a/packages/auth/src/services/index.ts b/packages/auth/src/services/index.ts index 23b25ad7..9b158d1f 100644 --- a/packages/auth/src/services/index.ts +++ b/packages/auth/src/services/index.ts @@ -1,6 +1,6 @@ import { Cookie, Lucia } from 'lucia'; -import { createService } from '@gsa-tts/forms-common'; +import { createService } from '@flexion/forms-common'; import { type UserSession, diff --git a/packages/auth/src/services/process-provider-callback.ts b/packages/auth/src/services/process-provider-callback.ts index ee108c28..2b9b8ac8 100644 --- a/packages/auth/src/services/process-provider-callback.ts +++ b/packages/auth/src/services/process-provider-callback.ts @@ -1,7 +1,7 @@ import { OAuth2RequestError } from 'arctic'; import { randomUUID } from 'crypto'; -import * as r from '@gsa-tts/forms-common'; +import * as r from '@flexion/forms-common'; import { type AuthServiceContext } from './index.js'; type LoginGovUser = { diff --git a/packages/auth/src/services/process-session-cookie.ts b/packages/auth/src/services/process-session-cookie.ts index e5a32153..72c87c53 100644 --- a/packages/auth/src/services/process-session-cookie.ts +++ b/packages/auth/src/services/process-session-cookie.ts @@ -1,6 +1,6 @@ import { verifyRequestOrigin } from 'lucia'; -import { type VoidResult } from '@gsa-tts/forms-common'; +import { type VoidResult } from '@flexion/forms-common'; import { type AuthServiceContext } from './index.js'; diff --git a/packages/auth/vitest.config.ts b/packages/auth/vitest.config.ts index 1e198670..0a61a98b 100644 --- a/packages/auth/vitest.config.ts +++ b/packages/auth/vitest.config.ts @@ -1,6 +1,6 @@ import { defineConfig, mergeConfig } from 'vitest/config'; -import { getVitestDatabaseContainerGlobalSetupPath } from '@gsa-tts/forms-database'; +import { getVitestDatabaseContainerGlobalSetupPath } from '@flexion/forms-database'; import sharedTestConfig from '../../vitest.shared'; export default mergeConfig( diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index ddf20648..e16ac2e2 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -1,5 +1,17 @@ # @gsa-tts/forms-common +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +## 0.1.3 + +### Patch Changes + +- bbd065d: Change end-to-end tests to run against server rendered app + ## 0.1.2 ### Patch Changes diff --git a/packages/common/README.md b/packages/common/README.md index 2cb5b9e0..a49d6d77 100644 --- a/packages/common/README.md +++ b/packages/common/README.md @@ -1 +1 @@ -# @gsa-tts/forms-common +# @flexion/forms-common diff --git a/packages/common/package.json b/packages/common/package.json index bc449c3a..afd87dae 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,11 +1,25 @@ { - "name": "@gsa-tts/forms-common", - "version": "0.1.2", + "name": "@flexion/forms-common", + "version": "0.2.0", "description": "10x Forms Platform shared resources", "type": "module", - "license": "CC0", + "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "development": { + "types": "./src/index.ts", + "import": "./src/index.ts" + }, + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "scripts": { "build": "tsc", "clean": "rimraf dist tsconfig.tsbuildinfo coverage", diff --git a/packages/database/CHANGELOG.md b/packages/database/CHANGELOG.md index 985ab384..2adea410 100644 --- a/packages/database/CHANGELOG.md +++ b/packages/database/CHANGELOG.md @@ -1,5 +1,23 @@ # @gsa-tts/forms-database +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-common@0.2.0 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [bbd065d] + - @flexion/forms-common@0.1.3 + ## 0.1.2 ### Patch Changes diff --git a/packages/database/README.md b/packages/database/README.md index a67f14b9..4ebc5fd6 100644 --- a/packages/database/README.md +++ b/packages/database/README.md @@ -1,4 +1,4 @@ -# @gsa-tts/forms-database +# @flexion/forms-database This package maintains the supporting infrastructure for the Forms Platform database. @@ -19,7 +19,7 @@ Application of database migrations are orchestrated by the application via ## Testing -Packages that leverage `@gsa-tts/forms-database` may use provided helpers for testing +Packages that leverage `@flexion/forms-database` may use provided helpers for testing purposes. ### Testing database gateway routines @@ -30,7 +30,7 @@ a clean database on both Sqlite3 and PostgreSQL: ```typescript import { expect, it } from 'vitest'; -import { type DbTestContext, describeDatabase } from '@gsa-tts/forms-database/testing'; +import { type DbTestContext, describeDatabase } from '@flexion/forms-database/testing'; describeDatabase('database connection', () => { it('selects all via kysely', async ({ db }) => { @@ -52,7 +52,7 @@ the `createInMemoryDatabaseContext` factory. This will provide an ephemeral in-memory Sqlite3 database. ```typescript -import { createInMemoryDatabaseContext } from '@gsa-tts/forms-database/context'; +import { createInMemoryDatabaseContext } from '@flexion/forms-database/context'; describe('business logic tested with in-memory database', () => { it('context helper has a connection to a sqlite database', async () => { diff --git a/packages/database/migrations/20251004224805_llm_request_cache.mjs b/packages/database/migrations/20251004224805_llm_request_cache.mjs new file mode 100644 index 00000000..bfb33f29 --- /dev/null +++ b/packages/database/migrations/20251004224805_llm_request_cache.mjs @@ -0,0 +1,27 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export async function up(knex) { + await knex.schema.createTable('llm_request_cache', table => { + table.increments('id').primary(); + table.string('cache_key', 64).notNullable().unique(); + table.text('response_data').notNullable(); + table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()); + table.timestamp('accessed_at').notNullable().defaultTo(knex.fn.now()); + table.integer('access_count').notNullable().defaultTo(1); + }); + + await knex.schema.table('llm_request_cache', table => { + table.index('accessed_at', 'idx_llm_cache_accessed'); + table.index('created_at', 'idx_llm_cache_created'); + }); +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export async function down(knex) { + await knex.schema.dropTableIfExists('llm_request_cache'); +} diff --git a/packages/database/migrations/20251007000000_create_form_jobs.mjs b/packages/database/migrations/20251007000000_create_form_jobs.mjs new file mode 100644 index 00000000..df23e6ae --- /dev/null +++ b/packages/database/migrations/20251007000000_create_form_jobs.mjs @@ -0,0 +1,58 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export async function up(knex) { + await knex.schema.createTable('form_jobs', table => { + table.uuid('id').primary(); + + // Foreign key to forms (CASCADE delete: jobs belong to forms) + table + .uuid('form_id') + .notNullable() + .references('id') + .inTable('forms') + .onDelete('CASCADE'); + + // Job type - extensible for future operations + table.text('job_type').notNullable(); + // Values: 'import-pdf', 'validate-schema', 'publish', 'export', etc. + + // Job status - represents operation state + table.text('status').notNullable(); + // Values: 'pending', 'processing', 'completed', 'failed' + + // Timing information + table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()); + table.timestamp('started_at').nullable(); + table.timestamp('completed_at').nullable(); + + // Error tracking + table.text('error_message').nullable(); + table.text('error_stack').nullable(); + + // Job metadata (input parameters, varies by job_type) + // For 'import-pdf': { documentId, fileName, userId } + // For 'publish': { targetEnvironment, publisherId } + table.text('metadata').nullable(); + + // Job result (output data, varies by job_type) + // For 'import-pdf': { patternsAdded: 5, fieldsExtracted: 12 } + // For 'validate': { errorsFound: 2, warningsFound: 5 } + table.text('result').nullable(); + + // Indexes for common queries + table.index('form_id', 'idx_form_jobs_form_id'); + table.index('status', 'idx_form_jobs_status'); + table.index(['form_id', 'job_type'], 'idx_form_jobs_form_type'); + table.index('created_at', 'idx_form_jobs_created'); + }); +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export async function down(knex) { + await knex.schema.dropTableIfExists('form_jobs'); +} diff --git a/packages/database/package.json b/packages/database/package.json index 5a2f0a0d..c98d43c8 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -1,24 +1,39 @@ { - "name": "@gsa-tts/forms-database", - "version": "0.1.2", + "name": "@flexion/forms-database", + "version": "0.2.0", "description": "10x Forms Platform database", "type": "module", - "license": "CC0", + "license": "Apache-2.0", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "dist/types/index.d.ts", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "exports": { ".": { + "development": { + "types": "./src/index.ts", + "import": "./src/index.ts" + }, "types": "./dist/types/index.d.ts", "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" }, "./context": { + "development": { + "types": "./src/context/index.ts", + "import": "./src/context/index.ts" + }, "types": "./dist/types/context/index.d.ts", "import": "./dist/esm/context.js", "require": "./dist/cjs/context.js" }, "./testing": { + "development": { + "types": "./src/testing.ts", + "import": "./src/testing.ts" + }, "types": "./dist/types/testing.d.ts", "import": "./dist/esm/testing.js", "require": "./dist/cjs/testing.js" @@ -31,7 +46,7 @@ "test": "vitest run --coverage" }, "dependencies": { - "@gsa-tts/forms-common": "workspace:*", + "@flexion/forms-common": "workspace:*", "@types/pg": "^8.11.6", "better-sqlite3": "^11.7.2", "knex": "^3.1.0", diff --git a/packages/database/src/clients/kysely/types.ts b/packages/database/src/clients/kysely/types.ts index 0c54b490..49a80c7b 100644 --- a/packages/database/src/clients/kysely/types.ts +++ b/packages/database/src/clients/kysely/types.ts @@ -16,6 +16,8 @@ export interface Database { forms: FormsTable; form_sessions: FormSessionsTable; form_documents: FormDocumentsTable; + form_jobs: FormJobsTable; + llm_request_cache: LlmRequestCacheTable; } export type DatabaseClient = Kysely; @@ -72,3 +74,32 @@ interface FormDocumentsTable { export type FormDocumentsTableSelectable = Selectable; export type FormDocumentsTableInsertable = Insertable; export type FormDocumentsTableUpdateable = Updateable; + +interface FormJobsTable { + id: string; + form_id: string; + job_type: string; + status: string; + created_at: DbDate; + started_at: DbDate | null; + completed_at: DbDate | null; + error_message: string | null; + error_stack: string | null; + metadata: string | null; + result: string | null; +} +export type FormJobsTableSelectable = Selectable; +export type FormJobsTableInsertable = Insertable; +export type FormJobsTableUpdateable = Updateable; + +interface LlmRequestCacheTable { + id: Generated; + cache_key: string; + response_data: string; + created_at: DbDate; + accessed_at: DbDate; + access_count: number; +} +export type LlmRequestCacheTableSelectable = Selectable; +export type LlmRequestCacheTableInsertable = Insertable; +export type LlmRequestCacheTableUpdateable = Updateable; diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts deleted file mode 100644 index 319fe17a..00000000 --- a/packages/database/src/schema.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type SessionSchema = { - id: string; - user_id: string; - session_token: string; - expires_at: number; - created_at: number; - updated_at: number; -}; diff --git a/packages/design/.eslintrc.cjs b/packages/design/.eslintrc.cjs index 0d5fa822..3ae371ac 100644 --- a/packages/design/.eslintrc.cjs +++ b/packages/design/.eslintrc.cjs @@ -28,4 +28,9 @@ module.exports = { rules: { 'react/prop-types': 'off', }, + settings: { + react: { + version: 'detect', + }, + }, }; diff --git a/packages/design/CHANGELOG.md b/packages/design/CHANGELOG.md index 28403260..eda1ae80 100644 --- a/packages/design/CHANGELOG.md +++ b/packages/design/CHANGELOG.md @@ -1,5 +1,45 @@ # @gsa-tts/forms-design +## 0.2.3 + +### Patch Changes + +- 82bb94d: Make form link in form list optional +- f3bc441: More aggressive refresh of forms list on AvailableFormList + +## 0.2.2 + +### Patch Changes + +- Include static assets and sass source in package + +## 0.2.1 + +### Patch Changes + +- Update main attribute in package.json + +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-common@0.2.0 + - @flexion/forms-core@0.2.0 + +## 0.1.3 + +### Patch Changes + +- bbd065d: Change end-to-end tests to run against server rendered app +- Updated dependencies [bbd065d] + - @flexion/forms-common@0.1.3 + - @flexion/forms-core@0.1.3 + ## 0.1.2 ### Patch Changes diff --git a/packages/design/README.md b/packages/design/README.md index 6b2bee0e..46650565 100644 --- a/packages/design/README.md +++ b/packages/design/README.md @@ -1,4 +1,4 @@ -# @gsa-tts/forms-design +# @flexion/forms-design This package encapsulates all the design components used in Forms Platform frontend applications. @@ -13,4 +13,4 @@ See relevant ADRs: - [documents/adr/0007-initial-css-strategy](../../documents/adr/0007-initial-css-strategy.md) - [documents/adr/0009-design-assets-workflow.md](../../documents/adr/0009-design-assets-workflow.md) -This package as a special watch task. If your dev server is running already (`pnpm dev`), you can open a separate terminal and run `pnpm test:watch` and any changes to the *.{ts,tsx} files in this package will run the test suite. If you'd like to run from the project root directory, you would run `pnpm --filter @gsa-tts/forms-design test:watch`. +This package as a special watch task. If your dev server is running already (`pnpm dev`), you can open a separate terminal and run `pnpm test:watch` and any changes to the *.{ts,tsx} files in this package will run the test suite. If you'd like to run from the project root directory, you would run `pnpm --filter @flexion/forms-design test:watch`. diff --git a/packages/design/package.json b/packages/design/package.json index af68635e..0386b52a 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,15 +1,31 @@ { - "name": "@gsa-tts/forms-design", - "version": "0.1.2", - "main": "src/index.ts", + "name": "@flexion/forms-design", + "version": "0.2.3", + "main": "dist/index.js", + "types": "dist/index.d.ts", "type": "module", + "exports": { + ".": { + "development": { + "types": "./src/index.ts", + "import": "./src/index.ts" + }, + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./*": "./*" + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "scripts": { "build": "run-p build:lib build:styles && pnpm build:storybook", "build:lib": "vite build", "build:storybook": "storybook build", "build:styles": "gulp update", "clean": "pnpm clean:lib && pnpm clean:styles", - "clean:lib": "rimraf dist", + "clean:lib": "rimraf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo coverage storybook-static", "clean:styles": "rimraf static", "dev": "run-p dev:*", "dev:lib": "vite", @@ -23,7 +39,9 @@ "test:watch": "pnpm onchange './**/*.{tsx,ts}' -- pnpm test:url" }, "files": [ - "dist/**/*" + "dist/**/*", + "static/**/*", + "sass/**/*" ], "size-limit": [ { @@ -71,8 +89,8 @@ "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", - "@gsa-tts/forms-common": "workspace:*", - "@gsa-tts/forms-core": "workspace:*", + "@flexion/forms-common": "workspace:*", + "@flexion/forms-core": "workspace:*", "@size-limit/preset-big-lib": "^11.1.6", "@tiptap/core": "^2.6.2", "@tiptap/react": "^2.6.2", diff --git a/packages/design/src/AvailableFormList/AvailableFormList.stories.tsx b/packages/design/src/AvailableFormList/AvailableFormList.stories.tsx index 05ad5cbf..e2873317 100644 --- a/packages/design/src/AvailableFormList/AvailableFormList.stories.tsx +++ b/packages/design/src/AvailableFormList/AvailableFormList.stories.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import type { Meta, StoryObj } from '@storybook/react'; -import { type FormService, createForm, nullSession } from '@gsa-tts/forms-core'; -import { createTestBrowserFormService } from '@gsa-tts/forms-core/context'; +import { type FormService, createForm, nullSession } from '@flexion/forms-core'; +import { createTestBrowserFormService } from '@flexion/forms-core/context'; import { FormManagerProvider } from '../FormManager/store.js'; import { createTestFormManagerContext } from '../test-form.js'; import AvailableFormList from './index.js'; diff --git a/packages/design/src/AvailableFormList/FormStatusBadge.tsx b/packages/design/src/AvailableFormList/FormStatusBadge.tsx new file mode 100644 index 00000000..594aab16 --- /dev/null +++ b/packages/design/src/AvailableFormList/FormStatusBadge.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import type { FormListItem } from '@flexion/forms-core'; +import styles from './formStatusBadge.module.css'; + +type FormStatusBadgeProps = { + form: FormListItem; +}; + +export const FormStatusBadge: React.FC = ({ form }) => { + const { latestJob } = form; + + // No job or completed job - no badge needed + if (!latestJob || latestJob.status === 'completed') { + return null; + } + + const isProcessing = latestJob.status === 'processing'; + const isFailed = latestJob.status === 'failed'; + + if (isProcessing) { + return ( + + + Processing + + ); + } + + if (isFailed) { + return ( + + + Import failed + + ); + } + + return null; +}; diff --git a/packages/design/src/AvailableFormList/formStatusBadge.module.css b/packages/design/src/AvailableFormList/formStatusBadge.module.css new file mode 100644 index 00000000..8ff3dcf3 --- /dev/null +++ b/packages/design/src/AvailableFormList/formStatusBadge.module.css @@ -0,0 +1,68 @@ +/* Badge container */ +.badge { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.875rem; /* 14px */ + font-weight: 600; + line-height: 1.2; + white-space: nowrap; +} + +/* Processing badge */ +.badgeProcessing { + background-color: #e7f6f8; /* USWDS info-lighter */ + color: #00687d; /* USWDS info-darker */ +} + +/* Failed badge */ +.badgeFailed { + background-color: #f4e3db; /* USWDS error-lighter */ + color: #b50909; /* USWDS red-warm-60v */ +} + +/* Spinner animation */ +.spinner { + width: 1rem; + height: 1rem; + animation: rotate 1s linear infinite; + flex-shrink: 0; +} + +.spinnerCircle { + stroke: currentColor; + stroke-dasharray: 40, 60; + stroke-dashoffset: 0; + animation: dash 1.5s ease-in-out infinite; + stroke-linecap: round; +} + +@keyframes rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes dash { + 0% { + stroke-dasharray: 1, 100; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 40, 100; + stroke-dashoffset: -20; + } + 100% { + stroke-dasharray: 40, 100; + stroke-dashoffset: -60; + } +} + +/* Error icon */ +.errorIcon { + width: 1rem; + height: 1rem; + flex-shrink: 0; +} diff --git a/packages/design/src/AvailableFormList/index.tsx b/packages/design/src/AvailableFormList/index.tsx index 8024ffef..18f7b0cf 100644 --- a/packages/design/src/AvailableFormList/index.tsx +++ b/packages/design/src/AvailableFormList/index.tsx @@ -1,16 +1,12 @@ -import React, { useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; +import React, { useEffect, useState, useRef } from 'react'; +import { Link, useLocation } from 'react-router-dom'; -import { type FormService } from '@gsa-tts/forms-core'; +import { type FormService, type FormListItem } from '@flexion/forms-core'; import * as AppRoutes from '../FormManager/routes.js'; +import { FormStatusBadge } from './FormStatusBadge.js'; -type FormDetails = { - id: string; - title: string; - description: string; -}; -export type UrlForForm = (id: string) => string; +export type UrlForForm = (id: string) => string | null; export type UrlForFormManager = UrlForForm; export default function AvailableFormList({ @@ -22,14 +18,48 @@ export default function AvailableFormList({ urlForForm: UrlForForm; urlForFormManager: UrlForFormManager; }) { - const [forms, setForms] = useState([]); - useEffect(() => { + const [forms, setForms] = useState([]); + const location = useLocation(); + const pollIntervalRef = useRef(null); + + const loadForms = React.useCallback(() => { formService.getFormList().then(result => { if (result.success) { setForms(result.data); } }); - }, []); + }, [formService]); + + useEffect(() => { + loadForms(); + }, [location.pathname, location.hash, location.key, loadForms]); + + // Poll if any forms are processing + useEffect(() => { + const hasProcessingForms = forms.some( + form => form.latestJob?.status === 'processing' + ); + + // Clear any existing interval + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + + // Start polling if needed + if (hasProcessingForms) { + pollIntervalRef.current = setInterval(() => { + loadForms(); + }, 3000); // Poll every 3 seconds + } + + return () => { + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + } + }; + }, [forms, loadForms]); + return ( <>
@@ -67,7 +97,7 @@ const FormList = ({ urlForForm, urlForFormManager, }: { - forms: FormDetails[]; + forms: FormListItem[]; urlForForm: UrlForForm; urlForFormManager: UrlForFormManager; }) => { @@ -97,35 +127,12 @@ const FormList = ({ ) : ( forms.map((form, index) => ( - - - {form.title} - - {form.description} - - - - + )) )} @@ -133,6 +140,112 @@ const FormList = ({ ); }; +const FormRow = ({ + form, + urlForForm, + urlForFormManager, +}: { + form: FormListItem; + urlForForm: UrlForForm; + urlForFormManager: UrlForFormManager; +}) => { + const [showError, setShowError] = React.useState(false); + + return ( + <> + + +
+ {form.title} + +
+ + {form.description} + + + + + {form.latestJob?.status === 'failed' && form.latestJob && ( + + +
+
+

+ Import error:{' '} + {form.latestJob.errorMessage || + 'An error occurred while processing this form.'}{' '} + +

+ {showError && form.latestJob.errorMessage && ( +
+ Technical details +
+                      {form.latestJob.errorMessage}
+                    
+
+ )} +
+
+ + + )} + + ); +}; + +const FormActions = ({ + form, + urlForForm, + urlForFormManager, +}: { + form: FormListItem; + urlForForm: UrlForForm; + urlForFormManager: UrlForFormManager; +}) => { + const formUrl = urlForForm(form.id); + const isProcessing = form.latestJob?.status === 'processing'; + + return ( +
+ {formUrl && !isProcessing && ( + + Go to form + + )} + + {isProcessing ? 'View' : 'Edit'} + + + Delete + +
+ ); +}; + const DebugTools = () => { return ( - -
- {previewPdfUrl ? ( - - ) : null} -
- - ); -}; diff --git a/packages/design/src/test-form.ts b/packages/design/src/test-form.ts index fef1b80f..d37b3e34 100644 --- a/packages/design/src/test-form.ts +++ b/packages/design/src/test-form.ts @@ -4,14 +4,14 @@ import { defaultFormConfig, type Blueprint, type Pattern, -} from '@gsa-tts/forms-core'; -import { createTestBrowserFormService } from '@gsa-tts/forms-core/context'; -import { type InputPattern } from '@gsa-tts/forms-core'; -import { type PagePattern } from '@gsa-tts/forms-core'; -import { type PageSetPattern } from '@gsa-tts/forms-core'; -import { type SequencePattern } from '@gsa-tts/forms-core'; - -import { type FormSummaryPattern } from '@gsa-tts/forms-core'; +} from '@flexion/forms-core'; +import { createTestBrowserFormService } from '@flexion/forms-core/context'; +import { type InputPattern } from '@flexion/forms-core'; +import { type PagePattern } from '@flexion/forms-core'; +import { type PageSetPattern } from '@flexion/forms-core'; +import { type SequencePattern } from '@flexion/forms-core'; + +import { type FormSummaryPattern } from '@flexion/forms-core'; import { type FormUIContext } from './Form/types.js'; import { defaultPatternComponents } from './Form/components/index.js'; import { defaultPatternEditComponents } from './FormManager/FormEdit/components/index.js'; diff --git a/packages/forms/CHANGELOG.md b/packages/forms/CHANGELOG.md index 9d9d703c..370c7db9 100644 --- a/packages/forms/CHANGELOG.md +++ b/packages/forms/CHANGELOG.md @@ -1,5 +1,25 @@ # @gsa-tts/forms-core +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-common@0.2.0 + - @flexion/forms-database@0.2.0 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [bbd065d] + - @flexion/forms-common@0.1.3 + - @flexion/forms-database@0.1.3 + ## 0.1.2 ### Patch Changes diff --git a/packages/forms/README.md b/packages/forms/README.md index a00dab18..5482575f 100644 --- a/packages/forms/README.md +++ b/packages/forms/README.md @@ -1,4 +1,4 @@ -# @gsa-tts/forms-core +# @flexion/forms-core This library includes all of the core business logic of Forms Platform. diff --git a/packages/forms/fixtures/ai-cache/8f/8fd9b9039c736540e0f3becade55e2d36a3d5afe3f448c8efdb86b10525cdfd4.json b/packages/forms/fixtures/ai-cache/8f/8fd9b9039c736540e0f3becade55e2d36a3d5afe3f448c8efdb86b10525cdfd4.json new file mode 100644 index 00000000..c9a61f9d --- /dev/null +++ b/packages/forms/fixtures/ai-cache/8f/8fd9b9039c736540e0f3becade55e2d36a3d5afe3f448c8efdb86b10525cdfd4.json @@ -0,0 +1,2109 @@ +{ + "object": { + "form_summary": { + "title": "Application for Presidential Pardon After Completion of Sentence", + "description": "Apply for a presidential pardon for a federal conviction. You must have completed your sentence at least 5 years ago or been released from custody at least 5 years ago." + }, + "pages": [ + { + "title": "Introduction", + "elements": [ + { + "component_type": "rich_text", + "text": "

What Is a Pardon and How Can It Help You?

Pardon is asking forgiveness from the President.

Pardon CAN:

  • Restore civil liberties, like the right to vote, or sit on a jury
  • Lift barriers to licensing, employment, housing, or education

Pardon CANNOT:

  • Erase a conviction
  • Expunge a conviction
" + }, + { + "component_type": "rich_text", + "text": "

Eligibility Requirements

To apply, you should:

  • Have a conviction under federal law, D.C. Code, or Uniform Code of Military Justice
  • Have been released from prison or home/community detention at least five years ago, OR
  • Have been sentenced at least five years ago, if you were not given a prison term
  • Live in the U.S. or its territories

Note: If you are still serving your sentence, use the commutation application form at justice.gov/pardon/apply-commutation

" + }, + { + "component_type": "rich_text", + "text": "

Before You Begin

This application requests detailed information about yourself, your conviction, your life since conviction, and reasons for seeking pardon. You may need several sessions to complete it.

Helpful documents to gather (if available):

  • Presentence investigation report
  • Judgment
  • Statement of reasons
  • Indictment or Information
  • Case docket report

The pardon application process can take months or years to complete. Keep your contact information up-to-date throughout the process.

" + } + ] + }, + { + "title": "Personal Information", + "elements": [ + { + "component_type": "rich_text", + "text": "

Your Name

" + }, + { + "component_type": "fieldset", + "legend": "Current full name", + "fields": [ + { + "component_type": "text_input", + "id": "First name", + "label": "First name", + "required": true + }, + { + "component_type": "text_input", + "id": "Middle name if you have one", + "label": "Middle name (if you have one)", + "required": false + }, + { + "component_type": "text_input", + "id": "Last name", + "label": "Last name", + "required": true + } + ] + }, + { + "component_type": "fieldset", + "legend": "Legal name at time of conviction (if different)", + "fields": [ + { + "component_type": "text_input", + "id": "First name_2", + "label": "First name", + "required": false + }, + { + "component_type": "text_input", + "id": "Middle Name if you have one", + "label": "Middle name (if you have one)", + "required": false + }, + { + "component_type": "text_input", + "id": "Last name_2", + "label": "Last name", + "required": false + } + ] + }, + { + "component_type": "text_input", + "id": "Other names married maiden aliases etc", + "label": "Other names (married, maiden, aliases, etc.)", + "required": false + }, + { + "component_type": "rich_text", + "text": "

Basic Information

" + }, + { + "component_type": "text_input", + "id": "Social security number", + "label": "Social Security Number", + "required": true + }, + { + "component_type": "text_input", + "id": "Date of birth Month Day Year", + "label": "Date of birth (Month, Day, Year)", + "required": true + }, + { + "component_type": "text_input", + "id": "Country where you", + "label": "Country where you were born", + "required": true + }, + { + "component_type": "text_input", + "id": "City and state where you", + "label": "City and state where you were born", + "required": true + } + ] + }, + { + "title": "Parents and Citizenship", + "elements": [ + { + "component_type": "text_input", + "id": "Parents 1s full name including maiden name", + "label": "Parent #1's full name (including maiden name)", + "required": true + }, + { + "component_type": "text_input", + "id": "Parents 2s full name including maiden name.undefined", + "label": "Parent #2's full name (including maiden name)", + "required": false + }, + { + "component_type": "radio_group", + "id": "Citizenship.undefined", + "legend": "Citizenship", + "options": [ + { + "id": "Citizenship.0", + "label": "U.S. citizen by birth", + "name": "Citizenship.undefined", + "default_checked": false + }, + { + "id": "Citizenship.1", + "label": "U.S. naturalized citizen", + "name": "Citizenship.undefined", + "default_checked": false + }, + { + "id": "Citizenship.2", + "label": "Other nationality", + "name": "Citizenship.undefined", + "default_checked": false + } + ] + }, + { + "component_type": "text_input", + "id": "Other nationality", + "label": "If other nationality, specify", + "required": false + } + ] + }, + { + "title": "Contact Information", + "elements": [ + { + "component_type": "rich_text", + "text": "

Current Address

" + }, + { + "component_type": "text_input", + "id": "Street address", + "label": "Street address", + "required": true + }, + { + "component_type": "text_input", + "id": "ApartmentUnit", + "label": "Apartment/Unit", + "required": false + }, + { + "component_type": "text_input", + "id": "City State", + "label": "City, State", + "required": true + }, + { + "component_type": "text_input", + "id": "Zip code", + "label": "Zip code", + "required": true + }, + { + "component_type": "rich_text", + "text": "

Contact Details

An email address is the best way to contact you. If you do not have an email address, you can provide the email of a person you trust, or your phone number.

" + }, + { + "component_type": "text_input", + "id": "Your email address or email of a trusted person", + "label": "Your email address or email of a trusted person", + "required": false + }, + { + "component_type": "text_input", + "id": "Phone number", + "label": "Phone number", + "required": false + }, + { + "component_type": "rich_text", + "text": "

Attorney Information (if applicable)

" + }, + { + "component_type": "text_input", + "id": "Attorneys name", + "label": "Attorney's name", + "required": false + }, + { + "component_type": "text_input", + "id": "Attorneys email address and phone number", + "label": "Attorney's email address and phone number", + "required": false + } + ] + }, + { + "title": "Previous Applications", + "elements": [ + { + "component_type": "paragraph", + "text": "Have you previously applied for federal commutation or pardon?" + }, + { + "component_type": "radio_group", + "id": "Have you applied for.undefined", + "legend": "Previous application for federal commutation or pardon", + "options": [ + { + "id": "Have you applied for.0", + "label": "Yes", + "name": "Have you applied for.undefined", + "default_checked": false + }, + { + "id": "Have you applied for.1", + "label": "No", + "name": "Have you applied for.undefined", + "default_checked": false + } + ] + }, + { + "component_type": "text_input", + "id": "Date applied monthyear", + "label": "Date applied (month/year)", + "required": false + }, + { + "component_type": "text_input", + "id": "Date of decision monthyear", + "label": "Date of decision (month/year)", + "required": false + } + ] + }, + { + "title": "Demographic Information", + "elements": [ + { + "component_type": "paragraph", + "text": "This information is for statistical data collection purposes." + }, + { + "component_type": "radio_group", + "id": "Latino.undefined", + "legend": "Are you Hispanic or Latino?", + "options": [ + { + "id": "Latino.0", + "label": "Yes", + "name": "Latino.undefined", + "default_checked": false + }, + { + "id": "Latino.1", + "label": "No", + "name": "Latino.undefined", + "default_checked": false + } + ] + }, + { + "component_type": "checkbox_group", + "legend": "Race (select all that apply)", + "options": [ + { + "id": "Alaska Native or American", + "label": "Alaska Native or American Indian", + "default_checked": false + }, + { + "id": "Black or African American", + "label": "Black or African American", + "default_checked": false + }, + { + "id": "White", + "label": "White", + "default_checked": false + }, + { + "id": "Asian", + "label": "Asian", + "default_checked": false + }, + { + "id": "Native Hawaiian or", + "label": "Native Hawaiian or Other Pacific Islander", + "default_checked": false + }, + { + "id": "Other", + "label": "Other", + "default_checked": false + } + ] + }, + { + "component_type": "radio_group", + "id": "Gender identity.undefined", + "legend": "Gender identity", + "options": [ + { + "id": "Gender identity.0", + "label": "Female", + "name": "Gender identity.undefined", + "default_checked": false + }, + { + "id": "Gender identity.1", + "label": "Male", + "name": "Gender identity.undefined", + "default_checked": false + }, + { + "id": "Gender identity.2", + "label": "Other", + "name": "Gender identity.undefined", + "default_checked": false + } + ] + } + ] + }, + { + "title": "Family Information", + "elements": [ + { + "component_type": "rich_text", + "text": "

Marital Status

" + }, + { + "component_type": "checkbox_group", + "legend": "Current marital status", + "options": [ + { + "id": "Civil uniondomestic partnership", + "label": "Civil union/domestic partnership", + "default_checked": false + }, + { + "id": "Divorced", + "label": "Divorced", + "default_checked": false + }, + { + "id": "Married", + "label": "Married", + "default_checked": false + }, + { + "id": "Never Married", + "label": "Never Married", + "default_checked": false + }, + { + "id": "Separated", + "label": "Separated", + "default_checked": false + }, + { + "id": "Widowed", + "label": "Widowed", + "default_checked": false + } + ] + }, + { + "component_type": "rich_text", + "text": "

Current Spouse/Partner (if applicable)

" + }, + { + "component_type": "text_input", + "id": "Spouse partner name", + "label": "Spouse/partner name", + "required": false + }, + { + "component_type": "text_input", + "id": "Date of marriage or civil uniondomestic partnership", + "label": "Date of marriage or civil union/domestic partnership", + "required": false + }, + { + "component_type": "text_input", + "id": "Place of marriage or civil uniondomestic partnership", + "label": "Place of marriage or civil union/domestic partnership", + "required": false + }, + { + "component_type": "rich_text", + "text": "

Children or Dependents (if applicable)

" + }, + { + "component_type": "fieldset", + "legend": "Child or dependent #1", + "fields": [ + { + "component_type": "text_input", + "id": "Full name of child or dependentRow1", + "label": "Full name", + "required": false + }, + { + "component_type": "text_input", + "id": "Date of birthRow1", + "label": "Date of birth", + "required": false + }, + { + "component_type": "text_input", + "id": "Names of other parentsRow1", + "label": "Name(s) of other parent(s)", + "required": false + }, + { + "component_type": "text_input", + "id": "Do you have custody YNRow1", + "label": "Do you have custody? (Y/N)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Child or dependent #2", + "fields": [ + { + "component_type": "text_input", + "id": "Full name of child or dependentRow2", + "label": "Full name", + "required": false + }, + { + "component_type": "text_input", + "id": "Date of birthRow2", + "label": "Date of birth", + "required": false + }, + { + "component_type": "text_input", + "id": "Names of other parentsRow2", + "label": "Name(s) of other parent(s)", + "required": false + }, + { + "component_type": "text_input", + "id": "Do you have custody YNRow2", + "label": "Do you have custody? (Y/N)", + "required": false + } + ] + }, + { + "component_type": "rich_text", + "text": "

Former Spouse/Partner (if applicable)

" + }, + { + "component_type": "text_input", + "id": "Former spouse or partner name", + "label": "Former spouse or partner name", + "required": false + }, + { + "component_type": "text_input", + "id": "Phone number_2", + "label": "Phone number", + "required": false + }, + { + "component_type": "text_input", + "id": "Date of marriage or civil uniondomestic partnership_2", + "label": "Date of marriage or civil union/domestic partnership", + "required": false + }, + { + "component_type": "text_input", + "id": "Date of divorce", + "label": "Date of divorce", + "required": false + }, + { + "component_type": "text_input", + "id": "Place of marriage or civil uniondomestic partnership_2", + "label": "Place of marriage or civil union/domestic partnership", + "required": false + }, + { + "component_type": "text_input", + "id": "Place of divorce", + "label": "Place of divorce", + "required": false + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages for your response to any questions in this section", + "label": "Check here if you are attaching additional pages for family information", + "default_checked": false + } + ] + }, + { + "title": "Reasons for Seeking Pardon", + "elements": [ + { + "component_type": "rich_text", + "text": "

Why Are You Seeking Pardon?

Be as specific as possible. You may want to include:

  • How your life would be different if granted pardon
  • Challenges that you have faced as a result of your conviction

If you have been denied a job, license, or other opportunity because of your conviction, attaching denial letters or related documents will help us review your application.

" + }, + { + "component_type": "text_input", + "id": "Your reasons for seeking pardon", + "label": "Your reasons for seeking pardon", + "required": true + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages for your response to any questions in this section_2", + "label": "Check here if you are attaching additional pages", + "default_checked": false + } + ] + }, + { + "title": "Community Activities", + "elements": [ + { + "component_type": "rich_text", + "text": "

Community Involvement Since Conviction

\"Community\" can include family, neighborhood, city, prison community, or organizations and associations. Examples include:

  • Assisting in extracurricular and education-related activities of children and grandchildren
  • Providing support to community members, such as neighbors and family members
  • Providing care to an aging relative
  • Service in or through a civic or religious organization or a professional association
  • Involvement in the prison community, including as tutor, mentor, or suicide watch companion
" + }, + { + "component_type": "fieldset", + "legend": "Activity #1", + "fields": [ + { + "component_type": "text_input", + "id": "Description of activityRow1", + "label": "Description of activity", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate start and end dates year to yearRow1", + "label": "Approximate start and end dates (year to year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Activity #2", + "fields": [ + { + "component_type": "text_input", + "id": "Description of activityRow2", + "label": "Description of activity", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate start and end dates year to yearRow2", + "label": "Approximate start and end dates (year to year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Activity #3", + "fields": [ + { + "component_type": "text_input", + "id": "Description of activityRow3", + "label": "Description of activity", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate start and end dates year to yearRow3", + "label": "Approximate start and end dates (year to year)", + "required": false + } + ] + }, + { + "component_type": "text_input", + "id": "NamesRow1", + "label": "Who can tell us about your participation in these activities? (Name(s))", + "required": false + }, + { + "component_type": "text_input", + "id": "Contact informationRow1", + "label": "Contact information for references", + "required": false + }, + { + "component_type": "text_input", + "id": "Reasons for engaging in community activities or inability to participate", + "label": "Is there anything you would like us to know about your reasons for engaging in community activities? If you have been unable to participate, explain why here.", + "required": false + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages for your response to any questions in this section_3", + "label": "Check here if you are attaching additional pages", + "default_checked": false + } + ] + }, + { + "title": "Education and Licenses", + "elements": [ + { + "component_type": "rich_text", + "text": "

Educational and Licensing Opportunities

Tell us about any educational or licensing opportunities you have had. These can be programs you have started or completed, including courses and licenses earned while incarcerated. Examples include:

  • College correspondence courses
  • Associate, bachelor, master programs
  • Department of Labor courses
  • Certificate programs
  • Vocational training
  • Commercial driver's license (CDL) courses
  • Licenses: cosmetology, real estate, nursing, teaching, welding, electrician, or other
" + }, + { + "component_type": "fieldset", + "legend": "Education #1", + "fields": [ + { + "component_type": "text_input", + "id": "School or program nameRow1", + "label": "School or program name", + "required": false + }, + { + "component_type": "text_input", + "id": "Topic or subject studied and Degree or certification receivedRow1", + "label": "Topic or subject studied and degree or certification received", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate dates attended monthyear to monthyearRow1", + "label": "Approximate dates attended (month/year to month/year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Education #2", + "fields": [ + { + "component_type": "text_input", + "id": "School or program nameRow2", + "label": "School or program name", + "required": false + }, + { + "component_type": "text_input", + "id": "Topic or subject studied and Degree or certification receivedRow2", + "label": "Topic or subject studied and degree or certification received", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate dates attended monthyear to monthyearRow2", + "label": "Approximate dates attended (month/year to month/year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "License #1", + "fields": [ + { + "component_type": "text_input", + "id": "License TypeRow1", + "label": "License type", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate date issued yearRow1", + "label": "Approximate date issued (year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "License #2", + "fields": [ + { + "component_type": "text_input", + "id": "License TypeRow2", + "label": "License type", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate date issued yearRow2", + "label": "Approximate date issued (year)", + "required": false + } + ] + }, + { + "component_type": "paragraph", + "text": "If you have been denied admission to educational programs or denied licenses due to your criminal record, provide details below. Attach denial or decision letters if available." + }, + { + "component_type": "fieldset", + "legend": "Denial #1", + "fields": [ + { + "component_type": "text_input", + "id": "School or program name or license typeRow1", + "label": "School or program name or license type", + "required": false + }, + { + "component_type": "text_input", + "id": "DetailsRow1", + "label": "Details", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate date of denial or when informed not eligible yearRow1", + "label": "Approximate date of denial or when informed not eligible (year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Denial #2", + "fields": [ + { + "component_type": "text_input", + "id": "School or program name or license typeRow2", + "label": "School or program name or license type", + "required": false + }, + { + "component_type": "text_input", + "id": "DetailsRow2", + "label": "Details", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate date of denial or when informed not eligible yearRow2", + "label": "Approximate date of denial or when informed not eligible (year)", + "required": false + } + ] + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages for your response to any questions in this section_4", + "label": "Check here if you are attaching additional pages", + "default_checked": false + } + ] + }, + { + "title": "Residential History", + "elements": [ + { + "component_type": "rich_text", + "text": "

Places Lived in the Last 3 Years

Provide addresses and approximate dates. Do not use P.O. Boxes. Include apartment/unit numbers. Do not leave any gaps in dates.

" + }, + { + "component_type": "fieldset", + "legend": "Previous address #1", + "fields": [ + { + "component_type": "text_input", + "id": "Street addressRow1", + "label": "Street address", + "required": false + }, + { + "component_type": "text_input", + "id": "Apartment UnitRow1", + "label": "Apartment/Unit", + "required": false + }, + { + "component_type": "text_input", + "id": "City stateRow1", + "label": "City, state", + "required": false + }, + { + "component_type": "text_input", + "id": "Zip codeRow1", + "label": "Zip code", + "required": false + }, + { + "component_type": "text_input", + "id": "When did you live there monthyear to monthyearRow1", + "label": "When did you live there? (month/year to month/year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Previous address #2", + "fields": [ + { + "component_type": "text_input", + "id": "Street addressRow2", + "label": "Street address", + "required": false + }, + { + "component_type": "text_input", + "id": "Apartment UnitRow2", + "label": "Apartment/Unit", + "required": false + }, + { + "component_type": "text_input", + "id": "City stateRow2", + "label": "City, state", + "required": false + }, + { + "component_type": "text_input", + "id": "Zip codeRow2", + "label": "Zip code", + "required": false + }, + { + "component_type": "text_input", + "id": "When did you live there monthyear to monthyearRow2", + "label": "When did you live there? (month/year to month/year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Previous address #3", + "fields": [ + { + "component_type": "text_input", + "id": "Street addressRow3", + "label": "Street address", + "required": false + }, + { + "component_type": "text_input", + "id": "Apartment UnitRow3", + "label": "Apartment/Unit", + "required": false + }, + { + "component_type": "text_input", + "id": "City stateRow3", + "label": "City, state", + "required": false + }, + { + "component_type": "text_input", + "id": "Zip codeRow3", + "label": "Zip code", + "required": false + }, + { + "component_type": "text_input", + "id": "When did you live there monthyear to monthyearRow3", + "label": "When did you live there? (month/year to month/year)", + "required": false + } + ] + }, + { + "component_type": "text_input", + "id": "monthyear to monthyear", + "label": "If you are now experiencing homelessness or have in the past, note the dates (month/year to month/year)", + "required": false + } + ] + }, + { + "title": "Military Service", + "elements": [ + { + "component_type": "paragraph", + "text": "If you have completed any military service, provide details here." + }, + { + "component_type": "checkbox", + "id": "Not applicable", + "label": "Not applicable", + "default_checked": false + }, + { + "component_type": "text_input", + "id": "Dates of service", + "label": "Dates of service", + "required": false + }, + { + "component_type": "text_input", + "id": "Branches", + "label": "Branch(es)", + "required": false + }, + { + "component_type": "text_input", + "id": "Serial number", + "label": "Serial number", + "required": false + }, + { + "component_type": "text_input", + "id": "Type of discharge", + "label": "Type of discharge", + "required": false + }, + { + "component_type": "text_input", + "id": "space to tell us briefly about your military service", + "label": "Tell us briefly about your military service. For example, any tours of duty, time overseas or in active combat, disciplinary sanctions or military criminal proceedings, commendations or medals, or other notable achievements. Attach a copy of your DD-214 if available.", + "required": false + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages for your response to any questions in this section_5", + "label": "Check here if you are attaching additional pages", + "default_checked": false + } + ] + }, + { + "title": "Employment History", + "elements": [ + { + "component_type": "rich_text", + "text": "

Job History (Last 7 Years)

Include full and part-time jobs. If applicable, include jobs while incarcerated. Use approximate dates. Do not leave any gaps in dates. If you are retired, give the approximate date your retirement began in the \"Current employer\" section.

" + }, + { + "component_type": "fieldset", + "legend": "Current employer", + "fields": [ + { + "component_type": "text_input", + "id": "Current employer", + "label": "Current employer", + "required": false + }, + { + "component_type": "text_input", + "id": "Type of business", + "label": "Type of business", + "required": false + }, + { + "component_type": "text_input", + "id": "Position", + "label": "Position", + "required": false + }, + { + "component_type": "text_input", + "id": "Monthyear started", + "label": "Month/year started", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Current employer address", + "fields": [ + { + "component_type": "text_input", + "id": "Employer street address", + "label": "Employer street address", + "required": false + }, + { + "component_type": "text_input", + "id": "City state 2", + "label": "City, state", + "required": false + }, + { + "component_type": "text_input", + "id": "Zip code_2", + "label": "Zip code", + "required": false + }, + { + "component_type": "text_input", + "id": "Supervisor name and phone", + "label": "Supervisor name and phone number", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Previous employer #1", + "fields": [ + { + "component_type": "text_input", + "id": "Previous employer nameRow1", + "label": "Employer name", + "required": false + }, + { + "component_type": "text_input", + "id": "Type of businessRow1", + "label": "Type of business", + "required": false + }, + { + "component_type": "text_input", + "id": "PositionRow1", + "label": "Position", + "required": false + }, + { + "component_type": "text_input", + "id": "Employer address and phone numberRow1", + "label": "Employer address and phone number", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate dates worked monthyear to monthyearRow1", + "label": "Approximate dates worked (month/year to month/year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Previous employer #2", + "fields": [ + { + "component_type": "text_input", + "id": "Previous employer nameRow2", + "label": "Employer name", + "required": false + }, + { + "component_type": "text_input", + "id": "Type of businessRow2", + "label": "Type of business", + "required": false + }, + { + "component_type": "text_input", + "id": "PositionRow2", + "label": "Position", + "required": false + }, + { + "component_type": "text_input", + "id": "Employer address and phone numberRow2", + "label": "Employer address and phone number", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate dates worked monthyear to monthyearRow2", + "label": "Approximate dates worked (month/year to month/year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Previous employer #3", + "fields": [ + { + "component_type": "text_input", + "id": "Previous employer nameRow3", + "label": "Employer name", + "required": false + }, + { + "component_type": "text_input", + "id": "Type of businessRow3", + "label": "Type of business", + "required": false + }, + { + "component_type": "text_input", + "id": "PositionRow3", + "label": "Position", + "required": false + }, + { + "component_type": "text_input", + "id": "Employer address and phone numberRow3", + "label": "Employer address and phone number", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate dates worked monthyear to monthyearRow3", + "label": "Approximate dates worked (month/year to month/year)", + "required": false + } + ] + }, + { + "component_type": "text_input", + "id": "Information on how you supported yourself while unemployed", + "label": "If you are currently unemployed or have been in the past, provide the dates and let us know how you supported yourself during that time", + "required": false + }, + { + "component_type": "text_input", + "id": "Details about how your criminal record has affected your ability to find word, if any", + "label": "If your criminal record has affected your ability to find work, provide details here. If you received a rejection letter or termination notice due to your conviction, you may attach a copy.", + "required": false + }, + { + "component_type": "text_input", + "id": "Details about work history", + "label": "Your work history will be reviewed as part of any background investigation. If you have been fired, accused of misconduct at a job, or given an unsatisfactory job performance rating, provide details here.", + "required": false + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages for your response to any questions in this section_6", + "label": "Check here if you are attaching additional pages", + "default_checked": false + } + ] + }, + { + "title": "Substance Use History", + "elements": [ + { + "component_type": "paragraph", + "text": "If you have struggled with substance use, provide details here. We recognize that many people have struggled with substance use and that this can be difficult to discuss. Your honest reflection on this topic is helpful to us. Give approximate dates, to the best of your ability." + }, + { + "component_type": "checkbox", + "id": "Not applicable_2", + "label": "Not applicable", + "default_checked": false + }, + { + "component_type": "fieldset", + "legend": "Substance use #1", + "fields": [ + { + "component_type": "text_input", + "id": "Type of drug or alcoholRow1", + "label": "Type of drug or alcohol", + "required": false + }, + { + "component_type": "text_input", + "id": "How often were you usingRow1", + "label": "How often were you using?", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate dates used monthyear to monthyearRow1", + "label": "Approximate dates used (month/year to month/year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Substance use #2", + "fields": [ + { + "component_type": "text_input", + "id": "Type of drug or alcoholRow2", + "label": "Type of drug or alcohol", + "required": false + }, + { + "component_type": "text_input", + "id": "How often were you usingRow2", + "label": "How often were you using?", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate dates used monthyear to monthyearRow2", + "label": "Approximate dates used (month/year to month/year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Substance use #3", + "fields": [ + { + "component_type": "text_input", + "id": "Type of drug or alcoholRow3", + "label": "Type of drug or alcohol", + "required": false + }, + { + "component_type": "text_input", + "id": "How often were you usingRow3", + "label": "How often were you using?", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate dates used monthyear to monthyearRow3", + "label": "Approximate dates used (month/year to month/year)", + "required": false + } + ] + }, + { + "component_type": "paragraph", + "text": "If you have been diagnosed with a substance use disorder, provide details here." + }, + { + "component_type": "text_input", + "id": "Diagnosis", + "label": "Diagnosis", + "required": false + }, + { + "component_type": "text_input", + "id": "Date of diagnosis monthyear", + "label": "Date of diagnosis (month/year)", + "required": false + }, + { + "component_type": "paragraph", + "text": "Provide information on any counseling or treatment you received or rehabilitation program you attended for substance use." + }, + { + "component_type": "text_input", + "id": "Facilitycounselordoctor name", + "label": "Facility/counselor/doctor name", + "required": false + }, + { + "component_type": "text_input", + "id": "When did you attend", + "label": "When did you attend? (month/year to month/year)", + "required": false + }, + { + "component_type": "fieldset", + "legend": "Facility address", + "fields": [ + { + "component_type": "text_input", + "id": "Street address_2", + "label": "Street address", + "required": false + }, + { + "component_type": "text_input", + "id": "Suite no", + "label": "Suite no.", + "required": false + }, + { + "component_type": "text_input", + "id": "City state_2", + "label": "City, state", + "required": false + }, + { + "component_type": "text_input", + "id": "Zip code_3", + "label": "Zip code", + "required": false + } + ] + }, + { + "component_type": "text_input", + "id": "Phone number_3", + "label": "Phone number", + "required": false + }, + { + "component_type": "text_input", + "id": "Email address", + "label": "Email address", + "required": false + }, + { + "component_type": "text_input", + "id": "Specify length of time in days months or years", + "label": "How long have you been sober? (Specify length of time in days, months, or years)", + "required": false + }, + { + "component_type": "text_input", + "id": "Is there anything else you would like to share about your history with sobriety and substance use 1", + "label": "Is there anything else you would like to share about your history with sobriety and substance use?", + "required": false + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages for your response to any questions in this section_7", + "label": "Check here if you are attaching additional pages", + "default_checked": false + } + ] + }, + { + "title": "Financial Information", + "elements": [ + { + "component_type": "rich_text", + "text": "

Debts and Bankruptcy

Provide details of any debts that are late or in default (including child support payments) or bankruptcy filings.

We recognize that criminal convictions may affect people's ability to get a job and may carry heavy financial penalties, making it more difficult to keep up with necessary expenses. We know this can be a difficult subject to discuss, but your honest reflection on this topic is helpful to us.

Give approximate dates and amounts, to the best of your ability. A credit report will be reviewed if a background investigation is initiated.

" + }, + { + "component_type": "fieldset", + "legend": "Debt #1", + "fields": [ + { + "component_type": "text_input", + "id": "Description of debt that is late or in defaultRow1", + "label": "Description of debt that is late or in default", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximately how much is the debtRow1", + "label": "Approximately how much is the debt?", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Debt #2", + "fields": [ + { + "component_type": "text_input", + "id": "Description of debt that is late or in defaultRow2", + "label": "Description of debt that is late or in default", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximately how much is the debtRow2", + "label": "Approximately how much is the debt?", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Debt #3", + "fields": [ + { + "component_type": "text_input", + "id": "Description of debt that is late or in defaultRow3", + "label": "Description of debt that is late or in default", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximately how much is the debtRow3", + "label": "Approximately how much is the debt?", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Bankruptcy #1", + "fields": [ + { + "component_type": "text_input", + "id": "Court where bankruptcy filedRow1", + "label": "Court where bankruptcy filed", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate year bankruptcy filed and outcomeRow1", + "label": "Approximate year bankruptcy filed and outcome", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximately how much debt did you want to dischargeRow1", + "label": "Approximately how much debt did you want to discharge?", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Bankruptcy #2", + "fields": [ + { + "component_type": "text_input", + "id": "Court where bankruptcy filedRow2", + "label": "Court where bankruptcy filed", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate year bankruptcy filed and outcomeRow2", + "label": "Approximate year bankruptcy filed and outcome", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximately how much debt did you want to dischargeRow2", + "label": "Approximately how much debt did you want to discharge?", + "required": false + } + ] + }, + { + "component_type": "text_input", + "id": "Information you would like to share about your experience with finances since your conviction", + "label": "Is there anything else you would like to share about your experience with finances since your conviction? This may include information on why you are unable to pay the above debts or filed for bankruptcy and any plans you have to catch up on payments for any debts that are late or in default.", + "required": false + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages for your response to any questions in this section_8", + "label": "Check here if you are attaching additional pages", + "default_checked": false + } + ] + }, + { + "title": "Conviction Details", + "elements": [ + { + "component_type": "rich_text", + "text": "

Case Background

Provide basic information on the conviction for which you are seeking pardon. If you are seeking pardon for more than one conviction, attach additional pages.

It is not required, but, if available, sending copies of the following documents with your application will help us review your case more quickly:

  • Presentence report
  • Judgment
  • Statement of reasons
  • Indictment or Information
  • Case docket report
" + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching any of the listed documents", + "label": "Check here if you are attaching any of the listed documents", + "default_checked": false + }, + { + "component_type": "radio_group", + "id": "Did you plead guilty.undefined", + "legend": "Did you plead guilty?", + "options": [ + { + "id": "Did you plead guilty.0", + "label": "Yes", + "name": "Did you plead guilty.undefined", + "default_checked": false + }, + { + "id": "Did you plead guilty.1", + "label": "No", + "name": "Did you plead guilty.undefined", + "default_checked": false + } + ] + }, + { + "component_type": "text_input", + "id": "Approximate dates of offense monthyear to", + "label": "Approximate date(s) of offense (month/year to month/year)", + "required": true + }, + { + "component_type": "text_input", + "id": "Approximate date you were sentenced", + "label": "Approximate date you were sentenced (month/year)", + "required": true + }, + { + "component_type": "text_input", + "id": "Court where you were prosecuted DC Superior Court", + "label": "Court where you were prosecuted (D.C. Superior Court, military court, or name of U.S. District Court)", + "required": true + }, + { + "component_type": "text_input", + "id": "Case number", + "label": "Case number", + "required": true + }, + { + "component_type": "text_input", + "id": "Of what were you convicted", + "label": "Of what were you convicted?", + "required": true + } + ] + }, + { + "title": "Sentence Information", + "elements": [ + { + "component_type": "rich_text", + "text": "

What Sentence Did You Receive?

Fill in where applicable.

" + }, + { + "component_type": "fieldset", + "legend": "Imprisonment", + "fields": [ + { + "component_type": "text_input", + "id": "Prison sentence months or years", + "label": "Prison sentence (months or years)", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate date you were released from", + "label": "Approximate date you were released from prison, community confinement, or home detention (month/year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Probation or supervised release", + "fields": [ + { + "component_type": "text_input", + "id": "Sentence for probation or supervised", + "label": "Sentence for probation or supervised release (months or years)", + "required": false + }, + { + "component_type": "text_input", + "id": "Approximate date you completed your term", + "label": "Approximate date you completed your term of probation or supervised release (month/year)", + "required": false + } + ] + }, + { + "component_type": "fieldset", + "legend": "Financial penalties", + "fields": [ + { + "component_type": "text_input", + "id": "Assessment amount", + "label": "Assessment amount", + "required": false + }, + { + "component_type": "text_input", + "id": "Fine amount", + "label": "Fine amount", + "required": false + }, + { + "component_type": "text_input", + "id": "Restitution amount", + "label": "Restitution amount", + "required": false + } + ] + } + ] + }, + { + "title": "Your Conduct and Responsibility", + "elements": [ + { + "component_type": "rich_text", + "text": "

Tell Us About Your Conduct

We want to hear from you, in your own words. The more specific and complete you are, the more helpful it is to us. We are specifically looking for information that is NOT in the public record of your case. You may wish to answer the following:

  • What was your role in the offense?
  • How, when, and why did you get involved?
  • What actions did you take in connection with the offense? (Include all actions, even if you pleaded guilty to only specific conduct, counts, or portions of the full criminal activity.)
" + }, + { + "component_type": "text_input", + "id": "Tell us about your conduct for which you were convicted", + "label": "Tell us about your conduct for which you were convicted", + "required": true + }, + { + "component_type": "text_input", + "id": "Explanation of why or why not you accept responsibility for your conduct", + "label": "Do you accept responsibility for your conduct? Explain why or why not.", + "required": true + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages for your response to any questions in this section_9", + "label": "Check here if you are attaching additional pages", + "default_checked": false + } + ] + }, + { + "title": "Other Criminal History", + "elements": [ + { + "component_type": "rich_text", + "text": "

Other Criminal History

Your criminal history will be reviewed as part of any background investigation. List any other arrests or convictions that may appear on your criminal history record, if any, including juvenile and expunged records, and provide any information you would like us to know about them. If you have your presentence report, you may attach it and provide missing or additional information you would like us to know below.

" + }, + { + "component_type": "text_input", + "id": "Tell us about any other criminal history", + "label": "Tell us about any other criminal history", + "required": false + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages for your response to any questions in this section_10", + "label": "Check here if you are attaching additional pages", + "default_checked": false + } + ] + }, + { + "title": "Certification and Oath", + "elements": [ + { + "component_type": "rich_text", + "text": "

Certification and Personal Oath

I certify, under penalty of perjury, that all information in my petition and any document submitted with it were provided or authorized by me and that I reviewed and understand the information contained in, and submitted with, my petition. I further certify, under penalty of perjury, that all the information I provided in the application is complete, true, and correct to the best of my knowledge, information, and belief.

In petitioning the President of the United States for pardon, I do solemnly swear that I will be law-abiding and will support and defend the Constitution of the United States against all enemies, foreign and domestic, and that I take this obligation freely and without any mental reservation whatsoever.

" + }, + { + "component_type": "paragraph", + "text": "Respectfully submitted this:" + }, + { + "component_type": "fieldset", + "legend": "Date of submission", + "fields": [ + { + "component_type": "text_input", + "id": "Day", + "label": "Day", + "required": true + }, + { + "component_type": "text_input", + "id": "Month", + "label": "Month", + "required": true + }, + { + "component_type": "text_input", + "id": "Year", + "label": "Year", + "required": true + } + ] + }, + { + "component_type": "paragraph", + "text": "Your signature (required)" + } + ] + }, + { + "title": "Authorization for Release", + "elements": [ + { + "component_type": "rich_text", + "text": "

Authorization for Release of Information

Carefully read this authorization, and if you agree, sign and date in ink.

I authorize any investigator, special agent, or other duly accredited representative of the Federal Bureau of Investigation, the Department of Defense, and any other authorized Federal agency, to obtain any information relating to my activities from schools, residential management agents, employers, criminal justice agencies, retail business establishments, courts, or other sources of information. This information may include, but is not limited to, my academic, residential, achievement, performance, attendance, disciplinary, employment history, criminal history, arrest, conviction, including the presentence investigation report, if any, medical, psychiatric/psychological, health care, and financial and credit information.

I understand that, for financial or lending institutions and certain other sources of information, a separate specific release may be needed (pursuant to their request or as may be required by law), and I may be contacted for such a release at a later date.

I further authorize the Federal Bureau of Investigation, the Department of Defense, and any other authorized Federal agency, to request criminal record information about me from criminal justice agencies for the purpose of determining my suitability for a government benefit.

I authorize custodians of records and sources of information pertaining to me to release such information upon request of the investigator, special agent, or other duly accredited representative of any Federal agency authorized above regardless of any previous agreement to the contrary. I understand that the information released by records custodians and sources of information is for official use by the Federal Government only for the purposes of processing my application for a government benefit, and may be redisclosed by the Government only as authorized by law.

Copies of this authorization that show my signature are as valid as the original release signed by me. This authorization is valid and shall remain in effect so long as I am under consideration for federal clemency.

" + }, + { + "component_type": "paragraph", + "text": "Signature (sign in ink) - required" + }, + { + "component_type": "text_input", + "id": "Full Name type or print legibly", + "label": "Full name (type or print legibly)", + "required": true + }, + { + "component_type": "text_input", + "id": "Date Signed", + "label": "Date signed", + "required": true + }, + { + "component_type": "text_input", + "id": "Other Names Used", + "label": "Other names used", + "required": false + }, + { + "component_type": "text_input", + "id": "Street Address", + "label": "Street address", + "required": true + }, + { + "component_type": "fieldset", + "legend": "City, State, ZIP", + "fields": [ + { + "component_type": "text_input", + "id": "City", + "label": "City", + "required": true + }, + { + "component_type": "text_input", + "id": "State", + "label": "State", + "required": true + }, + { + "component_type": "text_input", + "id": "ZIP Code", + "label": "ZIP Code", + "required": true + } + ] + }, + { + "component_type": "text_input", + "id": "Home Telephone Number include area code", + "label": "Home telephone number (include area code)", + "required": true + }, + { + "component_type": "text_input", + "id": "Social Security Number", + "label": "Social Security Number", + "required": true + } + ] + }, + { + "title": "Letter of Support #1", + "elements": [ + { + "component_type": "rich_text", + "text": "

Letter of Support

Note: You must provide exactly 3 letters of support from non-relatives as your primary references. Primary references must be willing to be interviewed during a background investigation.

" + }, + { + "component_type": "checkbox", + "id": "Primary reference select exactly 3", + "label": "Primary reference (select exactly 3)", + "default_checked": false + }, + { + "component_type": "text_input", + "id": "Name of petitioner", + "label": "On behalf of (Name of petitioner)", + "required": true + }, + { + "component_type": "text_input", + "id": "Number of years have known petitioner", + "label": "I certify that I have personally known the petitioner for ___ years and am not related to petitioner by blood or marriage.", + "required": true + }, + { + "component_type": "rich_text", + "text": "

In support of this pardon petition, I state the below:

Note: The information below should be based on your personal knowledge of the petitioner. Helpful information includes:

  • How you know the individual,
  • What you know of the person's reputation and conduct since their conviction, and
  • Their personal and professional activities, including at work, at home, and in the community.
" + }, + { + "component_type": "text_input", + "id": "Statement in support of the pardon petition", + "label": "Statement in support of the pardon petition", + "required": true + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages", + "label": "Check here if you are attaching additional pages", + "default_checked": false + }, + { + "component_type": "paragraph", + "text": "I affirm that the above information is true and correct to the best of my knowledge, information, and belief." + }, + { + "component_type": "paragraph", + "text": "Signature (required)" + }, + { + "component_type": "text_input", + "id": "Print Name", + "label": "Print name", + "required": true + }, + { + "component_type": "text_input", + "id": "Date", + "label": "Date", + "required": true + }, + { + "component_type": "text_input", + "id": "Address", + "label": "Address", + "required": true + }, + { + "component_type": "text_input", + "id": "Phone number_4", + "label": "Phone number", + "required": true + }, + { + "component_type": "text_input", + "id": "Email address_2", + "label": "Email address", + "required": true + } + ] + }, + { + "title": "Letter of Support #2", + "elements": [ + { + "component_type": "rich_text", + "text": "

Letter of Support

" + }, + { + "component_type": "checkbox", + "id": "Primary reference select exactly 3_2", + "label": "Primary reference (select exactly 3)", + "default_checked": false + }, + { + "component_type": "text_input", + "id": "Name of petitioner_2", + "label": "On behalf of (Name of petitioner)", + "required": true + }, + { + "component_type": "text_input", + "id": "Number of year you have known petitioner_2", + "label": "I certify that I have personally known the petitioner for ___ years and am not related to petitioner by blood or marriage.", + "required": true + }, + { + "component_type": "paragraph", + "text": "In support of this pardon petition, I state the below. The information should be based on your personal knowledge of the petitioner." + }, + { + "component_type": "text_input", + "id": "Statement in support of pardon petition_2", + "label": "Statement in support of the pardon petition", + "required": true + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages_2", + "label": "Check here if you are attaching additional pages", + "default_checked": false + }, + { + "component_type": "paragraph", + "text": "I affirm that the above information is true and correct to the best of my knowledge, information, and belief." + }, + { + "component_type": "paragraph", + "text": "Signature (required)" + }, + { + "component_type": "text_input", + "id": "Print Name_2", + "label": "Print name", + "required": true + }, + { + "component_type": "text_input", + "id": "Date_2", + "label": "Date", + "required": true + }, + { + "component_type": "text_input", + "id": "Address_2", + "label": "Address", + "required": true + }, + { + "component_type": "text_input", + "id": "Phone number_5", + "label": "Phone number", + "required": true + }, + { + "component_type": "text_input", + "id": "Email address_3", + "label": "Email address", + "required": true + } + ] + }, + { + "title": "Letter of Support #3", + "elements": [ + { + "component_type": "rich_text", + "text": "

Letter of Support

" + }, + { + "component_type": "checkbox", + "id": "Primary reference select exactly 3_3", + "label": "Primary reference (select exactly 3)", + "default_checked": false + }, + { + "component_type": "text_input", + "id": "Name of petitioner_3", + "label": "On behalf of (Name of petitioner)", + "required": true + }, + { + "component_type": "text_input", + "id": "Number of years you have known petitioner_3", + "label": "I certify that I have personally known the petitioner for ___ years and am not related to petitioner by blood or marriage.", + "required": true + }, + { + "component_type": "paragraph", + "text": "In support of this pardon petition, I state the below. The information should be based on your personal knowledge of the petitioner." + }, + { + "component_type": "text_input", + "id": "Statement in support of pardon petition_3", + "label": "Statement in support of the pardon petition", + "required": true + }, + { + "component_type": "checkbox", + "id": "Check here if you are attaching additional pages_3", + "label": "Check here if you are attaching additional pages", + "default_checked": false + }, + { + "component_type": "paragraph", + "text": "I affirm that the above information is true and correct to the best of my knowledge, information, and belief." + }, + { + "component_type": "paragraph", + "text": "Signature (required)" + }, + { + "component_type": "text_input", + "id": "Print Name_3", + "label": "Print name", + "required": true + }, + { + "component_type": "text_input", + "id": "Date_3", + "label": "Date", + "required": true + }, + { + "component_type": "text_input", + "id": "Address_3", + "label": "Address", + "required": true + }, + { + "component_type": "text_input", + "id": "Phone number_6", + "label": "Phone number", + "required": true + }, + { + "component_type": "text_input", + "id": "Email address_4", + "label": "Email address", + "required": true + } + ] + }, + { + "title": "Application Checklist", + "elements": [ + { + "component_type": "rich_text", + "text": "

Application Checklist

1. Gather the following information

Required:

" + }, + { + "component_type": "checkbox", + "id": "Application form pages 617", + "label": "Application form (pages 6-17)", + "default_checked": false + }, + { + "component_type": "checkbox", + "id": "Signed certification and personal oath", + "label": "Signed certification and personal oath (page 18)", + "default_checked": false + }, + { + "component_type": "checkbox", + "id": "Signed and completed Authorization for", + "label": "Signed and completed Authorization for Release of Information form (page 19)", + "default_checked": false + }, + { + "component_type": "checkbox", + "id": "3 signed letters of support from nonrelatives", + "label": "3 signed letters of support from non-relatives (pages 20-22) - You MUST select exactly 3 as your primary letters", + "default_checked": false + }, + { + "component_type": "paragraph", + "text": "Optional:" + }, + { + "component_type": "checkbox", + "id": "Official records presentence report judgment", + "label": "Official records: presentence report, judgment, statement of reasons, indictment or information, or court docket record", + "default_checked": false + }, + { + "component_type": "checkbox", + "id": "Personal records supporting answers", + "label": "Personal records supporting answers", + "default_checked": false + }, + { + "component_type": "checkbox", + "id": "Additional pages to complete answers", + "label": "Additional pages to complete answers", + "default_checked": false + }, + { + "component_type": "checkbox", + "id": "Additional pages with any information you feel", + "label": "Additional pages with any information you feel would make your application stronger but did not see a space to talk about it", + "default_checked": false + }, + { + "component_type": "rich_text", + "text": "

2. Submit your application

NOTE: Keep a copy of everything you submit for your personal records.

For general pardon applications

The fastest way to submit your application is by email. If you send it by mail, it may take longer to process.

By email: Email documents in PDF or Word format to USPardon.Attorney@usdoj.gov

By mail:
U.S. Dep't of Justice, Office of the Pardon Attorney
950 Pennsylvania Avenue, N.W.
Washington, D.C. 20530

For pardon of a general court-martial conviction only

Submit your application to the Secretary of the military department that had original jurisdiction in your case.

NOTE: Pardon of a military offense will not change the character of a military discharge.

" + }, + { + "component_type": "rich_text", + "text": "

3. Keep your contact information up-to-date

If your contact information changes, email us at USPardon.Attorney@usdoj.gov or send a letter to our mailing address so that we can reach you throughout the pardon application process.

The application process:
  1. Confirmation letter – We will send an email or letter letting you know we received your application and if it is missing any parts. If you have not received a confirmation after three months, email USPardon.Attorney@usdoj.gov (preferred) or send a letter to our mailing address. You may also check the status of your case on the Pardon Attorney's website at: https://www.justice.gov/pardon/search-clemency-case-status.
  2. Follow-up letters – It may take some time for review of your application to start. During the review, we may need more information or updates to your application. If we do, we will contact you by email (preferred) or mail.
  3. Background investigation – During the review of your application, a background investigation may be necessary. The investigation is conducted by agents of the Federal Bureau of Investigation (FBI). We will let you know by email (preferred) or mail if we have requested a background investigation. It may include interviews of you, the people who wrote your letters of support, neighbors, former and present employers, acquaintances, and other individuals who may be able to provide relevant information about you. The agent will be discreet and make reasonable efforts not to disclose the reason for the investigation, but we cannot guarantee that those interviewed will not learn that you are seeking pardon for a past criminal conviction.
  4. Notification of final decision – You will be notified when a final decision is made by the President on whether to grant or deny your pardon application. This may take years. No hearing will be held and there is no appeal from the President's decision to deny a request for pardon.
  5. Reapply – If your pardon request is denied, you may reapply two years after the date of the denial.
" + } + ] + } + ] + }, + "finishReason": "tool-calls", + "usage": { + "inputTokens": 28939, + "outputTokens": 19831, + "totalTokens": 48770, + "cachedInputTokens": 0 + }, + "warnings": [], + "providerMetadata": { + "bedrock": { + "usage": { + "cacheWriteInputTokens": 0 + }, + "isJsonResponseFromTool": true + } + }, + "response": { + "id": "aiobj-VUwBmDoiNnvalM1Fc5RQzwje", + "timestamp": "2025-10-05T05:55:05.242Z", + "modelId": "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + "headers": { + "connection": "keep-alive", + "content-length": "54943", + "content-type": "application/json", + "date": "Sun, 05 Oct 2025 05:55:05 GMT", + "x-amzn-requestid": "177f367e-4507-4179-a1f8-b2cbb9dbaae9" + } + }, + "request": {} +} \ No newline at end of file diff --git a/packages/forms/fixtures/ai-cache/a3/a353b2de50409d3a56d22762b1d1a46dde3065a938de55877960826eff5c2b01.json b/packages/forms/fixtures/ai-cache/a3/a353b2de50409d3a56d22762b1d1a46dde3065a938de55877960826eff5c2b01.json new file mode 100644 index 00000000..2c34744a --- /dev/null +++ b/packages/forms/fixtures/ai-cache/a3/a353b2de50409d3a56d22762b1d1a46dde3065a938de55877960826eff5c2b01.json @@ -0,0 +1,399 @@ +{ + "object": { + "form_summary": { + "title": "Application for Certificate of Pardon for Marijuana Offenses", + "description": "Apply for a certificate of pardon for federal offenses of simple possession, attempted possession, or use of marijuana under presidential proclamations." + }, + "pages": [ + { + "title": "Introduction", + "elements": [ + { + "component_type": "rich_text", + "text": "

Application for Certificate of Pardon for Marijuana Offenses

On October 6, 2022, and December 22, 2023, President Biden issued presidential proclamations pardoning federal and D.C. offenses for simple marijuana possession, attempted possession, and use.

How a Pardon Can Help You

A pardon is an expression of the President's forgiveness. It does not mean you are innocent or expunge your conviction, but it does remove civil disabilities such as restrictions on voting, holding office, or serving on a jury. It may also help with obtaining licenses, bonding, or employment.

You Qualify If:

  • On or before December 22, 2023, you were charged with or convicted of simple possession, attempted possession, or use of marijuana under federal code, D.C. code, or Code of Federal Regulations
  • You were a U.S. citizen or lawfully present in the United States at the time of the offense
  • You were a U.S. citizen or lawful permanent resident on December 22, 2023
" + }, + { + "component_type": "rich_text", + "text": "

What You'll Need

About You: Personal details including name, citizenship status, and contact information (mailing address and/or email).

About the Charge or Conviction: Whether it was a charge or conviction, court district, date, and if possible, case information (docket number, code section) and supporting documents.

Important: Submit a separate form for each conviction or charge. Without complete information, we cannot guarantee we can determine your eligibility.

" + }, + { + "component_type": "paragraph", + "text": "The estimated time to complete this application is 120 minutes. Information regarding gender, race, or ethnicity is optional and will not affect processing." + } + ] + }, + { + "title": "Personal Information", + "elements": [ + { + "component_type": "paragraph", + "text": "Provide your current legal name." + }, + { + "component_type": "fieldset", + "legend": "Current Name", + "fields": [ + { + "component_type": "text_input", + "id": "Fst Name 1", + "label": "First Name", + "required": true + }, + { + "component_type": "text_input", + "id": "Mid Name 1", + "label": "Middle Name", + "required": false + }, + { + "component_type": "text_input", + "id": "Lst Name 1", + "label": "Last Name", + "required": true + } + ] + }, + { + "component_type": "paragraph", + "text": "If your name was different at the time of conviction, provide that name below." + }, + { + "component_type": "fieldset", + "legend": "Name at Conviction (if different)", + "fields": [ + { + "component_type": "text_input", + "id": "Conv Fst Name", + "label": "First Name", + "required": false + }, + { + "component_type": "text_input", + "id": "Conv Mid Name", + "label": "Middle Name", + "required": false + }, + { + "component_type": "text_input", + "id": "Conv Lst Name", + "label": "Last Name", + "required": false + } + ] + }, + { + "component_type": "text_input", + "id": "Date of Birth", + "label": "Date of Birth (MM/DD/YYYY)", + "required": true + }, + { + "component_type": "text_input", + "id": "Gender", + "label": "Gender (optional)", + "required": false + } + ] + }, + { + "title": "Contact Information", + "elements": [ + { + "component_type": "paragraph", + "text": "Provide your mailing address and/or email address. We strongly recommend including an email address for faster communication." + }, + { + "component_type": "fieldset", + "legend": "Mailing Address", + "fields": [ + { + "component_type": "text_input", + "id": "Address", + "label": "Street Address (number, street, apartment/unit)", + "required": false + }, + { + "component_type": "text_input", + "id": "City", + "label": "City", + "required": false + }, + { + "component_type": "text_input", + "id": "State", + "label": "State", + "required": false + }, + { + "component_type": "text_input", + "id": "Zip Code", + "label": "ZIP Code", + "required": false + } + ] + }, + { + "component_type": "text_input", + "id": "Email Address", + "label": "Email Address", + "required": false + }, + { + "component_type": "text_input", + "id": "Phone Number", + "label": "Phone Number", + "required": false + } + ] + }, + { + "title": "Demographics", + "elements": [ + { + "component_type": "paragraph", + "text": "The following demographic information is optional and will not affect the processing of your application." + }, + { + "component_type": "radio_group", + "id": "Ethnicity.undefined", + "legend": "Are you Hispanic or Latino?", + "options": [ + { + "id": "Ethnicity.0", + "label": "Yes", + "name": "Ethnicity.undefined", + "default_checked": false + }, + { + "id": "Ethnicity.1", + "label": "No", + "name": "Ethnicity.undefined", + "default_checked": false + } + ] + }, + { + "component_type": "checkbox_group", + "legend": "Race (check all that apply)", + "options": [ + { + "id": "Nat Amer", + "label": "Alaska Native or American Indian", + "default_checked": false + }, + { + "id": "Asian", + "label": "Asian", + "default_checked": false + }, + { + "id": "Blck Amer", + "label": "Black or African American", + "default_checked": false + }, + { + "id": "Nat Haw Islander", + "label": "Native Hawaiian or Other Pacific Islander", + "default_checked": false + }, + { + "id": "White", + "label": "White", + "default_checked": false + }, + { + "id": "Other", + "label": "Other", + "default_checked": false + } + ] + } + ] + }, + { + "title": "Citizenship and Residency", + "elements": [ + { + "component_type": "paragraph", + "text": "Select your citizenship or residency status. If you are a naturalized citizen or lawful permanent resident, provide the relevant dates and documentation numbers." + }, + { + "component_type": "radio_group", + "id": "Citizenship.undefined", + "legend": "Citizenship or Residency Status", + "options": [ + { + "id": "Citizenship.0", + "label": "U.S. citizen by birth", + "name": "Citizenship.undefined", + "default_checked": false + }, + { + "id": "Citizenship.1", + "label": "U.S. naturalized citizen", + "name": "Citizenship.undefined", + "default_checked": false + }, + { + "id": "Citizenship.2", + "label": "Lawful Permanent Resident", + "name": "Citizenship.undefined", + "default_checked": false + } + ] + }, + { + "component_type": "text_input", + "id": "Naturalization Date_af_date", + "label": "Date Naturalization Granted (if applicable)", + "required": false + }, + { + "component_type": "text_input", + "id": "Residency Date_af_date", + "label": "Date Residency Granted (if applicable)", + "required": false + }, + { + "component_type": "text_input", + "id": "A-Number", + "label": "Alien Registration Number (A-Number), Certificate of Naturalization Number, or Citizenship Number (if applicable)", + "required": false + } + ] + }, + { + "title": "Conviction Information", + "elements": [ + { + "component_type": "paragraph", + "text": "Complete this section if you were CONVICTED of simple possession, attempted possession, or use of marijuana. Provide the conviction date, court information, docket number, and code section." + }, + { + "component_type": "text_input", + "id": "Convict-Date_af_date", + "label": "Date of Conviction (MM/DD/YYYY)", + "required": false + }, + { + "component_type": "text_input", + "id": "US District Court", + "label": "U.S. District Court (e.g., Northern, Eastern, Southern, Western)", + "required": false + }, + { + "component_type": "text_input", + "id": "Dist State", + "label": "District of (State)", + "required": false + }, + { + "component_type": "checkbox", + "id": "D.C. Superior Court 1", + "label": "D.C. Superior Court (check if applicable instead of U.S. District Court)", + "default_checked": false + }, + { + "component_type": "text_input", + "id": "Docket No", + "label": "Docket Number", + "required": false + }, + { + "component_type": "text_input", + "id": "Code Section", + "label": "Code Section", + "required": false + } + ] + }, + { + "title": "Charge Information", + "elements": [ + { + "component_type": "paragraph", + "text": "Complete this section if you were CHARGED (but not convicted) with simple possession, attempted possession, or use of marijuana. Provide the court information, code section, and docket number." + }, + { + "component_type": "text_input", + "id": "Code Section_2", + "label": "Code Section", + "required": false + }, + { + "component_type": "text_input", + "id": "US District Court_2", + "label": "U.S. District Court (e.g., Northern, Eastern, Southern, Western)", + "required": false + }, + { + "component_type": "text_input", + "id": "District 2", + "label": "District of (State)", + "required": false + }, + { + "component_type": "checkbox", + "id": "D.C. Superior Court 2", + "label": "D.C. Superior Court (check if applicable instead of U.S. District Court)", + "default_checked": false + }, + { + "component_type": "text_input", + "id": "Docket No 2", + "label": "Docket Number", + "required": false + } + ] + }, + { + "title": "Certification and Signature", + "elements": [ + { + "component_type": "rich_text", + "text": "

Certification

With knowledge of the penalties for false statements to Federal Agencies, as provided by 18 U.S.C. § 1001, and with knowledge that this statement is submitted to affect action by the U.S. Department of Justice, I certify that:

  1. The applicant was either a U.S. citizen or lawfully present in the United States at the time of the offense.
  2. The applicant was a U.S. citizen or lawful permanent resident on December 22, 2023.
  3. The above statements, and accompanying documents, are true and complete to the best of my knowledge, information, and belief.
  4. I acknowledge that any certificate issued in reliance on the above information will be voided if the information is subsequently determined to be false.
" + }, + { + "component_type": "text_input", + "id": "App Date", + "label": "Date (MM/DD/YYYY)", + "required": true + }, + { + "component_type": "paragraph", + "text": "After completing this form, sign and submit it with any supporting documents to USPardon.Attorney@usdoj.gov or mail to: U.S. Department of Justice, Office of the Pardon Attorney, 950 Pennsylvania Avenue NW, Washington, DC 20530." + } + ] + } + ] + }, + "finishReason": "tool-calls", + "usage": { + "inputTokens": 8233, + "outputTokens": 3532, + "totalTokens": 11765, + "cachedInputTokens": 0 + }, + "warnings": [], + "providerMetadata": { + "bedrock": { + "usage": { + "cacheWriteInputTokens": 0 + }, + "isJsonResponseFromTool": true + } + }, + "response": { + "id": "aiobj-lBGkNnDR0D5WSAaylwbnO2hf", + "timestamp": "2025-10-09T05:12:43.629Z", + "modelId": "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + "headers": { + "connection": "keep-alive", + "content-length": "9747", + "content-type": "application/json", + "date": "Thu, 09 Oct 2025 05:12:43 GMT", + "x-amzn-requestid": "abf19a6e-cba7-49cd-bf04-5434f3ac2a94" + } + }, + "request": {} +} \ No newline at end of file diff --git a/packages/forms/package.json b/packages/forms/package.json index f3acfba1..8411a4c8 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -1,22 +1,51 @@ { - "name": "@gsa-tts/forms-core", - "version": "0.1.2", + "name": "@flexion/forms-core", + "version": "0.2.0", "description": "10x Forms Platform form handling", "type": "module", - "license": "CC0", + "license": "Apache-2.0", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "dist/types/index.d.ts", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "exports": { ".": { + "development": { + "types": "./src/index.ts", + "import": "./src/index.ts" + }, "types": "./dist/types/index.d.ts", "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" }, "./context": { + "development": { + "types": "./src/context/index.ts", + "import": "./src/context/index.ts" + }, "types": "./dist/types/context/index.d.ts", "import": "./dist/esm/context.js", "require": "./dist/cjs/context.js" + }, + "./documents/pdf/context": { + "development": { + "types": "./src/documents/pdf/context.ts", + "import": "./src/documents/pdf/context.ts" + }, + "types": "./dist/types/documents/pdf/context.d.ts", + "import": "./dist/esm/documents/pdf/context.js", + "require": "./dist/cjs/documents/pdf/context.js" + }, + "./repository": { + "development": { + "types": "./src/repository/index.ts", + "import": "./src/repository/index.ts" + }, + "types": "./dist/types/repository/index.d.ts", + "import": "./dist/esm/repository.js", + "require": "./dist/cjs/repository.js" } }, "scripts": { @@ -26,12 +55,17 @@ "test": "vitest run --coverage" }, "dependencies": { - "@gsa-tts/forms-common": "workspace:*", - "@gsa-tts/forms-database": "workspace:*", + "@ai-sdk/amazon-bedrock": "^3.0.30", + "@aws-sdk/credential-providers": "^3.901.0", + "@aws-sdk/types": "^3.901.0", + "@flexion/forms-common": "workspace:*", + "@flexion/forms-database": "workspace:*", + "ai": "^5.0.59", + "kysely": "^0.27.4", "pdf-lib": "^1.17.1", "qs": "^6.13.0", "set-value": "^4.1.0", - "zod": "^3.23.8" + "zod": "^4.1.11" }, "devDependencies": { "@types/qs": "^6.9.15" diff --git a/packages/forms/rollup.config.js b/packages/forms/rollup.config.js index 69d1cc40..3125c2c5 100644 --- a/packages/forms/rollup.config.js +++ b/packages/forms/rollup.config.js @@ -14,6 +14,8 @@ export default { input: { index: 'src/index.ts', context: 'src/context/index.ts', + 'documents/pdf/context': 'src/documents/pdf/context.ts', + repository: 'src/repository/index.ts', }, output: [ { diff --git a/packages/forms/src/blueprint.ts b/packages/forms/src/blueprint.ts index b9c61887..d2d7182b 100644 --- a/packages/forms/src/blueprint.ts +++ b/packages/forms/src/blueprint.ts @@ -6,7 +6,7 @@ import { generatePatternId, getPatternMap, removeChildPattern, -} from './pattern'; +} from './pattern.js'; import { type FieldsetPattern, type FormSummaryPattern, @@ -14,8 +14,8 @@ import { type PageSetPattern, type RepeaterPattern, type SequencePattern, -} from './patterns'; -import { type Blueprint, type FormOutput, type FormSummary } from './types'; +} from './patterns/index.js'; +import { type Blueprint, type FormOutput, type FormSummary } from './types.js'; export const nullBlueprint: Blueprint = { summary: { @@ -710,14 +710,33 @@ export const addFormOutput = ( /** * Updates the summary of a given form with the provided summary details. + * Also updates the form-summary pattern to keep them in sync. */ export const updateFormSummary = ( form: Blueprint, summary: FormSummary ): Blueprint => { + // Find the form-summary pattern and update it + const formSummaryPatternEntry = Object.entries(form.patterns).find( + ([_, pattern]) => pattern.type === 'form-summary' + ); + + const updatedPatterns = { ...form.patterns }; + if (formSummaryPatternEntry) { + const [patternId, pattern] = formSummaryPatternEntry; + updatedPatterns[patternId] = { + ...pattern, + data: { + ...pattern.data, + ...summary, + }, + }; + } + return { ...form, summary, + patterns: updatedPatterns, }; }; diff --git a/packages/forms/src/builder/index.ts b/packages/forms/src/builder/index.ts index 26db7aaf..201db56a 100644 --- a/packages/forms/src/builder/index.ts +++ b/packages/forms/src/builder/index.ts @@ -1,4 +1,4 @@ -import { type VoidResult } from '@gsa-tts/forms-common'; +import { type VoidResult } from '@flexion/forms-common'; import { addPageToPageSet, addPatternToFieldset, diff --git a/packages/forms/src/builder/parse-form.test.ts b/packages/forms/src/builder/parse-form.test.ts index 409fb762..72a40704 100644 --- a/packages/forms/src/builder/parse-form.test.ts +++ b/packages/forms/src/builder/parse-form.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { failure, success } from '@gsa-tts/forms-common'; +import { failure, success } from '@flexion/forms-common'; import { parseForm, parseFormString } from './parse-form'; import { defaultFormConfig, type InputPattern } from '../patterns'; @@ -139,11 +139,11 @@ describe('parseFormString', () => { '[\n' + ' {\n' + ' "code": "custom",\n' + - ' "message": "Invalid pattern",\n' + ' "path": [\n' + ' "patterns",\n' + ' "invalidPattern"\n' + - ' ]\n' + + ' ],\n' + + ' "message": "Invalid pattern"\n' + ' }\n' + ']', }); diff --git a/packages/forms/src/builder/parse-form.ts b/packages/forms/src/builder/parse-form.ts index dab74f2f..96a300d9 100644 --- a/packages/forms/src/builder/parse-form.ts +++ b/packages/forms/src/builder/parse-form.ts @@ -1,8 +1,8 @@ import * as z from 'zod'; -import { failure, success, type Result } from '@gsa-tts/forms-common'; -import type { FormConfig } from '../pattern'; -import type { Blueprint } from '../types'; +import { failure, success, type Result } from '@flexion/forms-common'; +import type { FormConfig } from '../pattern.js'; +import type { Blueprint } from '../types.js'; /** * Parses and validates an object against a form schema defined by the given configuration. diff --git a/packages/forms/src/context/browser/form-repo.ts b/packages/forms/src/context/browser/form-repo.ts index 68c163b8..c3bc138a 100644 --- a/packages/forms/src/context/browser/form-repo.ts +++ b/packages/forms/src/context/browser/form-repo.ts @@ -3,7 +3,7 @@ import { type VoidResult, failure, success, -} from '@gsa-tts/forms-common'; +} from '@flexion/forms-common'; import { FormSession, @@ -144,7 +144,7 @@ export class BrowserFormRepository implements FormRepository { addDocument(document: { fileName: string; data: Uint8Array; - extract: { parsedPdf: ParsedPdf; fields: DocumentFieldMap }; + extract?: { parsedPdf: ParsedPdf; fields: DocumentFieldMap }; }) { const documentId = crypto.randomUUID(); const data = uint8ArrayToBase64(document.data); @@ -155,7 +155,7 @@ export class BrowserFormRepository implements FormRepository { type: 'pdf', file_name: document.fileName, data, - extract: JSON.stringify(document.extract), + extract: document.extract ? JSON.stringify(document.extract) : '', }) ); return Promise.resolve( @@ -183,6 +183,33 @@ export class BrowserFormRepository implements FormRepository { data: base64ToUint8Array(json.data), }); } + + // Job methods are not supported in browser context + createFormJob(): Promise { + return Promise.resolve( + failure('Job management is not supported in browser context') + ); + } + + completeFormJob(): Promise { + return Promise.resolve( + failure('Job management is not supported in browser context') + ); + } + + failFormJob(): Promise { + return Promise.resolve( + failure('Job management is not supported in browser context') + ); + } + + getLatestFormJob(): Promise { + return Promise.resolve(success(null)); + } + + getFormJobs(): Promise { + return Promise.resolve(success([])); + } } /** diff --git a/packages/forms/src/context/browser/session-repo.ts b/packages/forms/src/context/browser/session-repo.ts index 2d33c120..e4cdbac1 100644 --- a/packages/forms/src/context/browser/session-repo.ts +++ b/packages/forms/src/context/browser/session-repo.ts @@ -1,4 +1,4 @@ -import { type Result, type VoidResult } from '@gsa-tts/forms-common'; +import { type Result, type VoidResult } from '@flexion/forms-common'; import { type FormSession } from '../../index.js'; /** diff --git a/packages/forms/src/context/index.ts b/packages/forms/src/context/index.ts index d73929c7..a0e52b57 100644 --- a/packages/forms/src/context/index.ts +++ b/packages/forms/src/context/index.ts @@ -1,13 +1,36 @@ -import type { ParsePdf } from '../documents/index.js'; import type { FormConfig } from '../pattern.js'; -import type { FormRepository } from '../repository/index.js'; +import type { FormRepository } from '../repository/types.js'; +import type { PdfParser } from '../documents/pdf/services/parser-interface.js'; +import type { ParsedPdf } from '../documents/pdf/domain/pattern-mapper.js'; +import type { DocumentFieldMap } from '../documents/types.js'; export { BrowserFormRepository } from './browser/form-repo.js'; export { createTestBrowserFormService } from './test/index.js'; +/** + * Function type for parsing PDFs. + * Parser and config are pre-configured, only requires PDF bytes. + */ +export type ParsePdfFn = ( + pdf: Uint8Array +) => Promise<{ parsedPdf: ParsedPdf; fields: DocumentFieldMap }>; + +/** + * Context for form service operations. + * The parsePdf function is created by createFormService from parser + config. + */ export type FormServiceContext = { repository: FormRepository; config: FormConfig; isUserLoggedIn: () => boolean; - parsePdf: ParsePdf; + getUserId?: () => string; + parser: PdfParser; +}; + +/** + * Internal context used within service methods. + * Includes parsePdf wrapper created by createFormService. + */ +export type InternalFormServiceContext = FormServiceContext & { + parsePdf: ParsePdfFn; }; diff --git a/packages/forms/src/context/test/index.ts b/packages/forms/src/context/test/index.ts index 8122c05d..9bd39bdf 100644 --- a/packages/forms/src/context/test/index.ts +++ b/packages/forms/src/context/test/index.ts @@ -1,5 +1,5 @@ import { BrowserFormRepository } from '../browser/form-repo.js'; -import { parsePdf } from '../../documents/pdf/index.js'; +import { createSimpleFakeParser } from '../../documents/pdf/index.js'; import { defaultFormConfig } from '../../patterns/index.js'; import { type FormService, createFormService } from '../../services/index.js'; @@ -15,7 +15,7 @@ export const createTestBrowserFormService = ( repository, config: defaultFormConfig, isUserLoggedIn: () => true, - parsePdf, + parser: createSimpleFakeParser(), }); if (testData) { Object.entries(testData).forEach(([id, blueprint]) => { diff --git a/packages/forms/src/documents/__tests__/document.test.ts b/packages/forms/src/documents/__tests__/document.test.ts index ccb42df1..e9657761 100644 --- a/packages/forms/src/documents/__tests__/document.test.ts +++ b/packages/forms/src/documents/__tests__/document.test.ts @@ -8,6 +8,7 @@ import { type PagePattern } from '../../patterns/page/config.js'; import { addDocument } from '../document.js'; import { loadSamplePDF } from './sample-data.js'; +import { createSimpleFakeParser } from '../pdf/adapters/fake-parser.js'; describe('addDocument document processing', () => { it('creates expected blueprint', async () => { @@ -22,10 +23,7 @@ describe('addDocument document processing', () => { data: new Uint8Array(pdfBytes), }, { - fetchPdfApiResponse: async () => { - const { mockResponse } = await import('../pdf/mock-response.js'); - return mockResponse; - }, + parser: createSimpleFakeParser(), } ); const rootPattern = getPattern( @@ -35,7 +33,7 @@ describe('addDocument document processing', () => { console.error(JSON.stringify(errors, null, 2)); // Fix these expect(rootPattern).toEqual(expect.objectContaining({ type: 'page-set' })); - expect(rootPattern.data.pages.length).toEqual(4); + expect(rootPattern.data.pages.length).toEqual(1); for (let page = 0; page < rootPattern.data.pages.length; page++) { const pagePattern = getPattern( updatedForm, diff --git a/packages/forms/src/documents/__tests__/doj-pardon-marijuana.test.ts b/packages/forms/src/documents/__tests__/doj-pardon-marijuana.test.ts index d0582b19..b8b3cd92 100644 --- a/packages/forms/src/documents/__tests__/doj-pardon-marijuana.test.ts +++ b/packages/forms/src/documents/__tests__/doj-pardon-marijuana.test.ts @@ -1,10 +1,11 @@ import { describe, expect, test } from 'vitest'; -import { Success } from '@gsa-tts/forms-common'; +import { Success } from '@flexion/forms-common'; import { type DocumentFieldMap } from '../index.js'; -import { fillPDF } from '../pdf/index.js'; +import { fillPDF, parsePdf } from '../pdf/index.js'; import { getDocumentFieldData } from '../pdf/extract.js'; +import { PageSetPattern } from '../../patterns/page-set/config.js'; import { loadSamplePDF } from './sample-data.js'; @@ -79,6 +80,43 @@ describe('DOJ Pardon Attorney Office - Marijuana pardon application form', () => value: '12345', }); }); + + test('generates guided interview from PDF via Bedrock', async () => { + const pdfBytes = await loadSamplePDF( + 'doj-pardon-marijuana/demo-application_for_certificate_of_pardon_for_simple_marijuana_possession.pdf' + ); + + const { createTestPdfParser } = await import('../pdf/context.js'); + const { defaultFormConfig } = await import('../../patterns/index.js'); + const parser = createTestPdfParser(); + const result = await parsePdf( + { parser, formConfig: defaultFormConfig }, + pdfBytes + ); + const { parsedPdf, fields } = result; + + // Should create valid pattern structure + expect(parsedPdf.root).toBe('root'); + expect(parsedPdf.patterns['root']).toBeDefined(); + expect(parsedPdf.errors.length).toBe(0); + + // Should organize into pages + const rootPattern = parsedPdf.patterns['root'] as PageSetPattern; + expect(rootPattern.data.pages.length).toBeGreaterThan(0); + + // Should maintain field mappings + const fieldNames = Object.values(parsedPdf.outputs).map( + output => output.name + ); + expect(fieldNames.length).toBeGreaterThan(0); + + // Should have a title and description + expect(parsedPdf.title).toBeTruthy(); + expect(parsedPdf.description).toBeTruthy(); + + // Should also extract raw field data + expect(Object.keys(fields).length).toBeGreaterThan(0); + }, 300000); // 5 minute timeout for initial Bedrock call and caching }); const getFieldByName = (fields: DocumentFieldMap, name: string) => { diff --git a/packages/forms/src/documents/__tests__/fill-pdf.test.ts b/packages/forms/src/documents/__tests__/fill-pdf.test.ts index fb349661..2bd596c2 100644 --- a/packages/forms/src/documents/__tests__/fill-pdf.test.ts +++ b/packages/forms/src/documents/__tests__/fill-pdf.test.ts @@ -1,6 +1,6 @@ import { beforeAll, describe, expect, it } from 'vitest'; -import { type Failure, type Success } from '@gsa-tts/forms-common'; +import { type Failure, type Success } from '@flexion/forms-common'; import { fillPDF } from '../index.js'; import { loadSamplePDF } from './sample-data.js'; diff --git a/packages/forms/src/documents/__tests__/test-documents.ts b/packages/forms/src/documents/__tests__/test-documents.ts index 1febb015..0c674e50 100644 --- a/packages/forms/src/documents/__tests__/test-documents.ts +++ b/packages/forms/src/documents/__tests__/test-documents.ts @@ -4,12 +4,55 @@ import { defaultFormConfig } from '../../patterns'; import { addDocument } from '../document'; import { type Blueprint } from '../..'; import { loadSamplePDF } from './sample-data'; +import { FakePdfParser } from '../pdf/adapters/fake-parser.js'; +import type { ExtractedForm } from '../pdf/domain/schema.js'; + +// Simple mock for testing +const MOCK_EXTRACTED: ExtractedForm = { + form_summary: { + title: 'Test Form', + description: 'Test form for unit tests', + }, + pages: [ + { + title: 'Personal Information', + elements: [ + { + component_type: 'paragraph', + text: 'Please provide your information', + }, + { + component_type: 'fieldset', + legend: 'Name', + fields: [ + { + component_type: 'text_input', + id: 'firstName', + label: 'First Name', + required: true, + }, + { + component_type: 'text_input', + id: 'lastName', + label: 'Last Name', + required: true, + }, + ], + }, + ], + }, + ], +}; export const createTestFormWithPDF = async () => { const pdfBytes = await loadSamplePDF( 'doj-pardon-marijuana/demo-application_for_certificate_of_pardon_for_simple_marijuana_possession.pdf' ); const builder = new BlueprintBuilder(defaultFormConfig); + + // Use fake parser for testing + const fakeParser = new FakePdfParser(MOCK_EXTRACTED); + const { updatedForm } = await addDocument( builder.form, { @@ -17,7 +60,7 @@ export const createTestFormWithPDF = async () => { data: new Uint8Array(pdfBytes), }, { - fetchPdfApiResponse: async () => SERVICE_RESPONSE, + parser: fakeParser, } ); @@ -34,1318 +77,3 @@ export const getMockFormData = (form: Blueprint): PatternValueMap => { return acc; }, {} as PatternValueMap); }; - -const SERVICE_RESPONSE = { - message: 'PDF parsed successfully', - parsed_pdf: { - raw_text: - 'OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nOn October 6, 2022, President Biden issued a presidential proclamation that pardoned many federal and D.C. \noffenses for simple marijuana possession. On December 22, 2023, President Biden issued another proclamation that \nexpanded the relief provided by the original proclamation by pardoning the federal offenses of simple possession, \nattempted possession, and use of marijuana. \nHow a pardon can help you \nA pardon is an expression of the President\u2019s forgiveness. It does not mean you are innocent or expunge your \nconviction. But it does remove civil disabilities\u2014such as restrictions on the right to vote, to hold office, or to sit \non a jury\u2014that are imposed because of the pardoned conviction. It may also be helpful in obtaining licenses, \nbonding, or employment. Learn more about the pardon. \nYou qualify for the pardon if: \n\u2022 On or before December 22, 2023, you were charged with or convicted of simple possession, attempted \npossession, or use of marijuana under the federal code, the District of Columbia code, or the Code of \nFederal Regulations \n\u2022 You were a U.S. citizen or lawfully present in the United States at the time of the offense \n\u2022 You were a U.S. citizen or lawful permanent resident on December 22, 2023 \nRequest a certificate to show proof of the pardon \nA Certificate of Pardon is proof that you were pardoned under the proclamation. The certificate is the only \ndocumentation you will receive of the pardon. Use the application below to start your request. \nWhat you\'ll need for the request \nAbout you \nYou can submit a request for yourself or someone else can submit on your behalf. You must provide \npersonal details, like name or citizenship status and either a mailing address, an email address or both to \ncontact you. We strongly recommend including an email address, if available, as we may not be able to \nrespond as quickly if you do not provide it. You can also use the mailing address or email address of \nanother person, if you do not have your own. \nAbout the charge or conviction \nYou must state whether it was a charge or conviction, the court district where it happened, and the date \n(month, day, year). If possible, you should also: \n\u2022 enter information about your case (docket or case number and the code section that was \ncharged) \n\u2022 upload your documents \no charging documents, like the indictment, complaint, criminal information, ticket or \ncitation; or \no conviction documents, like the judgment of conviction, the court docket sheet showing \nthe sentence and date it was imposed, or if you did not go to court, the receipt showing \npayment of fine \nIf you were charged by a ticket or citation and paid a fine instead of appearing in court, you should also provide the \ndate of conviction or the date the fine was paid. \nWithout this information, we can\'t guarantee that we\'ll be able to determine if you qualify for the pardon under \nthe proclamation. \n \nPage 1 of 4 \nUnited States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024 OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nInstructions: \nAn online version of this application is available at: Presidential Proclamation on Marijuana Possession \n(justice.gov). You can also complete and return this application with the required documents to \nUSPardon.Attorney@usdoj.gov or U.S. Department of Justice, Office of the Pardon Attorney, 950 Pennsylvania \nAvenue NW, Washington, DC 20530. \nPublic Burden Statement: \nThis collection meets the requirements of 44 U.S.C. \u00a7 3507, as amended by the Paperwork Reduction Act of 1995. \nWe estimate that it will take 120 minutes to read the instructions, gather the relevant materials, and answer \nquestions on the form. Send comments regarding the burden estimate or any other aspect of this collection of \ninformation, including suggestions for reducing this burden, to Office of the Pardon Attorney, U.S. Department of \nJustice, Attn: OMB Number 1123-0014, RFK Building, 950 Pennsylvania Avenue, N.W., Washington DC 20530. \nThe OMB Clearance number, 1123-0014, is currently valid. \nPrivacy Act Statement: \nThe Office of the Pardon Attorney has authority to collect this information under the U.S. Constitution, Article \nII, Section 2 (the pardon clause); Orders of the Attorney General Nos. 1798-93, 58 Fed. Reg. 53658 and 53659 \n(1993), 2317-2000, 65 Fed. Reg. 48381 (2000), and 2323-2000, 65 Fed. Reg. 58223 and 58224 (2000), codified in \n28 C.F.R. \u00a7\u00a7 1.1 et seq. (the rules governing petitions for executive clemency); and Order of the Attorney General \nNo. 1012-83, 48 Fed. Reg. 22290 (1983), as codified in 28 C.F.R. \u00a7\u00a7 0.35 and 0.36 (the authority of the Office of \nthe Pardon Attorney). The principal purpose for collecting this information is to enable the Office of the Pardon \nAttorney to issue an individual certificate of pardon to you. The routine uses which may be made of this \ninformation include provision of data to the President and his staff, other governmental entities, and the public. \nThe full list of routine uses for this correspondence can be found in the System of Records Notice titled, \u201cPrivacy \nAct of 1974; System of Records,\u201d published in Federal Register, September 15, 2011, Vol. 76, No. 179, at pages \n57078 through 57080; as amended by \u201cPrivacy Act of 1974; System of Records,\u201d published in the Federal \nRegister, May 25, 2017, Vol. 82, No. 100, at page 24161, and at the U.S. Department of Justice, Office of Privacy \nand Civil Liberties\' website at: https://www.justice.gov/opcl/doj-systems-records#OPA. \nBy signing the attached form, you consent to allowing the Office of the Pardon Attorney to obtain information \nregarding your citizenship and/or immigration status from the courts, from other government agencies, from other \ncomponents within the Department of Justice, and from the Department of Homeland Security, U.S. Citizenship \nand Immigration Services (DHS-USCIS), Systematic Alien Verification for Entitlements (SAVE) program. The \ninformation received from these sources will be used for the sole purposes of determining an applicant\'s \nqualification for a Certificate of Pardon under the December 22 proclamation and for record-keeping of those \ndeterminations. Further, please be aware that if the Office of the Pardon Attorney is unable to verify your \ncitizenship or immigration status based on the information provided below, we may contact you to obtain \nadditional verification information. Learn more about the DHS-USCIS\'s SAVE program and its ordinary uses. \nYour disclosure of information to the Office of the Pardon Attorney on this form is voluntary. If you do not \ncomplete all or some of the information fields in this form, however, the Office of the Pardon Attorney may not be \nable to effectively respond. Information regarding gender, race, or ethnicity is not required and will not affect the \nprocessing of the application. \nNote: Submit a separate form for each conviction or charge for which you are seeking a certificate of pardon. \nApplication Form on page 3. \nPage 2 of 4 \nUnited States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024 OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nComplete the following: \nName: \n(first) (middle) (last) \nName at Conviction: \n(if different) (first) (middle) (last) \nAddress: \n(number) (street) (apartment/unit no.) \n\n(city) (state) (Zip Code) \nEmail Address: Phone Number: \nDate of Birth: Gender: Are you Hispanic or Latino?: Yes No \nRace: Alaska Native or American Indian Asian Black or African American \nNative Hawaiian or Other Pacific Islander White Other \nCitizenship or Residency Status: \nU.S. citizen by birth \nU.S. naturalized citizen Date Naturalization Granted: \nLawful Permanent Resident Date Residency Granted: \nAlien Registration Number (A-Number), Certificate of Naturalization Number, or Citizenship Number \n(if applicant is a lawful permanent resident or naturalized citizen): \n(A-Number) \n1. Applicant was convicted on: in the U.S. District Court for the \n(month/day/year) (Northern, etc.) \nDistrict of (state) or D.C. Superior Court of simple possession of marijuana, under \nDocket No. : and Code Section: ; OR \n(docket number) (code section) \n2. Applicant was charged with Code Section: in the U.S. District Court for the \n(code section) (Eastern, etc.) \nDistrict of or D.C. Superior Court under Docket No: \n(state) (docket number) \n \nUnited States Department of Justice Office of the Pardon Attorney Page 3 of 4 Washington, D.C. 20530 January 2024 OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nWith knowledge of the penalties for false statements to Federal Agencies, as provided by 18 \nU.S.C. \u00a7 1001, and with knowledge that this statement is submitted by me to affect action by \nthe U.S. Department of Justice, I certify that: \n1. The applicant was either a U.S. citizen or lawfully present in the United States at the time of the \noffense. \n2. The applicant was a U.S. citizen or lawful permanent resident on December 22, 2023. \n3. The above statements, and accompanying documents, are true and complete to the \n best of my knowledge, information, and belief. \n4. I acknowledge that any certificate issued in reliance on the above information will be \nvoided, if the information is subsequently determined to be false. \n\n(date) (signature) \nPage 4 of 4 \nUnited States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024 ', - form_summary: { - component_type: 'form_summary', - title: 'My Form Title', - description: 'My Form Description', - }, - elements: [ - { - component_type: 'paragraph', - text: "OMB Control No: 1123-0014 Expires 03/31/2027 APPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF SIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF MARIJUANA On October 6, 2022, President Biden issued a presidential proclamation that pardoned many federal and D.C. offenses for simple marijuana possession. On December 22, 2023, President Biden issued another proclamation that expanded the relief provided by the original proclamation by pardoning the federal offenses of simple possession, attempted possession, and use of marijuana. How a pardon can help you A pardon is an expression of the President\u2019s forgiveness. It does not mean you are innocent or expunge your conviction. But it does remove civil disabilities\u2014such as restrictions on the right to vote, to hold office, or to sit on a jury\u2014that are imposed because of the pardoned conviction. It may also be helpful in obtaining licenses, bonding, or employment. Learn more about the pardon. You qualify for the pardon if: \u2022 On or before December 22, 2023, you were charged with or convicted of simple possession, attempted possession, or use of marijuana under the federal code, the District of Columbia code, or the Code of Federal Regulations \u2022 You were a U.S. citizen or lawfully present in the United States at the time of the offense \u2022 You were a U.S. citizen or lawful permanent resident on December 22, 2023 Request a certificate to show proof of the pardon A Certificate of Pardon is proof that you were pardoned under the proclamation. The certificate is the only documentation you will receive of the pardon. Use the application below to start your request. What you'll need for the request About you You can submit a request for yourself or someone else can submit on your behalf. You must provide personal details, like name or citizenship status and either a mailing address, an email address or both to contact you. We strongly recommend including an email address, if available, as we may not be able to respond as quickly if you do not provide it. You can also use the mailing address or email address of another person, if you do not have your own. About the charge or conviction You must state whether it was a charge or conviction, the court district where it happened, and the date (month, day, year). If possible, you should also: \u2022 enter information about your case (docket or case number and the code section that was charged) \u2022 upload your documents o charging documents, like the indictment, complaint, criminal information, ticket or citation; or o conviction documents, like the judgment of conviction, the court docket sheet showing the sentence and date it was imposed, or if you did not go to court, the receipt showing payment of fine If you were charged by a ticket or citation and paid a fine instead of appearing in court, you should also provide the date of conviction or the date the fine was paid. Without this information, we can't guarantee that we'll be able to determine if you qualify for the pardon under the proclamation. Page 1 of 4 United States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024", - style: 'normal', - page: 0, - }, - { - component_type: 'paragraph', - text: "OMB Control No: 1123-0014 Expires 03/31/2027 APPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF SIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF MARIJUANA Instructions: An online version of this application is available at: Presidential Proclamation on Marijuana Possession (justice.gov). You can also complete and return this application with the required documents to USPardon.Attorney@usdoj.gov or U.S. Department of Justice, Office of the Pardon Attorney, 950 Pennsylvania Avenue NW, Washington, DC 20530. Public Burden Statement: This collection meets the requirements of 44 U.S.C. \u00a7 3507, as amended by the Paperwork Reduction Act of 1995. We estimate that it will take 120 minutes to read the instructions, gather the relevant materials, and answer questions on the form. Send comments regarding the burden estimate or any other aspect of this collection of information, including suggestions for reducing this burden, to Office of the Pardon Attorney, U.S. Department of Justice, Attn: OMB Number 1123-0014, RFK Building, 950 Pennsylvania Avenue, N.W., Washington DC 20530. The OMB Clearance number, 1123-0014, is currently valid. Privacy Act Statement: The Office of the Pardon Attorney has authority to collect this information under the U.S. Constitution, Article II, Section 2 (the pardon clause); Orders of the Attorney General Nos. 1798-93, 58 Fed. Reg. 53658 and 53659 (1993), 2317-2000, 65 Fed. Reg. 48381 (2000), and 2323-2000, 65 Fed. Reg. 58223 and 58224 (2000), codified in 28 C.F.R. \u00a7\u00a7 1.1 et seq. (the rules governing petitions for executive clemency); and Order of the Attorney General No. 1012-83, 48 Fed. Reg. 22290 (1983), as codified in 28 C.F.R. \u00a7\u00a7 0.35 and 0.36 (the authority of the Office of the Pardon Attorney). The principal purpose for collecting this information is to enable the Office of the Pardon Attorney to issue an individual certificate of pardon to you. The routine uses which may be made of this information include provision of data to the President and his staff, other governmental entities, and the public. The full list of routine uses for this correspondence can be found in the System of Records Notice titled, \u201cPrivacy Act of 1974; System of Records,\u201d published in Federal Register, September 15, 2011, Vol. 76, No. 179, at pages 57078 through 57080; as amended by \u201cPrivacy Act of 1974; System of Records,\u201d published in the Federal Register, May 25, 2017, Vol. 82, No. 100, at page 24161, and at the U.S. Department of Justice, Office of Privacy and Civil Liberties' website at: https://www.justice.gov/opcl/doj-systems-records#OPA. By signing the attached form, you consent to allowing the Office of the Pardon Attorney to obtain information regarding your citizenship and/or immigration status from the courts, from other government agencies, from other components within the Department of Justice, and from the Department of Homeland Security, U.S. Citizenship and Immigration Services (DHS-USCIS), Systematic Alien Verification for Entitlements (SAVE) program. The information received from these sources will be used for the sole purposes of determining an applicant's qualification for a Certificate of Pardon under the December 22 proclamation and for record-keeping of those determinations. Further, please be aware that if the Office of the Pardon Attorney is unable to verify your citizenship or immigration status based on the information provided below, we may contact you to obtain additional verification information. Learn more about the DHS-USCIS's SAVE program and its ordinary uses. Your disclosure of information to the Office of the Pardon Attorney on this form is voluntary. If you do not complete all or some of the information fields in this form, however, the Office of the Pardon Attorney may not be able to effectively respond. Information regarding gender, race, or ethnicity is not required and will not affect the processing of the application. Note: Submit a separate form for each conviction or charge for which you are seeking a certificate of pardon. Application Form on page 3. Page 2 of 4 United States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024", - style: 'normal', - page: 1, - }, - { - component_type: 'paragraph', - text: 'OMB Control No: 1123-0014 Expires 03/31/2027 APPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF SIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF MARIJUANA Complete the following:', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Name: ', - fields: [ - { - component_type: 'text_input', - id: 'Fst Name 1', - label: 'First Name', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: '', - label: 'Middle Name', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: '', - label: 'Last Name', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(first) (middle) (last)', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Name at Conviction: ', - fields: [ - { - component_type: 'text_input', - id: 'Conv Fst Name', - label: 'First Name at Conviction', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: 'Conv Mid Name', - label: 'Middle Name at Conviction', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: 'Conv Lst Name', - label: 'Last Name at Conviction', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(if different) (first) (middle) (last)', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Address: ', - fields: [ - { - component_type: 'text_input', - id: 'Address', - label: 'Address (number, street, apartment/unit number)', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(number) (street) (apartment/unit no.)', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'City', - fields: [ - { - component_type: 'text_input', - id: 'City', - label: 'City', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: 'State', - label: 'State', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: 'Zip Code', - label: '(Zip Code)', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(city) (state) (Zip Code)', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Email Address: ', - fields: [ - { - component_type: 'text_input', - id: 'Email Address', - label: 'Email Address', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Phone Number: ', - fields: [ - { - component_type: 'text_input', - id: 'Phone Number', - label: 'Phone Number', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: 'Date of Birth: Gender:', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Date of Birth', - fields: [ - { - component_type: 'text_input', - id: 'Date of Birth', - label: 'Date of Birth', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: 'Gender', - label: 'Gender', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'radio_group', - legend: 'Are you Hispanic or Latino?: ', - options: [ - { - id: 'Yes', - label: 'Yes ', - name: 'Yes', - default_checked: false, - page: 2, - }, - { - id: 'No', - label: 'No ', - name: 'No', - default_checked: false, - page: 2, - }, - ], - id: 'Ethnicity', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Race:', - fields: [ - { - component_type: 'checkbox', - id: 'Nat Amer', - label: 'Alaska Native or American Indian ', - default_checked: false, - struct_parent: 20, - page: 2, - }, - { - component_type: 'checkbox', - id: 'Asian', - label: 'Asian ', - default_checked: false, - struct_parent: 21, - page: 2, - }, - { - component_type: 'checkbox', - id: 'Blck Amer', - label: 'Black or African American ', - default_checked: false, - struct_parent: 22, - page: 2, - }, - { - component_type: 'checkbox', - id: 'Nat Haw Islander', - label: 'Native Hawaiian or Other Pacific Islander ', - default_checked: false, - struct_parent: 23, - page: 2, - }, - { - component_type: 'checkbox', - id: 'White', - label: 'White ', - default_checked: false, - struct_parent: 24, - page: 2, - }, - { - component_type: 'checkbox', - id: 'Other', - label: 'Other ', - default_checked: false, - struct_parent: 25, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'radio_group', - legend: 'Citizenship or Residency Status: ', - options: [ - { - id: 'Birth', - label: 'U.S. citizen by birth ', - name: 'Birth', - default_checked: false, - page: 2, - }, - { - id: 'Naturalized', - label: 'U.S. naturalized citizen ', - name: 'Naturalized', - default_checked: false, - page: 2, - }, - { - id: 'Permanent_Resident', - label: 'Lawful Permanent Resident ', - name: 'Permanent_Resident', - default_checked: false, - page: 2, - }, - ], - id: 'Citizenship', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'U.S. naturalized citizen ', - fields: [ - { - component_type: 'text_input', - id: 'Residency Date_af_date', - label: 'Date Residency Granted (mm/dd/yyyy)', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: '', - label: 'date naturalization granted', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: 'Date Residency Granted: Alien Registration Number (A-Number), Certificate of Naturalization Number, or Citizenship Number', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: - '(if applicant is a lawful permanent resident or naturalized citizen): ', - fields: [ - { - component_type: 'text_input', - id: 'A-Number', - label: 'Alien Registration, Naturalization, or Citizenship Number', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(A-Number) 1.', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: ' Applicant was convicted on: ', - fields: [ - { - component_type: 'text_input', - id: 'Convict-Date_af_date', - label: 'Convict Date', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'fieldset', - legend: 'in the U.S. District Court for the ', - fields: [ - { - component_type: 'text_input', - id: 'US District Court', - label: 'US District Court', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(month/day/year) (Northern, etc.)', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'District of ', - fields: [ - { - component_type: 'text_input', - id: 'Dist State', - label: 'State', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(state) or D.C. Superior Court of simple possession of marijuana, under :', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Docket No. ', - fields: [ - { - component_type: 'text_input', - id: 'Docket No', - label: 'Docket Number', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: ';', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'and Code Section: ', - fields: [ - { - component_type: 'text_input', - id: 'Code Section', - label: 'Code Section', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: 'OR (docket number) (code section) 2.', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: ' Applicant was charged with Code Section: ', - fields: [ - { - component_type: 'text_input', - id: 'Code Section_2', - label: 'Code Section', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'fieldset', - legend: 'in the U.S. District Court for the ', - fields: [ - { - component_type: 'text_input', - id: 'US District Court_2', - label: 'U.S. District Court', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(code section) (Eastern, etc.)', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'District of ', - fields: [ - { - component_type: 'text_input', - id: 'District 2', - label: 'State', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: 'or', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'D.C. Superior Court under Docket No: ', - fields: [ - { - component_type: 'text_input', - id: 'Docket No 2', - label: 'Docket No 2', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(state) (docket number) United States Department of Justice Office of the Pardon Attorney Page 3 of 4 Washington, D.C. 20530 January 2024', - style: 'normal', - page: 2, - }, - { - component_type: 'paragraph', - text: 'OMB Control No: 1123-0014 Expires 03/31/2027 APPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF SIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF MARIJUANA With knowledge of the penalties for false statements to Federal Agencies, as provided by 18 U.S.C. \u00a7 1001, and with knowledge that this statement is submitted by me to affect action by the U.S. Department of Justice, I certify that: 1. The applicant was either a U.S. citizen or lawfully present in the United States at the time of the offense. 2. The applicant was a U.S. citizen or lawful permanent resident on December 22, 2023. 3. The above statements, and accompanying documents, are true and complete to the best of my knowledge, information, and belief. 4. I acknowledge that any certificate issued in reliance on the above information will be voided, if the information is subsequently determined to be false.', - style: 'normal', - page: 3, - }, - { - component_type: 'fieldset', - legend: 'App Date', - fields: [ - { - component_type: 'text_input', - id: 'App Date', - label: 'Date', - default_value: '', - required: true, - page: 3, - }, - ], - page: 3, - }, - { - component_type: 'paragraph', - text: '(date) (signature) Page 4 of 4 United States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024', - style: 'normal', - page: 3, - }, - ], - raw_fields: { - '0': [], - '1': [], - '2': [ - { - type: '/Tx', - var_name: 'Fst Name 1', - field_dict: { - field_type: '/Tx', - coordinates: [97.0, 636.960022, 233.279999, 659.640015], - field_label: 'Fst Name 1', - field_instructions: 'First Name', - struct_parent: 4, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Fst Name 1', - struct_parent: 4, - }, - { - type: '/Tx', - var_name: '', - field_dict: { - coordinates: [233.087006, 637.580994, 390.214996, 659.320007], - field_instructions: 'Middle Name', - struct_parent: 5, - name: 0, - field_type: '/Tx', - font_info: '', - field_label: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Mid Name 1/0', - struct_parent: 5, - }, - { - type: '/Tx', - var_name: '', - field_dict: { - coordinates: [390.996002, 637.492981, 548.124023, 659.231995], - field_instructions: 'Last Name', - struct_parent: 6, - name: 0, - field_type: '/Tx', - font_info: '', - field_label: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Lst Name 1/0', - struct_parent: 6, - }, - { - type: '/Tx', - var_name: 'Conv Fst Name', - field_dict: { - field_type: '/Tx', - coordinates: [153.740005, 598.085022, 283.246002, 620.765015], - field_label: 'Conv Fst Name', - field_instructions: 'First Name at Conviction', - struct_parent: 7, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Conv Fst Name', - struct_parent: 7, - }, - { - type: '/Tx', - var_name: 'Conv Mid Name', - field_dict: { - field_type: '/Tx', - coordinates: [282.497986, 598.164001, 410.80899, 620.843994], - field_label: 'Conv Mid Name', - field_instructions: 'Middle Name at Conviction', - struct_parent: 8, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Conv Mid Name', - struct_parent: 8, - }, - { - type: '/Tx', - var_name: 'Conv Lst Name', - field_dict: { - field_type: '/Tx', - coordinates: [410.212006, 597.677002, 536.132019, 620.357971], - field_label: 'Conv Lst Name', - field_instructions: 'Last Name at Conviction', - struct_parent: 9, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Conv Lst Name', - struct_parent: 9, - }, - { - type: '/Tx', - var_name: 'Address', - field_dict: { - field_type: '/Tx', - coordinates: [102.839996, 563.880005, 547.080017, 586.559998], - field_label: 'Address', - field_instructions: - 'Address (number, street, apartment/unit number)', - struct_parent: 10, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Address', - struct_parent: 10, - }, - { - type: '/Tx', - var_name: 'City', - field_dict: { - field_type: '/Tx', - coordinates: [64.500504, 531.0, 269.519989, 551.880005], - field_label: 'City', - field_instructions: 'City', - struct_parent: 11, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'City', - struct_parent: 11, - }, - { - type: '/Tx', - var_name: 'State', - field_dict: { - field_type: '/Tx', - coordinates: [273.959991, 531.0, 440.519989, 551.880005], - field_label: 'State', - field_instructions: 'State', - struct_parent: 12, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'State', - struct_parent: 12, - }, - { - type: '/Tx', - var_name: 'Zip Code', - field_dict: { - field_type: '/Tx', - coordinates: [444.959991, 531.0, 552.719971, 551.880005], - field_label: 'Zip Code', - field_instructions: '(Zip Code)', - struct_parent: 13, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Zip Code', - struct_parent: 13, - }, - { - type: '/Tx', - var_name: 'Email Address', - field_dict: { - field_type: '/Tx', - coordinates: [131.863998, 489.600006, 290.743988, 512.280029], - field_label: 'Email Address', - field_instructions: 'Email Address', - struct_parent: 14, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Email Address', - struct_parent: 14, - }, - { - type: '/Tx', - var_name: 'Phone Number', - field_dict: { - field_type: '/Tx', - coordinates: [385.679993, 489.600006, 549.599976, 512.280029], - field_label: 'Phone Number', - field_instructions: 'Phone Number', - struct_parent: 15, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Phone Number', - struct_parent: 15, - }, - { - type: '/Tx', - var_name: 'Date of Birth', - field_dict: { - field_type: '/Tx', - coordinates: [126.480003, 451.679993, 197.880005, 474.359985], - field_label: 'Date of Birth', - field_instructions: 'Date of Birth', - struct_parent: 16, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Date of Birth', - struct_parent: 16, - }, - { - type: '/Tx', - var_name: 'Gender', - field_dict: { - field_type: '/Tx', - coordinates: [241.559998, 451.679993, 313.079987, 474.359985], - field_label: 'Gender', - field_instructions: 'Gender', - struct_parent: 17, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Gender', - struct_parent: 17, - }, - { - type: '/Btn', - var_name: '', - field_dict: { - coordinates: [505.618988, 450.865997, 523.619019, 468.865997], - struct_parent: 18, - name: 'Yes', - field_type: '/Btn', - field_instructions: '', - font_info: '', - field_label: '', - flags: { - '15': 'NoToggleToOff: (Radio buttons only) If set, exactly one radio button must be selected at all times; clicking the currently selected button has no effect. If clear, clicking the selected button deselects it, leaving no button selected.', - '16': 'Radio: If set, the field is a set of radio buttons; if clear, the field is a check box. This flag is meaningful only if the Pushbutton flag is clear.', - }, - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Ethnicity/Yes', - struct_parent: 18, - }, - { - type: '/Btn', - var_name: '', - field_dict: { - coordinates: [558.213013, 450.865997, 576.213013, 468.865997], - struct_parent: 19, - name: 'No', - field_type: '/Btn', - field_instructions: '', - font_info: '', - field_label: '', - flags: { - '15': 'NoToggleToOff: (Radio buttons only) If set, exactly one radio button must be selected at all times; clicking the currently selected button has no effect. If clear, clicking the selected button deselects it, leaving no button selected.', - '16': 'Radio: If set, the field is a set of radio buttons; if clear, the field is a check box. This flag is meaningful only if the Pushbutton flag is clear.', - }, - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Ethnicity/No', - struct_parent: 19, - }, - { - type: '/Btn', - var_name: 'Nat Amer', - field_dict: { - field_type: '/Btn', - coordinates: [280.10199, 426.162994, 298.10199, 444.162994], - field_label: 'Nat Amer', - field_instructions: 'Alaska Native or American Indian', - struct_parent: 20, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Nat Amer', - struct_parent: 20, - }, - { - type: '/Btn', - var_name: 'Asian', - field_dict: { - field_type: '/Btn', - coordinates: [366.563995, 426.162994, 384.563995, 444.162994], - field_label: 'Asian', - field_instructions: 'Asian', - struct_parent: 21, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Asian', - struct_parent: 21, - }, - { - type: '/Btn', - var_name: 'Blck Amer', - field_dict: { - field_type: '/Btn', - coordinates: [531.517029, 426.162994, 549.517029, 444.162994], - field_label: 'Blck Amer', - field_instructions: 'Black or African American', - struct_parent: 22, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Blck Amer', - struct_parent: 22, - }, - { - type: '/Btn', - var_name: 'Nat Haw Islander', - field_dict: { - field_type: '/Btn', - coordinates: [309.587006, 401.061005, 327.587006, 419.061005], - field_label: 'Nat Haw Islander', - field_instructions: 'Native Hawaiian or Other Pacific Islander', - struct_parent: 23, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Nat Haw Islander', - struct_parent: 23, - }, - { - type: '/Btn', - var_name: 'White', - field_dict: { - field_type: '/Btn', - coordinates: [438.681, 401.061005, 456.681, 419.061005], - field_label: 'White', - field_instructions: 'White', - struct_parent: 24, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'White', - struct_parent: 24, - }, - { - type: '/Btn', - var_name: 'Other', - field_dict: { - field_type: '/Btn', - coordinates: [508.806, 401.061005, 526.80603, 419.061005], - field_label: 'Other', - field_instructions: 'Other', - struct_parent: 25, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Other', - struct_parent: 25, - }, - { - type: '/Btn', - var_name: '', - field_dict: { - coordinates: [98.414398, 349.662994, 116.414001, 367.662994], - field_instructions: 'U S Citizen by birth', - struct_parent: 26, - name: 'Birth', - field_type: '/Btn', - font_info: '', - field_label: '', - flags: { - '15': 'NoToggleToOff: (Radio buttons only) If set, exactly one radio button must be selected at all times; clicking the currently selected button has no effect. If clear, clicking the selected button deselects it, leaving no button selected.', - '16': 'Radio: If set, the field is a set of radio buttons; if clear, the field is a check box. This flag is meaningful only if the Pushbutton flag is clear.', - }, - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Citizenship/Birth', - struct_parent: 26, - }, - { - type: '/Btn', - var_name: '', - field_dict: { - coordinates: [98.414398, 331.733002, 116.414001, 349.733002], - field_instructions: 'U S naturalized citizen', - struct_parent: 27, - name: 'Naturalized', - field_type: '/Btn', - font_info: '', - field_label: '', - flags: { - '15': 'NoToggleToOff: (Radio buttons only) If set, exactly one radio button must be selected at all times; clicking the currently selected button has no effect. If clear, clicking the selected button deselects it, leaving no button selected.', - '16': 'Radio: If set, the field is a set of radio buttons; if clear, the field is a check box. This flag is meaningful only if the Pushbutton flag is clear.', - }, - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Citizenship/Naturalized', - struct_parent: 27, - }, - { - type: '/Btn', - var_name: '', - field_dict: { - coordinates: [98.414398, 313.006012, 116.414001, 331.006012], - field_instructions: 'Lawful Permenent Resident', - struct_parent: 29, - name: 'Permanent Resident', - field_type: '/Btn', - font_info: '', - field_label: '', - flags: { - '15': 'NoToggleToOff: (Radio buttons only) If set, exactly one radio button must be selected at all times; clicking the currently selected button has no effect. If clear, clicking the selected button deselects it, leaving no button selected.', - '16': 'Radio: If set, the field is a set of radio buttons; if clear, the field is a check box. This flag is meaningful only if the Pushbutton flag is clear.', - }, - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Citizenship/Permanent Resident', - struct_parent: 29, - }, - { - type: '/Tx', - var_name: '', - field_dict: { - coordinates: [432.306, 331.979004, 489.425995, 352.92099], - field_instructions: 'date naturalization granted', - struct_parent: 28, - name: 0, - field_type: '/Tx', - font_info: '', - field_label: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Naturalization Date_af_date/0', - struct_parent: 28, - }, - { - type: '/Tx', - var_name: 'Residency Date_af_date', - field_dict: { - field_type: '/Tx', - coordinates: [414.304993, 329.523987, 471.424988, 308.582001], - field_label: 'Residency Date_af_date', - field_instructions: 'Date Residency Granted (mm/dd/yyyy)', - struct_parent: 30, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Residency Date_af_date', - struct_parent: 30, - }, - { - type: '/Tx', - var_name: 'A-Number', - field_dict: { - field_type: '/Tx', - coordinates: [296.279999, 257.76001, 507.959991, 280.440002], - field_label: 'A-Number', - field_instructions: - 'Alien Registration, Naturalization, or Citizenship Number', - struct_parent: 31, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'A-Number', - struct_parent: 31, - }, - { - type: '/Tx', - var_name: 'Convict-Date_af_date', - field_dict: { - field_type: '/Tx', - coordinates: [203.602005, 218.822006, 301.363007, 245.341995], - field_label: 'Convict-Date_af_date', - field_instructions: 'Convict Date', - struct_parent: 32, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Convict-Date_af_date', - struct_parent: 32, - }, - { - type: '/Tx', - var_name: 'US District Court', - field_dict: { - field_type: '/Tx', - coordinates: [451.200012, 219.0, 522.719971, 241.679993], - field_label: 'US District Court', - field_instructions: 'US District Court', - struct_parent: 33, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'US District Court', - struct_parent: 33, - }, - { - type: '/Tx', - var_name: 'Dist State', - field_dict: { - field_type: '/Tx', - coordinates: [105.720001, 187.919998, 177.240005, 210.600006], - field_label: 'Dist State', - field_instructions: 'State', - struct_parent: 34, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Dist State', - struct_parent: 34, - }, - { - type: '/Tx', - var_name: 'Docket No', - field_dict: { - field_type: '/Tx', - coordinates: [114.015999, 153.479996, 262.575989, 176.160004], - field_label: 'Docket No', - field_instructions: 'Docket Number', - struct_parent: 36, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Docket No', - struct_parent: 36, - }, - { - type: '/Tx', - var_name: 'Code Section', - field_dict: { - field_type: '/Tx', - coordinates: [349.320007, 153.479996, 448.320007, 176.160004], - field_label: 'Code Section', - field_instructions: 'Code Section', - struct_parent: 37, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Code Section', - struct_parent: 37, - }, - { - type: '/Tx', - var_name: 'Code Section_2', - field_dict: { - field_type: '/Tx', - coordinates: [266.640015, 121.440002, 316.200012, 144.119995], - field_label: 'Code Section_2', - field_instructions: 'Code Section', - struct_parent: 38, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Code Section_2', - struct_parent: 38, - }, - { - type: '/Tx', - var_name: 'US District Court_2', - field_dict: { - field_type: '/Tx', - coordinates: [464.040009, 121.32, 542.039978, 144.0], - field_label: 'US District Court_2', - field_instructions: 'U.S. District Court', - struct_parent: 39, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'US District Court_2', - struct_parent: 39, - }, - { - type: '/Tx', - var_name: 'District 2', - field_dict: { - field_type: '/Tx', - coordinates: [105.720001, 86.760002, 188.160004, 109.440002], - field_label: 'District 2', - field_instructions: 'State', - struct_parent: 40, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'District 2', - struct_parent: 40, - }, - { - type: '/Tx', - var_name: 'Docket No 2', - field_dict: { - field_type: '/Tx', - coordinates: [403.920013, 86.760002, 525.0, 109.440002], - field_label: 'Docket No 2', - field_instructions: 'Docket No 2', - struct_parent: 42, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Docket No 2', - struct_parent: 42, - }, - ], - '3': [ - { - type: '/Tx', - var_name: 'App Date', - field_dict: { - field_type: '/Tx', - coordinates: [75.120003, 396.720001, 219.479996, 425.519989], - field_label: 'App Date', - field_instructions: 'Date', - struct_parent: 44, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 3, - path: 'App Date', - struct_parent: 44, - }, - ], - }, - grouped_items: [], - raw_fields_pages: { - '0': "OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nOn October 6, 2022, President Biden issued a presidential proclamation that pardoned many federal and D.C. \noffenses for simple marijuana possession. On December 22, 2023, President Biden issued another proclamation that \nexpanded the relief provided by the original proclamation by pardoning the federal offenses of simple possession, \nattempted possession, and use of marijuana. \nHow a pardon can help you \nA pardon is an expression of the President\u2019s forgiveness. It does not mean you are innocent or expunge your \nconviction. But it does remove civil disabilities\u2014such as restrictions on the right to vote, to hold office, or to sit \non a jury\u2014that are imposed because of the pardoned conviction. It may also be helpful in obtaining licenses, \nbonding, or employment. Learn more about the pardon. \nYou qualify for the pardon if: \n\u2022 On or before December 22, 2023, you were charged with or convicted of simple possession, attempted \npossession, or use of marijuana under the federal code, the District of Columbia code, or the Code of \nFederal Regulations \n\u2022 You were a U.S. citizen or lawfully present in the United States at the time of the offense \n\u2022 You were a U.S. citizen or lawful permanent resident on December 22, 2023 \nRequest a certificate to show proof of the pardon \nA Certificate of Pardon is proof that you were pardoned under the proclamation. The certificate is the only \ndocumentation you will receive of the pardon. Use the application below to start your request. \nWhat you'll need for the request \nAbout you \nYou can submit a request for yourself or someone else can submit on your behalf. You must provide \npersonal details, like name or citizenship status and either a mailing address, an email address or both to \ncontact you. We strongly recommend including an email address, if available, as we may not be able to \nrespond as quickly if you do not provide it. You can also use the mailing address or email address of \nanother person, if you do not have your own. \nAbout the charge or conviction \nYou must state whether it was a charge or conviction, the court district where it happened, and the date \n(month, day, year). If possible, you should also: \n\u2022 enter information about your case (docket or case number and the code section that was \ncharged) \n\u2022 upload your documents \no charging documents, like the indictment, complaint, criminal information, ticket or \ncitation; or \no conviction documents, like the judgment of conviction, the court docket sheet showing \nthe sentence and date it was imposed, or if you did not go to court, the receipt showing \npayment of fine \nIf you were charged by a ticket or citation and paid a fine instead of appearing in court, you should also provide the \ndate of conviction or the date the fine was paid. \nWithout this information, we can't guarantee that we'll be able to determine if you qualify for the pardon under \nthe proclamation. \n \nPage 1 of 4 \nUnited States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024 ", - '1': "OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nInstructions: \nAn online version of this application is available at: Presidential Proclamation on Marijuana Possession \n(justice.gov). You can also complete and return this application with the required documents to \nUSPardon.Attorney@usdoj.gov or U.S. Department of Justice, Office of the Pardon Attorney, 950 Pennsylvania \nAvenue NW, Washington, DC 20530. \nPublic Burden Statement: \nThis collection meets the requirements of 44 U.S.C. \u00a7 3507, as amended by the Paperwork Reduction Act of 1995. \nWe estimate that it will take 120 minutes to read the instructions, gather the relevant materials, and answer \nquestions on the form. Send comments regarding the burden estimate or any other aspect of this collection of \ninformation, including suggestions for reducing this burden, to Office of the Pardon Attorney, U.S. Department of \nJustice, Attn: OMB Number 1123-0014, RFK Building, 950 Pennsylvania Avenue, N.W., Washington DC 20530. \nThe OMB Clearance number, 1123-0014, is currently valid. \nPrivacy Act Statement: \nThe Office of the Pardon Attorney has authority to collect this information under the U.S. Constitution, Article \nII, Section 2 (the pardon clause); Orders of the Attorney General Nos. 1798-93, 58 Fed. Reg. 53658 and 53659 \n(1993), 2317-2000, 65 Fed. Reg. 48381 (2000), and 2323-2000, 65 Fed. Reg. 58223 and 58224 (2000), codified in \n28 C.F.R. \u00a7\u00a7 1.1 et seq. (the rules governing petitions for executive clemency); and Order of the Attorney General \nNo. 1012-83, 48 Fed. Reg. 22290 (1983), as codified in 28 C.F.R. \u00a7\u00a7 0.35 and 0.36 (the authority of the Office of \nthe Pardon Attorney). The principal purpose for collecting this information is to enable the Office of the Pardon \nAttorney to issue an individual certificate of pardon to you. The routine uses which may be made of this \ninformation include provision of data to the President and his staff, other governmental entities, and the public. \nThe full list of routine uses for this correspondence can be found in the System of Records Notice titled, \u201cPrivacy \nAct of 1974; System of Records,\u201d published in Federal Register, September 15, 2011, Vol. 76, No. 179, at pages \n57078 through 57080; as amended by \u201cPrivacy Act of 1974; System of Records,\u201d published in the Federal \nRegister, May 25, 2017, Vol. 82, No. 100, at page 24161, and at the U.S. Department of Justice, Office of Privacy \nand Civil Liberties' website at: https://www.justice.gov/opcl/doj-systems-records#OPA. \nBy signing the attached form, you consent to allowing the Office of the Pardon Attorney to obtain information \nregarding your citizenship and/or immigration status from the courts, from other government agencies, from other \ncomponents within the Department of Justice, and from the Department of Homeland Security, U.S. Citizenship \nand Immigration Services (DHS-USCIS), Systematic Alien Verification for Entitlements (SAVE) program. The \ninformation received from these sources will be used for the sole purposes of determining an applicant's \nqualification for a Certificate of Pardon under the December 22 proclamation and for record-keeping of those \ndeterminations. Further, please be aware that if the Office of the Pardon Attorney is unable to verify your \ncitizenship or immigration status based on the information provided below, we may contact you to obtain \nadditional verification information. Learn more about the DHS-USCIS's SAVE program and its ordinary uses. \nYour disclosure of information to the Office of the Pardon Attorney on this form is voluntary. If you do not \ncomplete all or some of the information fields in this form, however, the Office of the Pardon Attorney may not be \nable to effectively respond. Information regarding gender, race, or ethnicity is not required and will not affect the \nprocessing of the application. \nNote: Submit a separate form for each conviction or charge for which you are seeking a certificate of pardon. \nApplication Form on page 3. \nPage 2 of 4 \nUnited States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024 ", - '2': 'OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nComplete the following: \nName: \n(first) (middle) (last) \nName at Conviction: \n(if different) (first) (middle) (last) \nAddress: \n(number) (street) (apartment/unit no.) \n\n(city) (state) (Zip Code) \nEmail Address: Phone Number: \nDate of Birth: Gender: Are you Hispanic or Latino?: Yes No \nRace: Alaska Native or American Indian Asian Black or African American \nNative Hawaiian or Other Pacific Islander White Other \nCitizenship or Residency Status: \nU.S. citizen by birth \nU.S. naturalized citizen Date Naturalization Granted: \nLawful Permanent Resident Date Residency Granted: \nAlien Registration Number (A-Number), Certificate of Naturalization Number, or Citizenship Number \n(if applicant is a lawful permanent resident or naturalized citizen): \n(A-Number) \n1. Applicant was convicted on: in the U.S. District Court for the \n(month/day/year) (Northern, etc.) \nDistrict of (state) or D.C. Superior Court of simple possession of marijuana, under \nDocket No. : and Code Section: ; OR \n(docket number) (code section) \n2. Applicant was charged with Code Section: in the U.S. District Court for the \n(code section) (Eastern, etc.) \nDistrict of or D.C. Superior Court under Docket No: \n(state) (docket number) \n \nUnited States Department of Justice Office of the Pardon Attorney Page 3 of 4 Washington, D.C. 20530 January 2024 ', - '3': 'OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nWith knowledge of the penalties for false statements to Federal Agencies, as provided by 18 \nU.S.C. \u00a7 1001, and with knowledge that this statement is submitted by me to affect action by \nthe U.S. Department of Justice, I certify that: \n1. The applicant was either a U.S. citizen or lawfully present in the United States at the time of the \noffense. \n2. The applicant was a U.S. citizen or lawful permanent resident on December 22, 2023. \n3. The above statements, and accompanying documents, are true and complete to the \n best of my knowledge, information, and belief. \n4. I acknowledge that any certificate issued in reliance on the above information will be \nvoided, if the information is subsequently determined to be false. \n\n(date) (signature) \nPage 4 of 4 \nUnited States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024 ', - }, - }, - cache_id: 'Cache ID is not implemented yet', -}; diff --git a/packages/forms/src/documents/document.ts b/packages/forms/src/documents/document.ts index ed84341e..bd5d46fb 100644 --- a/packages/forms/src/documents/document.ts +++ b/packages/forms/src/documents/document.ts @@ -8,15 +8,13 @@ import { type Pattern } from '../pattern.js'; import { type InputPattern } from '../patterns/input/config.js'; import { type SequencePattern } from '../patterns/sequence.js'; import { type Blueprint } from '../types.js'; +import { defaultFormConfig } from '../patterns/index.js'; import { getDocumentFieldData } from './pdf/extract.js'; import { type PDFDocument } from './pdf/index.js'; -import { - type FetchPdfApiResponse, - type ParsedPdf, - fetchPdfApiResponse, - processApiResponse, -} from './pdf/parsing-api.js'; +import { type ParsedPdf } from './pdf/domain/pattern-mapper.js'; +import { type PdfParser } from './pdf/services/parser-interface.js'; +import { parsePdfToPatterns } from './pdf/services/parse-pdf-to-patterns.js'; import { type DocumentFieldMap } from './types.js'; @@ -55,14 +53,39 @@ export const addDocument = async ( data: Uint8Array; }, context: { - fetchPdfApiResponse: FetchPdfApiResponse; - } = { fetchPdfApiResponse } + parser?: PdfParser; + } = {} ) => { const fields = await getDocumentFieldData(fileDetails.data); - const json = await context.fetchPdfApiResponse(fileDetails.data); - const parsedPdf = await processApiResponse(json); - if (parsedPdf) { + // Skip parsing if no parser provided (fallback to simple field extraction) + if (!context.parser) { + const formWithFields = addDocumentFieldsToForm(form, fields); + const updatedForm = addFormOutput(formWithFields, { + id: 'document-1', // TODO: generate a unique ID + path: fileDetails.name, + fields, + formFields: Object.fromEntries( + Object.keys(fields).map(field => { + return [field, fields[field].name]; + }) + ), + }); + return { + newFields: fields, + updatedForm, + }; + } + + // Parse PDF to patterns using injected parser + const result = await parsePdfToPatterns( + { parser: context.parser, formConfig: defaultFormConfig }, + fileDetails.data + ); + + if (result.success) { + const parsedPdf = result.data; + form = updateFormSummary(form, { title: parsedPdf.title || fileDetails.name, description: parsedPdf.description, @@ -84,6 +107,7 @@ export const addDocument = async ( errors: parsedPdf.errors, }; } else { + // Fallback: parsing failed, use simple field extraction const formWithFields = addDocumentFieldsToForm(form, fields); const updatedForm = addFormOutput(formWithFields, { id: 'document-1', // TODO: generate a unique ID diff --git a/packages/forms/src/documents/pdf/adapters/bedrock-parser.ts b/packages/forms/src/documents/pdf/adapters/bedrock-parser.ts new file mode 100644 index 00000000..ee391381 --- /dev/null +++ b/packages/forms/src/documents/pdf/adapters/bedrock-parser.ts @@ -0,0 +1,264 @@ +import { success, failure, type Result } from '@flexion/forms-common'; +import type { PdfParser } from '../services/parser-interface.js'; +import type { FieldMetadata, ParseError } from '../domain/types.js'; +import { ExtractedFormSchema, type ExtractedForm } from '../domain/schema.js'; +import type { LlmContext } from '../../../llm/services/context.js'; +import { generateObjectCached } from '../../../llm/services/generate-object.js'; +import { + createBedrockModel, + type BedrockConfig, +} from '../../../llm/providers/bedrock.js'; + +/** + * System prompt for the LLM + */ +const SYSTEM_PROMPT = `You are a forms architecture expert specializing in guided interviews. + +Your task is to convert fillable PDF forms into multi-page guided interview structures that provide a better user experience through progressive disclosure and clear organization. + +IMPORTANT: Focus on producing the structured output efficiently. Keep descriptions concise and avoid verbose summaries. + +Guided Interview Design Principles: + +1. PAGE STRUCTURE + - Organize content into logical pages (aim for 5-8 fields per page) + - Give each page a SHORT, DESCRIPTIVE title in plain language + - Page titles should be suitable for navigation (e.g., "Personal Information", "Contact Details", "Employment History") + - Page 0: Introduction with form summary and high-level instructions + - Pages 1-N: Logical sections of the form + - Final page: Declarations, signatures, submission info + +2. PROGRESSIVE DISCLOSURE + - Break long forms into multiple pages + - Group related information together within pages + - Order pages logically (personal info → specific details → review) + +3. CLEAR CONTEXT + - Use 'paragraph' for simple plain text instructions + - Use 'rich_text' for formatted content (headings, lists, emphasis) + - IMPORTANT: rich_text must use HTML format (h2, h3, p, ul, li, strong, etc.) + - Place context BEFORE the related fields + - Use fieldset legends to label grouped fields + +4. USER-FRIENDLY LABELS + - Convert technical field names to plain language + - "Fst Name 1" → "First Name" + - Add helpful hints where appropriate + +5. LOGICAL GROUPING + - Use fieldsets for related fields (e.g., name parts, address components) + - Group by topic, not just by PDF page + +6. CHECKBOX GROUPS + - Use 'checkbox_group' for semantically related checkboxes + - Examples: race options, interests, preferences, multi-select categories + - Each checkbox option 'id' must be the EXACT field ID from PDF metadata + - The group 'legend' should describe what the checkboxes represent + - Individual 'checkbox' component is for standalone checkboxes only + +7. RADIO GROUPS + - CRITICAL: Radio group 'id' must be the EXACT field name from metadata + - Do NOT simplify, clean, or modify the radio group ID + - Example: If metadata shows "Ethnicity.undefined", use "Ethnicity.undefined" exactly + - Radio option 'id' should follow pattern: {groupId}.{index} + - Use numeric indices starting from 0 (e.g., "Ethnicity.0", "Ethnicity.1") + - Radio option 'name' field must match the group 'id' exactly + - The 'legend' field should be user-friendly, but 'id' must match metadata + +8. FIELD ID PRESERVATION + - CRITICAL: Use exact field IDs from the metadata for all input fields + - The 'id' field must match the PDF field name exactly (including suffixes like .undefined) + - Only the 'label' should be user-friendly`; + +/** + * Builds the user prompt with field metadata + */ +const buildPrompt = (fieldMetadata: FieldMetadata[]): string => { + return `I'm providing: +1. A fillable PDF document (attached) +2. Metadata about PDF form fields (JSON below) + +Your task: Create a guided interview structure following the schema. + +CRITICAL REQUIREMENTS: +- Include EVERY field from the metadata - do not omit any fields +- Use the EXACT field IDs from the metadata. Do not modify, clean, or simplify them. +- For radio groups: The group 'id' must match the RadioGroup field name exactly (e.g., "Ethnicity.undefined") +- For radio options: Use numeric indices (e.g., "Ethnicity.0", "Ethnicity.1", not "Ethnicity.yes") +- For checkbox groups: Each option 'id' must match the individual CheckBox field name exactly +- Use plain language for all labels and legends. +- Be concise in all text fields (form summary, instructions, labels) + +Field Metadata: +${JSON.stringify(fieldMetadata, null, 2)} + +Please analyze the PDF and field metadata to create a well-organized guided interview structure that follows the design principles outlined in the system prompt.`; +}; + +/** + * Extracts all field IDs from the parsed output by traversing the nested structure + */ +const extractFieldIds = (output: ExtractedForm): Set => { + const ids = new Set(); + + const processElement = ( + element: (typeof output.pages)[0]['elements'][0] + ): void => { + switch (element.component_type) { + case 'text_input': + case 'checkbox': + ids.add(element.id); + break; + case 'checkbox_group': + element.options.forEach(opt => ids.add(opt.id)); + break; + case 'radio_group': + ids.add(element.id); + break; + case 'fieldset': + element.fields.forEach(field => ids.add(field.id)); + break; + // paragraph and rich_text have no field IDs + } + }; + + output.pages.forEach(page => { + page.elements.forEach(processElement); + }); + + return ids; +}; + +/** + * Finds fields from metadata that are missing in the parser output + */ +const findMissingFields = ( + metadata: FieldMetadata[], + output: ExtractedForm +): FieldMetadata[] => { + const outputIds = extractFieldIds(output); + return metadata.filter(field => !outputIds.has(field.id)); +}; + +/** + * Adds missing fields to a fallback page in the output to ensure completeness + */ +const addMissingFieldsToOutput = ( + output: ExtractedForm, + missingFields: FieldMetadata[] +): void => { + if (missingFields.length === 0) return; + + // Find or create "Additional Information" page + let additionalPage = output.pages.find( + p => p.title === 'Additional Information' + ); + + if (!additionalPage) { + additionalPage = { + title: 'Additional Information', + elements: [], + }; + output.pages.push(additionalPage); + } + + // Add context paragraph if this is a newly created page + if (additionalPage.elements.length === 0) { + additionalPage.elements.push({ + component_type: 'paragraph', + text: 'The following fields were not categorized in the form structure:', + }); + } + + // Add each missing field as a text input (safest default assumption) + missingFields.forEach(field => { + additionalPage!.elements.push({ + component_type: 'text_input', + id: field.id, + label: field.label || field.id, + required: false, + }); + }); +}; + +/** + * PDF parser using AWS Bedrock (Claude) via AI SDK. + * Uses LLM context for automatic caching and provider management. + */ +export class BedrockParser implements PdfParser { + private readonly model: ReturnType; + private readonly llmContext: LlmContext; + + constructor(llmContext: LlmContext, config: Partial = {}) { + this.llmContext = llmContext; + this.model = createBedrockModel(config); + } + + async parse( + pdfBytes: Uint8Array, + metadata: FieldMetadata[] + ): Promise> { + try { + const prompt = buildPrompt(metadata); + + // Use cached generateObject from llm services + const result = await generateObjectCached(this.llmContext, { + model: this.model, + schema: ExtractedFormSchema, + schemaName: 'GuidedInterviewForm', + schemaDescription: + 'A structured guided interview form with multiple pages and organized elements', + system: SYSTEM_PROMPT, + messages: [ + { + role: 'user', + content: [ + { + type: 'file', + data: pdfBytes, + mediaType: 'application/pdf', + }, + { + type: 'text', + text: prompt, + }, + ], + }, + ], + }); + + // Extract and type the parsed object (schema ensures correct type) + const parsedForm = result.object as ExtractedForm; + + // Validate that all fields from metadata are present in the output + const missingFields = findMissingFields(metadata, parsedForm); + if (missingFields.length > 0) { + console.warn( + `Parser omitted ${missingFields.length} field(s): ${missingFields.map(f => f.id).join(', ')}` + ); + addMissingFieldsToOutput(parsedForm, missingFields); + } + + return success(parsedForm); + } catch (error) { + console.error('Bedrock API error:', error); + const parseError: ParseError = { + code: 'PARSER_ERROR', + message: 'Failed to parse PDF with Bedrock', + details: error, + }; + return failure(parseError); + } + } +} + +/** + * Factory function to create BedrockParser with default configuration. + * @deprecated Use `new BedrockParser(llmContext)` directly instead. + */ +export const createBedrockParser = ( + llmContext: LlmContext, + config?: Partial +): BedrockParser => { + return new BedrockParser(llmContext, config); +}; diff --git a/packages/forms/src/documents/pdf/adapters/fake-parser.ts b/packages/forms/src/documents/pdf/adapters/fake-parser.ts new file mode 100644 index 00000000..f16fbee1 --- /dev/null +++ b/packages/forms/src/documents/pdf/adapters/fake-parser.ts @@ -0,0 +1,47 @@ +import { success, type Result } from '@flexion/forms-common'; +import type { PdfParser } from '../services/parser-interface.js'; +import type { FieldMetadata, ParseError } from '../domain/types.js'; +import type { ExtractedForm } from '../domain/schema.js'; + +/** + * Fake PDF parser for testing. + * Returns a pre-configured response without making any external calls. + */ +export class FakePdfParser implements PdfParser { + private readonly mockResponse: ExtractedForm; + + constructor(mockResponse: ExtractedForm) { + this.mockResponse = mockResponse; + } + + async parse( + _pdfBytes: Uint8Array, + _metadata: FieldMetadata[] + ): Promise> { + // Simulate async operation + return Promise.resolve(success(this.mockResponse)); + } +} + +/** + * Factory function to create a simple fake parser with minimal data + */ +export const createSimpleFakeParser = (): FakePdfParser => { + return new FakePdfParser({ + form_summary: { + title: 'Test Form', + description: 'Test form description', + }, + pages: [ + { + title: 'Page 1', + elements: [ + { + component_type: 'paragraph', + text: 'Welcome to the test form', + }, + ], + }, + ], + }); +}; diff --git a/packages/forms/src/documents/pdf/adapters/pdf-lib-fields.ts b/packages/forms/src/documents/pdf/adapters/pdf-lib-fields.ts new file mode 100644 index 00000000..bfe116a3 --- /dev/null +++ b/packages/forms/src/documents/pdf/adapters/pdf-lib-fields.ts @@ -0,0 +1,216 @@ +import { + PDFField, + PDFTextField, + PDFCheckBox, + PDFDropdown, + PDFOptionList, + PDFRadioGroup, + PDFForm, + PDFName, + createPDFAcroFields, +} from 'pdf-lib'; + +import type { DocumentFieldValue } from '../../types.js'; +import type { PDFFieldType } from '../index.js'; + +/** + * Handler for a specific PDF field type. + * Groups extraction and filling logic together for related field types. + */ +interface FieldHandler { + type: PDFFieldType; + isInstance(field: PDFField): field is T; + extract(field: T): DocumentFieldValue; + fill(form: PDFForm, name: string, value: any): void; +} + +const textFieldHandler: FieldHandler = { + type: 'TextField', + + isInstance(field: PDFField): field is PDFTextField { + return field instanceof PDFTextField; + }, + + extract(field: PDFTextField): DocumentFieldValue { + return { + type: 'TextField', + name: field.getName(), + label: field.getName(), + value: field.getText() || '', + maxLength: field.getMaxLength(), + required: field.isRequired(), + }; + }, + + fill(form: PDFForm, name: string, value: string) { + const field = form.getTextField(name); + field.setText(value); + }, +}; + +const checkBoxHandler: FieldHandler = { + type: 'CheckBox', + + isInstance(field: PDFField): field is PDFCheckBox { + return field instanceof PDFCheckBox; + }, + + extract(field: PDFCheckBox): DocumentFieldValue { + return { + type: 'CheckBox', + name: field.getName(), + label: field.getName(), + value: field.isChecked(), + required: field.isRequired(), + }; + }, + + fill(form: PDFForm, name: string, value: boolean) { + const field = form.getCheckBox(name); + if (value) { + field.check(); + } else { + field.uncheck(); + } + }, +}; + +const dropdownHandler: FieldHandler = { + type: 'Dropdown', + + isInstance(field: PDFField): field is PDFDropdown { + return field instanceof PDFDropdown; + }, + + extract(field: PDFDropdown): DocumentFieldValue { + return { + type: 'Dropdown', + name: field.getName(), + label: field.getName(), + value: field.getSelected(), + required: field.isRequired(), + }; + }, + + fill(form: PDFForm, name: string, value: string) { + const field = form.getDropdown(name); + field.select(value); + }, +}; + +const optionListHandler: FieldHandler = { + type: 'OptionList', + + isInstance(field: PDFField): field is PDFOptionList { + return field instanceof PDFOptionList; + }, + + extract(field: PDFOptionList): DocumentFieldValue { + return { + type: 'OptionList', + name: field.getName(), + label: field.getName(), + value: field.getSelected(), + required: field.isRequired(), + }; + }, + + fill(form: PDFForm, name: string, value: string) { + const field = form.getDropdown(name); + field.select(value); + }, +}; + +const radioGroupHandler: FieldHandler = { + type: 'RadioGroup', + + isInstance(field: PDFField): field is PDFRadioGroup { + return field instanceof PDFRadioGroup; + }, + + extract(field: PDFRadioGroup): DocumentFieldValue { + return { + type: 'RadioGroup', + name: field.getName(), + options: field.getOptions(), + label: field.getName(), + value: field.getSelected() || '', // pdfLib allows this to be undefined + required: field.isRequired(), + }; + }, + + fill(form: PDFForm, name: string, value: string) { + // TODO: harmonize the option ids between pdf-lib and the API at ingestion time + try { + const field = form.getRadioGroup(name); + field.select(value); + } catch (error: any) { + // This logic should work even if pdf-lib misidentifies the field type + // TODO: radioParent should contain the name, not the id + const [radioParent, radioChild] = value.split('.'); + if (radioChild) { + // TODO: resolve import failure when spaces are present in name, id + const radioChildWithSpace = radioChild.replace('_', ' '); + const field = form.getField(name); + const acroField = field.acroField; + acroField.dict.set(PDFName.of('V'), PDFName.of(radioChildWithSpace)); + const kids = createPDFAcroFields(acroField.Kids()).map(_ => _[0]); + kids.forEach(kid => { + kid.dict.set(PDFName.of('AS'), PDFName.of(radioChildWithSpace)); + }); + } + } + }, +}; + +const FIELD_HANDLERS: ReadonlyArray> = [ + textFieldHandler, + checkBoxHandler, + dropdownHandler, + optionListHandler, + radioGroupHandler, +] as const; + +/** + * Extracts field data from a PDF field. + * Adapts pdf-lib fields to our internal DocumentFieldValue type. + */ +export const extractField = (field: PDFField): DocumentFieldValue => { + const handler = FIELD_HANDLERS.find(h => h.isInstance(field)); + return handler + ? handler.extract(field) + : { + type: 'not-supported', + name: field.getName(), + error: `unsupported type: ${field.constructor.name}`, + }; +}; + +/** + * Fills a PDF form field with a value. + * Adapts our internal values to pdf-lib form operations. + */ +export const fillField = ( + form: PDFForm, + fieldType: PDFFieldType, + name: string, + value: any +) => { + // Handle special cases that don't have handlers + if (fieldType === 'Paragraph' || fieldType === 'RichText') { + return; // do nothing + } + + if (fieldType === 'Attachment') { + // Attachment uses dropdown logic + const field = form.getDropdown(name); + field.select(value); + return; + } + + const handler = FIELD_HANDLERS.find(h => h.type === fieldType); + if (!handler) { + throw new Error(`Unknown field type: ${fieldType}`); + } + handler.fill(form, name, value); +}; diff --git a/packages/forms/src/documents/pdf/al_name_change.ts b/packages/forms/src/documents/pdf/al_name_change.ts deleted file mode 100644 index d243c2b3..00000000 --- a/packages/forms/src/documents/pdf/al_name_change.ts +++ /dev/null @@ -1,869 +0,0 @@ -export default { - raw_text: 'Request to Change Name', - title: 'Request to Change Name (For An Adult)', - description: '', - elements: [ - { - id: '1fc0b74a-7139-46a7-9aad-8e3e3ff94a6d', - group_id: 2, - element_type: 'text', - element_params: { - text: 'In the Probate Court of (county): ', - text_style: 'JTUTCJ+ArialMT 9.840000000000032', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'County_Name1', - placeholder: '', - instructions: 'Type name of Alabama county where you live', - required: false, - options: [], - }, - }, - ], - parent: null, - }, - { - id: '6904a3ef-17f6-4a1b-83c7-4a963f1a43c5', - group_id: 3, - element_type: 'text', - element_params: { - text: 'Your current name', - text_style: 'JTUTCJ+ArialMT 9.840000000000032', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Current_First_Name1', - placeholder: '', - instructions: 'Type your current first name', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Current_Middle_Name1', - placeholder: '', - instructions: 'Type your current middle name', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Current_Last_Name1', - placeholder: '', - instructions: 'Type your current last name', - required: false, - options: [], - }, - }, - ], - parent: null, - }, - { - id: '0a10d256-c938-4c83-9045-8f31df99bbff', - group_id: 5, - element_type: 'text', - element_params: { - text: 'To ask the court to change your name, you must fill out this form, and: ', - text_style: 'subheading', - options: null, - }, - inputs: [], - parent: null, - }, - { - id: '9ffecb48-b274-44a9-ab21-17f90db129ca', - group_id: 6, - element_type: 'text', - element_params: { - text: 'Attach a certified copy of your birth certificate and a copy of your photo ID, and ', - text_style: 'indent', - options: null, - }, - inputs: [], - parent: null, - }, - { - id: '21ce04d1-7d40-40c9-bdd5-e738b801a6d3', - group_id: 7, - element_type: 'text', - element_params: { - text: ' File your form and attachments in the same county where you live. ', - text_style: 'indent', - options: null, - }, - inputs: [], - parent: null, - }, - { - id: 'fd565668-436f-42f3-9969-4de1b86db4ae', - group_id: 8, - element_type: 'text', - element_params: { - text: 'I declare that the following information is true: ', - text_style: 'subheading', - options: null, - }, - inputs: [], - parent: null, - }, - { - id: '767b5b4e-f1f0-4ca2-a606-d74562d4b78b', - group_id: 9, - element_type: 'text', - element_params: { - text: 'My current name', - text_style: 'JTUTCJ+ArialMT 8.879999999999995', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Current_First_Name2', - placeholder: '', - instructions: 'Type your current first name', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Current_Middle_Name2', - placeholder: '', - instructions: 'Type your current middle name', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Current_Last_Name2', - placeholder: '', - instructions: 'Type your current last name', - required: false, - options: [], - }, - }, - ], - parent: null, - }, - { - id: '91955d9c-b3fc-4190-b1fd-bfddd49a2a6c', - group_id: 11, - element_type: 'text', - element_params: { - text: 'My address', - text_style: 'JTUTCJ+ArialMT 8.879999999999995', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Street_Address', - placeholder: '', - instructions: 'Type your street address', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'City', - placeholder: '', - instructions: 'Type the name of your city', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'State', - placeholder: '', - instructions: 'Type the state where you live', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Zip', - placeholder: '', - instructions: 'Type your zip code', - required: false, - options: [], - }, - }, - ], - parent: null, - }, - { - id: '7ce2801f-022d-4140-b004-fdcdaf9a0767', - group_id: 13, - element_type: 'text', - element_params: { - text: 'My phone numbers', - text_style: 'DURMBL+Arial-BoldMT 8.879999999999995', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Home_Phone', - placeholder: '', - instructions: 'Type your home phone number including area code', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Work_Phone', - placeholder: '', - instructions: 'Type your work phone number including area code', - required: false, - options: [], - }, - }, - ], - parent: null, - }, - { - id: 'c3212cab-6019-4e60-8198-f11b87c81e82', - group_id: 14, - element_type: 'text', - element_params: { - text: 'My date of birth is (mm/dd/yyyy): ', - text_style: 'JTUTCJ+ArialMT 8.879999999999995', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'DOB', - placeholder: '', - instructions: 'Type your date of birth as mm/dd/yyyy', - required: false, - options: [], - }, - }, - ], - parent: null, - }, - { - id: 'ce713b94-0008-43ab-ace4-6b38d01b2f76', - group_id: 15, - element_type: 'text', - element_params: { - text: 'My name at birth', - text_style: 'JTUTCJ+ArialMT 8.879999999999995', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Birth_First_Name', - placeholder: '', - instructions: 'Type the first name you were given at birth', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Birth_Middle_Name', - placeholder: '', - instructions: 'Type the middle name you were given at birth', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Birth_Last_Name', - placeholder: '', - instructions: 'Type the last name you were given at birth', - required: false, - options: [], - }, - }, - ], - parent: null, - }, - { - id: 'f228e971-d4db-43f6-a03b-4eb009f214b0', - group_id: 17, - element_type: 'text', - element_params: { - text: 'I am an adult (19 or older), of sound mind, and live in (name of Alabama county): ', - text_style: 'LEDWHU+TimesNewRomanPSMT 8.879999999999995', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'County_Name2', - placeholder: '', - instructions: 'Type the name of the county where you live now', - required: false, - options: [], - }, - }, - ], - parent: null, - }, - { - id: 'dcb5d901-a293-4f53-b663-a4d401af0812', - group_id: 18, - element_type: 'text', - element_params: { - text: 'The attached copy of my photo ID is my (choose one):', - text_style: 'heading', - options: null, - }, - inputs: [], - parent: 'PhotoID', - }, - { - id: 'dcb5d901-a293-4f53-b663-a4d401ac0812', - group_id: 18, - element_type: 'text', - element_params: { - text: 'Driver\u2019s license, # ', - text_style: 'JTUTCJ+ArialMT 12.0', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'DL#', - placeholder: '', - instructions: "Type your driver's license number", - required: false, - options: [], - }, - }, - ], - parent: 'PhotoID', - }, - { - id: '1a8cbd28-f600-47f8-9ce0-85ae136ba338', - group_id: 18, - element_type: 'text', - element_params: { - text: 'Non-driver\u2019s photo, ID #: ', - text_style: 'JTUTCJ+ArialMT 8.879999999999995', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'ID#', - placeholder: '', - instructions: - "Type the number from your photo ID (not driver's license)", - required: false, - options: [], - }, - }, - ], - parent: 'PhotoID', - }, - { - id: 'a532ebcf-b71a-4135-9de1-819bc98815b0', - group_id: 19, - element_type: 'text', - element_params: { - text: 'I ask the court to change my name because (explain why you want to change your name): ', - text_style: 'LEDWHU+TimesNewRomanPSMT 8.879999999999995', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'Why_change_name1', - placeholder: '', - instructions: 'Type why you want to change your name', - required: false, - options: [], - }, - }, - ], - parent: null, - }, - { - id: 'f21048d9-95f4-4e83-843b-cf8ae282f4ed', - group_id: 22, - element_type: 'text', - element_params: { - text: 'My new name', - text_style: 'JTUTCJ+ArialMT 8.879999999999995', - options: null, - }, - inputs: [ - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'New_First_Name', - placeholder: '', - instructions: 'Type what you want your new first name to be', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'New_Middle_Name', - placeholder: '', - instructions: 'Type what you want your new middle name to be', - required: false, - options: [], - }, - }, - { - input_type: 'Tx', - input_params: { - text: "", - text_style: 'Helv 10 Tf 0 g', - output_id: 'New_Last_Name', - placeholder: '', - instructions: 'Type what you want your new last name to be', - required: false, - options: [], - }, - }, - ], - parent: null, - }, - { - id: '9a3644e2-05f3-4dae-8c64-6abd788d0c67', - group_id: 24, - element_type: 'text', - element_params: { - text: 'I also declare: ', - text_style: 'subheading', - options: null, - }, - inputs: [], - parent: null, - }, - { - id: '14cdb97d-3024-43f4-932d-6b3f29bf2ffa', - group_id: 25, - element_type: 'text', - element_params: { - text: ' I am not now facing criminal charges, nor am I involved in any other court case. ', - text_style: 'indent', - options: null, - }, - inputs: [], - parent: null, - }, - { - id: '1a37c28e-c38b-4154-80c9-0ef8238731c6', - group_id: 26, - element_type: 'text', - element_params: { - text: ' I have never been convicted of a criminal sex offense (as defined in Alabama Code \u00a7 15-20-21), a crime of moral turpitude, or a felony. ', - text_style: 'indent', - options: null, - }, - inputs: [], - parent: null, - }, - { - id: 'c38a01c9-d076-4356-b732-276e06311503', - group_id: 28, - element_type: 'text', - element_params: { - text: ' I am not asking to change my name to avoid paying my debts or to commit fraud. ', - text_style: 'indent', - options: null, - }, - inputs: [], - parent: null, - }, - ], - raw_fields: [ - { - type: '/Tx', - var_name: 'Current_First_Name1', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [143.52, 660.72, 315.114, 674.88], - field_label: 'Current_First_Name1', - field_instructions: 'Type your current first name', - }, - }, - { - type: '/Tx', - var_name: 'Current_Middle_Name1', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [319.867, 660.368, 446.006, 674.629], - field_label: 'Current_Middle_Name1', - field_instructions: 'Type your current middle name', - }, - }, - { - type: '/Tx', - var_name: 'Current_Last_Name1', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [448.846, 661.013, 552.413, 674.629], - field_label: 'Current_Last_Name1', - field_instructions: 'Type your current last name', - }, - }, - { - type: '/Tx', - var_name: 'Current_First_Name2', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [175.92, 557.04, 330.412, 571.2], - field_label: 'Current_First_Name2', - field_instructions: 'Type your current first name', - }, - }, - { - type: '/Tx', - var_name: 'Current_Middle_Name2', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [335.345, 557.185, 456.324, 572.091], - field_label: 'Current_Middle_Name2', - field_instructions: 'Type your current middle name', - }, - }, - { - type: '/Tx', - var_name: 'Current_Last_Name2', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [459.809, 557.185, 555.638, 572.091], - field_label: 'Current_Last_Name2', - field_instructions: 'Type your current last name', - }, - }, - { - type: '/Tx', - var_name: 'Street_Address', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [148.8, 528.48, 284.625, 542.64], - field_label: 'Street_Address', - field_instructions: 'Type your street address', - }, - }, - { - type: '/Tx', - var_name: 'City', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [286.978, 527.52, 391.19, 542.426], - field_label: 'City', - field_instructions: 'Type the name of your city', - }, - }, - { - type: '/Tx', - var_name: 'State', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [392.74, 528.165, 462.129, 542.426], - field_label: 'State', - field_instructions: 'Type the state where you live', - }, - }, - { - type: '/Tx', - var_name: 'Zip', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [464.324, 527.52, 554.993, 543.071], - field_label: 'Zip', - field_instructions: 'Type your zip code', - }, - }, - { - type: '/Tx', - var_name: 'Home_Phone', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [224.4, 500.16, 359.16, 514.32], - field_label: 'Home_Phone', - field_instructions: 'Type your home phone number including area code', - }, - }, - { - type: '/Tx', - var_name: 'Work_Phone', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [392.88, 500.16, 555.48, 514.32], - field_label: 'Work_Phone', - field_instructions: 'Type your work phone number including area code', - }, - }, - { - type: '/Tx', - var_name: 'DOB', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [224.16, 480.48, 555.48, 494.64], - field_label: 'DOB', - field_instructions: 'Type your date of birth as mm/dd/yyyy', - }, - }, - { - type: '/Tx', - var_name: 'Birth_First_Name', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [182.64, 457.68, 320.739, 471.84], - field_label: 'Birth_First_Name', - field_instructions: 'Type the first name you were given at birth', - }, - }, - { - type: '/Tx', - var_name: 'Birth_Middle_Name', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [324.381, 457.872, 445.361, 472.133], - field_label: 'Birth_Middle_Name', - field_instructions: 'Type the middle name you were given at birth', - }, - }, - { - type: '/Tx', - var_name: 'Birth_Last_Name', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [446.911, 457.872, 554.993, 472.133], - field_label: 'Birth_Last_Name', - field_instructions: 'Type the last name you were given at birth', - }, - }, - { - type: '/Tx', - var_name: 'County_Name1', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [221.76, 685.275, 413.28, 700.08], - field_label: 'County_Name1', - field_instructions: - 'Type name of Alabama county where Probate Court is located', - }, - }, - { - type: '/Tx', - var_name: 'County_Name2', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [434.16, 428.64, 555.48, 442.8], - field_label: 'County_Name2', - field_instructions: 'Type the name of the county where you live now', - }, - }, - { - type: '/Tx', - var_name: 'DL#', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [410.64, 406.08, 555.48, 420.24], - field_label: 'DL#', - field_instructions: "Type your driver's license number", - }, - }, - { - type: '/Btn', - var_name: 'PhotoID', - field_dict: { - font_info: '/ZaDb 10 Tf 0 g', - flags: 49152, - field_type: 'button_field', - field_label: 'PhotoID', - child_fields: [ - { - coordinates: [320.512, 405.766, 330.773, 416.027], - }, - { - coordinates: [319.867, 391.578, 330.773, 403.129], - }, - ], - num_children: 2, - }, - }, - { - type: '/Tx', - var_name: 'ID#', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [436.08, 393.6, 555.48, 405.885], - field_label: 'ID#', - field_instructions: - "Type the number from your photo ID (not driver's license)", - }, - }, - { - type: '/Tx', - var_name: 'Why_change_name1', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [466.32, 375.36, 555.48, 389.52], - field_label: 'Why_change_name1', - field_instructions: 'Type why you want to change your name', - }, - }, - { - type: '/Tx', - var_name: 'Why_change_name2', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [77.52, 360.48, 555.48, 374.64], - field_label: 'Why_change_name2', - field_instructions: 'Continue typing why you want to change your name', - }, - }, - { - type: '/Tx', - var_name: 'Why_change_name3', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [77.52, 346.56, 555.48, 359.64], - field_label: 'Why_change_name3', - field_instructions: 'Continue typing why you want to change your name', - }, - }, - { - type: '/Tx', - var_name: 'New_First_Name', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [207.84, 326.4, 333.637, 340.56], - field_label: 'New_First_Name', - field_instructions: 'Type what you want your new first name to be', - }, - }, - { - type: '/Tx', - var_name: 'New_Middle_Name', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [335.989, 326.313, 449.876, 340.575], - field_label: 'New_Middle_Name', - field_instructions: 'Type what you want your new middle name to be', - }, - }, - { - type: '/Tx', - var_name: 'New_Last_Name', - field_dict: { - font_info: '/Helv 10 Tf 0 g', - field_type: 'text_field', - coordinates: [450.781, 325.668, 564.021, 340.575], - field_label: 'New_Last_Name', - field_instructions: 'Type what you want your new last name to be', - }, - }, - ], -}; diff --git a/packages/forms/src/documents/pdf/context.ts b/packages/forms/src/documents/pdf/context.ts new file mode 100644 index 00000000..b1bad74f --- /dev/null +++ b/packages/forms/src/documents/pdf/context.ts @@ -0,0 +1,73 @@ +import type { DatabaseContext } from '@flexion/forms-database'; +import type { PdfParser } from './services/parser-interface.js'; +import { createBedrockParser } from './adapters/bedrock-parser.js'; +import { + createProductionLlmContext, + createTestLlmContext, + createNoopLlmContext, +} from '../../llm/services/context.js'; + +/** + * Creates a production PDF parser with database-backed LLM caching. + * Use this in production applications and server-side code. + * + * @param db - Database context for persistent LLM response caching + * @returns PdfParser configured for production use + * + * @example + * ```typescript + * const db = await createPostgresDatabaseContext(config); + * const parser = createProductionPdfParser(db); + * ``` + */ +export const createProductionPdfParser = (db: DatabaseContext): PdfParser => { + const llmContext = createProductionLlmContext(db); + return createBedrockParser(llmContext); +}; + +/** + * Creates a test PDF parser with filesystem-backed LLM caching (VCR pattern). + * Enables "record once, replay forever" testing workflow. + * + * By default, uses a shared cache directory at the workspace root to ensure + * all tests and CLI tools can share cached Bedrock responses. + * + * @param cachePath - Directory path for storing cached LLM responses (defaults to workspace root) + * @returns PdfParser configured for testing + * + * @example + * ```typescript + * const parser = createTestPdfParser(); + * // First run: records live Bedrock API response to workspace root cache + * // Subsequent runs: replays from shared cache, no API calls + * ``` + */ +export const createTestPdfParser = (cachePath?: string): PdfParser => { + const llmContext = createTestLlmContext(cachePath); + return createBedrockParser(llmContext); +}; + +/** + * Creates a PDF parser with no LLM caching. + * Every parse call will make a live API request to Bedrock. + * + * Use cases: + * - Development when you want fresh results every time + * - Debugging caching issues + * - Scenarios where caching is explicitly undesirable + * + * WARNING: This will make real API calls and incur costs on every invocation. + * For most use cases, prefer createProductionPdfParser or createTestPdfParser. + * + * @returns PdfParser with no-op cache + * + * @example + * ```typescript + * const parser = createNoopPdfParser(); + * // Every call hits Bedrock API (no caching) + * ``` + */ +export const createNoopPdfParser = (): PdfParser => { + const llmContext = createNoopLlmContext(); + return createBedrockParser(llmContext); +}; diff --git a/packages/forms/src/documents/pdf/domain/field-extractor.ts b/packages/forms/src/documents/pdf/domain/field-extractor.ts new file mode 100644 index 00000000..dc0329d1 --- /dev/null +++ b/packages/forms/src/documents/pdf/domain/field-extractor.ts @@ -0,0 +1,46 @@ +import { success, failure } from '@flexion/forms-common'; +import { getDocumentFieldData } from '../extract.js'; +import type { + FieldMetadata, + ExtractFieldMetadataResult, + ParseError, +} from './types.js'; + +/** + * Extracts field metadata from PDF bytes. + * This is a domain function that transforms raw PDF field data into + * a format suitable for the parsing workflow. + * + * @param pdfBytes - Raw PDF file bytes + * @returns Result containing array of field metadata or error + */ +export const extractFieldMetadata = async ( + pdfBytes: Uint8Array +): Promise => { + try { + const documentFields = await getDocumentFieldData(pdfBytes); + + const metadata: FieldMetadata[] = []; + for (const field of Object.values(documentFields)) { + // Skip unsupported field types + if (field.type !== 'not-supported') { + metadata.push({ + id: field.name, + type: field.type, + label: field.label, + instructions: '', + page: 0, + }); + } + } + + return success(metadata); + } catch (error) { + const parseError: ParseError = { + code: 'FIELD_EXTRACTION_ERROR', + message: 'Failed to extract field metadata from PDF', + details: error, + }; + return failure(parseError); + } +}; diff --git a/packages/forms/src/documents/pdf/domain/pattern-mapper.ts b/packages/forms/src/documents/pdf/domain/pattern-mapper.ts new file mode 100644 index 00000000..1822df58 --- /dev/null +++ b/packages/forms/src/documents/pdf/domain/pattern-mapper.ts @@ -0,0 +1,243 @@ +import { success, failure, type Result } from '@flexion/forms-common'; +import { PagePattern } from '../../../patterns/page/config.js'; +import { PageSetPattern } from '../../../patterns/page-set/config.js'; +import { type DocumentFieldMap } from '../../types.js'; +import { + createPattern, + FormConfig, + Pattern, + PatternId, + PatternMap, +} from '../../../pattern.js'; +import { FormErrors } from '../../../error.js'; +import type { ParseError } from './types.js'; +import type { ExtractedForm } from './schema.js'; +import type { MappingContext } from '../patterns/types.js'; +import { + inputPatternHandler, + checkboxPatternHandler, + checkboxGroupPatternHandler, + radioGroupPatternHandler, + paragraphPatternHandler, + richTextPatternHandler, + fieldsetPatternHandler, +} from '../patterns/index.js'; + +/** + * Result type for parsed PDF with patterns + */ +export type ParsedPdf = { + patterns: PatternMap; + errors: { + type: Pattern['type']; + data: Pattern['data']; + errors: FormErrors; + }[]; + outputs: DocumentFieldMap; + root: PatternId; + title: string; + description: string; +}; + +/** + * Maps an ExtractedForm to internal pattern representation. + * This is a pure domain function with no external dependencies. + * + * @param config - Form configuration (pattern definitions) + * @param extracted - Parsed form structure from LLM parser + * @returns Result containing ParsedPdf or error + */ +export const mapExtractedObjectToPatterns = ( + config: FormConfig, + extracted: ExtractedForm +): Result => { + try { + const parsedPdf: ParsedPdf = { + patterns: {}, + errors: [], + outputs: {}, + root: 'root', + title: extracted.form_summary.title || 'Default Form Title', + description: + extracted.form_summary.description || 'Default Form Description', + }; + + // Create mapping context with helper functions + const context: MappingContext = { + config, + processPattern: (type, data, id) => + processPatternData(config, parsedPdf, type, data, id), + }; + + // Process form summary + processPatternData(config, parsedPdf, 'form-summary', { + title: extracted.form_summary.title || 'Default Form Title', + description: + extracted.form_summary.description || 'Default Form Description', + }); + + // Process each page + const pageIds: PatternId[] = []; + for (const page of extracted.pages) { + const pageElementIds: PatternId[] = []; + + // Process elements within the page + for (const element of page.elements) { + let result; + + // Map each element type using pattern handlers + switch (element.component_type) { + case 'paragraph': + result = paragraphPatternHandler.parse(element, context); + if (result.pattern) { + pageElementIds.push(result.pattern.id); + } + break; + + case 'rich_text': + result = richTextPatternHandler.parse(element, context); + if (result.pattern) { + pageElementIds.push(result.pattern.id); + } + break; + + case 'text_input': + result = inputPatternHandler.parse(element, context); + if (result.pattern) { + pageElementIds.push(result.pattern.id); + if (result.output) { + parsedPdf.outputs[result.output[0]] = result.output[1]; + } + } + break; + + case 'checkbox': + result = checkboxPatternHandler.parse(element, context); + if (result.pattern) { + pageElementIds.push(result.pattern.id); + if (result.output) { + parsedPdf.outputs[result.output[0]] = result.output[1]; + } + } + break; + + case 'checkbox_group': + // Checkbox group returns a fieldset, but outputs are handled separately + result = checkboxGroupPatternHandler.parse(element, context); + if (result.pattern) { + pageElementIds.push(result.pattern.id); + // Map outputs for each checkbox in the group + for (const option of element.options) { + const checkboxResult = checkboxPatternHandler.parse( + { + component_type: 'checkbox', + id: option.id, + label: option.label, + default_checked: option.default_checked, + }, + context + ); + if (checkboxResult.output) { + parsedPdf.outputs[checkboxResult.output[0]] = + checkboxResult.output[1]; + } + } + } + break; + + case 'radio_group': + result = radioGroupPatternHandler.parse(element, context); + if (result.pattern) { + pageElementIds.push(result.pattern.id); + if (result.output) { + parsedPdf.outputs[result.output[0]] = result.output[1]; + } + } + break; + + case 'fieldset': + result = fieldsetPatternHandler.parse(element, context); + if (result.pattern) { + pageElementIds.push(result.pattern.id); + // Map outputs for each field in the fieldset + for (const field of element.fields) { + let fieldResult; + if (field.component_type === 'text_input') { + fieldResult = inputPatternHandler.parse(field, context); + } else if (field.component_type === 'checkbox') { + fieldResult = checkboxPatternHandler.parse(field, context); + } + if (fieldResult?.output) { + parsedPdf.outputs[fieldResult.output[0]] = + fieldResult.output[1]; + } + } + } + break; + } + } + + // Create page pattern with title from schema + const pagePattern = processPatternData( + config, + parsedPdf, + 'page', + { + title: page.title, + patterns: pageElementIds, + }, + undefined + ); + if (pagePattern) { + pageIds.push(pagePattern.id); + } + } + + // Assign the pages to the root page set + const rootPattern = processPatternData( + config, + parsedPdf, + 'page-set', + { + pages: pageIds, + }, + 'root' + ); + if (rootPattern) { + parsedPdf.patterns['root'] = rootPattern; + } + + return success(parsedPdf); + } catch (error) { + const parseError: ParseError = { + code: 'PATTERN_MAPPING_ERROR', + message: 'Failed to map extracted object to patterns', + details: error, + }; + return failure(parseError); + } +}; + +/** + * Helper function to create and register a pattern. + * Accumulates errors if pattern creation fails. + */ +const processPatternData = ( + config: FormConfig, + parsedPdf: ParsedPdf, + patternType: T['type'], + patternData: T['data'], + patternId?: PatternId +): T | undefined => { + const result = createPattern(config, patternType, patternData, patternId); + if (!result.success) { + parsedPdf.errors.push({ + type: patternType, + data: patternData, + errors: result.error, + }); + return undefined; + } + parsedPdf.patterns[result.data.id] = result.data; + return result.data; +}; diff --git a/packages/forms/src/documents/pdf/domain/schema.ts b/packages/forms/src/documents/pdf/domain/schema.ts new file mode 100644 index 00000000..97067acf --- /dev/null +++ b/packages/forms/src/documents/pdf/domain/schema.ts @@ -0,0 +1,124 @@ +import { z } from 'zod'; + +/** + * Parser Output Schema + * + * This schema defines the structured output format expected from a PDF parser like our LLM parser. + * It represents a parser-independent interpretation of a PDF form. + */ + +const FormSummary = z.object({ + title: z.string().describe('Title of the form'), + description: z.string().describe('Brief description of the form purpose'), +}); + +const TxInput = z.object({ + component_type: z.literal('text_input'), + id: z.string().describe('Exact field ID from PDF metadata'), + label: z.string().describe('User-friendly field label'), + default_value: z.string().optional().describe('Default value if any'), + required: z.boolean().default(false).describe('Whether field is required'), +}); + +const Checkbox = z.object({ + component_type: z.literal('checkbox'), + id: z.string().describe('Exact field ID from PDF metadata'), + label: z.string().describe('User-friendly checkbox label'), + default_checked: z + .boolean() + .default(false) + .describe('Whether checked by default'), +}); + +const CheckboxGroupOption = z.object({ + id: z.string().describe('Exact checkbox field ID from PDF metadata'), + label: z.string().describe('User-friendly checkbox label'), + default_checked: z + .boolean() + .default(false) + .describe('Whether checked by default'), +}); + +const CheckboxGroup = z.object({ + component_type: z.literal('checkbox_group'), + legend: z.string().describe('Legend/label for the checkbox group'), + options: z + .array(CheckboxGroupOption) + .describe('Checkbox options (each is an independent PDF field)'), +}); + +const RadioGroupOption = z.object({ + id: z.string().describe('Option identifier (format: {groupId}.{index})'), + label: z.string().describe('Option label'), + name: z.string().describe('Radio group name (must match group id)'), + default_checked: z + .boolean() + .default(false) + .describe('Whether selected by default'), +}); + +const RadioGroup = z.object({ + component_type: z.literal('radio_group'), + id: z.string().describe('Exact radio group field ID from PDF metadata'), + legend: z.string().describe('Legend/label for the radio group'), + options: z.array(RadioGroupOption).describe('Radio button options'), +}); + +const Paragraph = z.object({ + component_type: z.literal('paragraph'), + text: z.string().describe('Plain text content for instructions or context'), +}); + +const RichText = z.object({ + component_type: z.literal('rich_text'), + text: z + .string() + .describe( + 'Rich text content in HTML format (use semantic HTML tags like h2, h3, p, ul, li, strong, etc.)' + ), +}); + +const FieldsetField = z.discriminatedUnion('component_type', [ + TxInput, + Checkbox, +]); + +const Fieldset = z.object({ + component_type: z.literal('fieldset'), + legend: z.string().describe('Legend for the grouped fields'), + fields: z.array(FieldsetField).describe('Fields within this fieldset'), +}); + +const Element = z.discriminatedUnion('component_type', [ + TxInput, + Checkbox, + CheckboxGroup, + RadioGroup, + Paragraph, + RichText, + Fieldset, +]); + +const Page = z.object({ + title: z + .string() + .describe('Short, descriptive page title for navigation (plain language)'), + elements: z.array(Element).describe('Elements on this page in display order'), +}); + +export const ExtractedFormSchema = z.object({ + form_summary: FormSummary.describe('High-level form summary'), + pages: z.array(Page).describe('Pages in the guided interview, in order'), +}); + +export type ExtractedForm = z.infer; + +// Type exports for individual components (useful for mapper functions) +export type TxInputComponent = z.infer; +export type CheckboxComponent = z.infer; +export type CheckboxGroupComponent = z.infer; +export type RadioGroupComponent = z.infer; +export type ParagraphComponent = z.infer; +export type RichTextComponent = z.infer; +export type FieldsetComponent = z.infer; +export type PageComponent = z.infer; diff --git a/packages/forms/src/documents/pdf/domain/types.ts b/packages/forms/src/documents/pdf/domain/types.ts new file mode 100644 index 00000000..7bc28f89 --- /dev/null +++ b/packages/forms/src/documents/pdf/domain/types.ts @@ -0,0 +1,47 @@ +import { type Result } from '@flexion/forms-common'; + +/** + * Domain Types for PDF Parsing + * + * These are generic types used across all PDF parsing implementations. + * Parser-specific schemas live in their own modules. + */ + +// ============================================================================ +// Error Types +// ============================================================================ + +export type ParseErrorCode = + | 'INVALID_PDF' + | 'FIELD_EXTRACTION_ERROR' + | 'PARSER_ERROR' + | 'SCHEMA_VALIDATION_ERROR' + | 'PATTERN_MAPPING_ERROR'; + +export type ParseError = { + code: ParseErrorCode; + message: string; + details?: unknown; +}; + +// ============================================================================ +// Field Metadata +// ============================================================================ + +/** + * Neutral representation of PDF field metadata. + * This is extracted from pdf-lib and can be used by any parser. + */ +export type FieldMetadata = { + id: string; + type: string; + label: string; + instructions?: string; + page: number; +}; + +// ============================================================================ +// Domain Function Result Types +// ============================================================================ + +export type ExtractFieldMetadataResult = Result; diff --git a/packages/forms/src/documents/pdf/extract.ts b/packages/forms/src/documents/pdf/extract.ts index 099dfb3c..3e68e6ce 100644 --- a/packages/forms/src/documents/pdf/extract.ts +++ b/packages/forms/src/documents/pdf/extract.ts @@ -1,31 +1,8 @@ -import { - PDFDocument, - PDFName, - PDFDict, - PDFTextField, - PDFField, - PDFCheckBox, - PDFDropdown, - PDFOptionList, - PDFRadioGroup, -} from 'pdf-lib'; +import { PDFDocument, PDFName, PDFDict } from 'pdf-lib'; import { stringToBase64 } from '../../util/base64.js'; -import type { DocumentFieldValue, DocumentFieldMap } from '../types.js'; - -// TODO: copied from pdf-lib acrofield internals, check if it's already exposed outside of acroform somewhere -export const getWidgets = async (pdfDoc: PDFDocument): Promise => { - return pdfDoc.context - .enumerateIndirectObjects() - .map(([, obj]) => obj) - .filter( - obj => - obj instanceof PDFDict && - obj.get(PDFName.of('Type')) === PDFName.of('Annot') && - obj.get(PDFName.of('Subtype')) === PDFName.of('Widget') - ) - .map(obj => obj as PDFDict); -}; +import type { DocumentFieldMap } from '../types.js'; +import { extractField } from './adapters/pdf-lib-fields.js'; export const getDocumentFieldData = async ( pdfBytes: Uint8Array @@ -45,59 +22,21 @@ export const getDocumentFieldData = async ( return Object.fromEntries( fields.map(field => { - return [stringToBase64(field.getName()), getFieldValue(field)]; + return [stringToBase64(field.getName()), extractField(field)]; }) ); }; -const getFieldValue = (field: PDFField): DocumentFieldValue => { - if (field instanceof PDFTextField) { - return { - type: 'TextField', - name: field.getName(), - label: field.getName(), - value: field.getText() || '', - maxLength: field.getMaxLength(), - required: field.isRequired(), - }; - } else if (field instanceof PDFCheckBox) { - return { - type: 'CheckBox', - name: field.getName(), - label: field.getName(), - value: field.isChecked(), - required: field.isRequired(), - }; - } else if (field instanceof PDFDropdown) { - return { - type: 'Dropdown', - name: field.getName(), - label: field.getName(), - value: field.getSelected(), - required: field.isRequired(), - }; - } else if (field instanceof PDFOptionList) { - return { - type: 'OptionList', - name: field.getName(), - label: field.getName(), - value: field.getSelected(), - required: field.isRequired(), - }; - } else if (field instanceof PDFRadioGroup) { - return { - type: 'RadioGroup', - name: field.getName(), - options: field.getOptions(), - label: field.getName(), - value: field.getSelected() || '', // pdfLib allows this to be undefined - required: field.isRequired(), - }; - } else { - return { - type: 'not-supported', - name: field.getName(), - error: `unsupported type: ${field.constructor.name}`, - }; - } +// TODO: copied from pdf-lib acrofield internals, check if it's already exposed outside of acroform somewhere +const getWidgets = async (pdfDoc: PDFDocument): Promise => { + return pdfDoc.context + .enumerateIndirectObjects() + .map(([, obj]) => obj) + .filter( + obj => + obj instanceof PDFDict && + obj.get(PDFName.of('Type')) === PDFName.of('Annot') && + obj.get(PDFName.of('Subtype')) === PDFName.of('Widget') + ) + .map(obj => obj as PDFDict); }; diff --git a/packages/forms/src/documents/pdf/generate.ts b/packages/forms/src/documents/pdf/generate.ts index 4206508f..d41f6f3b 100644 --- a/packages/forms/src/documents/pdf/generate.ts +++ b/packages/forms/src/documents/pdf/generate.ts @@ -1,13 +1,9 @@ -import { - PDFDocument, - PDFName, - createPDFAcroFields, - type PDFForm, -} from 'pdf-lib'; +import { PDFDocument } from 'pdf-lib'; -import { Result } from '@gsa-tts/forms-common'; +import { Result } from '@flexion/forms-common'; import { type FormOutput } from '../../index.js'; import { type PDFFieldType } from './index.js'; +import { fillPdfField } from './patterns/index.js'; export const createFormOutputFieldData = ( output: FormOutput, @@ -41,44 +37,12 @@ export const fillPDF = async ( try { Object.entries(fieldData).forEach(([name, value]) => { try { - setFormFieldData(form, value.type, name, value.value); + fillPdfField(form, value.type, name, value.value); } catch (error: any) { console.log('Error setting form field ', error.message); } }); } catch (error: any) { - const fieldDataNames = Object.keys(fieldData); // names we got from API - const fields = form.getFields(); - const fieldNames = fields.map(field => field.getName()); // fieldnames we ripped from the PDF - - // Combine the two arrays with an indication of their source - const combinedNames = [ - ...fieldDataNames.map(name => ({ name, source: 'API' })), - ...fieldNames.map(name => ({ name, source: 'pdf-lib' })), - ]; - - // Use a Map to keep track of unique names and their sources - const uniqueNamesMap = new Map(); - - combinedNames.forEach(({ name, source }) => { - if (!uniqueNamesMap.has(name)) { - uniqueNamesMap.set(name, []); - } - uniqueNamesMap.get(name).push(source); - }); - - // Convert the Map to an array of objects and sort it alphabetically by name - const uniqueNamesArray = Array.from(uniqueNamesMap.entries()) - .map(([name, sources]) => ({ name, sources })) - .sort((a, b) => a.name.localeCompare(b.name)); - - if (error?.message) { - return { - success: false, - error: error?.message || 'error setting PDF field', - }; - } - return { success: false, error: error?.message || 'error setting PDF field', @@ -89,56 +53,3 @@ export const fillPDF = async ( data: await pdfDoc.save(), }; }; - -const setFormFieldData = ( - form: PDFForm, - fieldType: PDFFieldType, - fieldName: string, - fieldValue: any -) => { - if (fieldType === 'TextField') { - const field = form.getTextField(fieldName); - field.setText(fieldValue); - } else if (fieldType === 'CheckBox') { - const field = form.getCheckBox(fieldName); - if (fieldValue) { - field.check(); - } else { - field.uncheck(); - } - } else if (fieldType === 'Attachment') { - const field = form.getDropdown(fieldName); - field.select(fieldValue); - } else if (fieldType === 'Dropdown') { - const field = form.getDropdown(fieldName); - field.select(fieldValue); - } else if (fieldType === 'OptionList') { - const field = form.getDropdown(fieldName); - field.select(fieldValue); - } else if (fieldType === 'RadioGroup') { - // TODO: harmonize the option ids between pdf-lib and the API at ingestion time - try { - const field = form.getRadioGroup(fieldName); - field.select(fieldValue); - } catch (error: any) { - // This logic should work even if pdf-lib misidentifies the field type - // TODO: radioParent should contain the name, not the id - const [radioParent, radioChild] = fieldValue.split('.'); - if (radioChild) { - // TODO: resolve import failure when spaces are present in name, id - const radioChildWithSpace = radioChild.replace('_', ' '); - const field = form.getField(fieldName); - const acroField = field.acroField; - acroField.dict.set(PDFName.of('V'), PDFName.of(radioChildWithSpace)); - const kids = createPDFAcroFields(acroField.Kids()).map(_ => _[0]); - kids.forEach(kid => { - kid.dict.set(PDFName.of('AS'), PDFName.of(radioChildWithSpace)); - }); - } - } - } else if (fieldType === 'Paragraph' || fieldType === 'RichText') { - // do nothing - } else { - const exhaustiveCheck: never = fieldType; - } -}; diff --git a/packages/forms/src/documents/pdf/index.ts b/packages/forms/src/documents/pdf/index.ts index edafb097..d714808c 100644 --- a/packages/forms/src/documents/pdf/index.ts +++ b/packages/forms/src/documents/pdf/index.ts @@ -1,14 +1,37 @@ -import { getDocumentFieldData } from './extract.js'; -import { - type ParsedPdf, - fetchPdfApiResponse, - processApiResponse, -} from './parsing-api.js'; -import type { DocumentFieldMap } from '../types.js'; +// Primary PDF parsing API (Node.js only - uses bedrock) +export { parsePdf } from './parsing-api.js'; +export type { ParsePdf } from './parsing-api.js'; +// Re-export types and utilities +export type { ParsedPdf } from './domain/pattern-mapper.js'; +export type { FieldMetadata, ParseError } from './domain/types.js'; +export type { ExtractedForm } from './domain/schema.js'; +export type { PdfParser } from './services/parser-interface.js'; +export type { PdfParsingContext } from './services/context.js'; + +// Parser implementations (Node.js only) +// NOTE: createBedrockParser is not exported here to avoid pulling in Node.js-only +// AWS SDK code into browser bundles. Import directly from './adapters/bedrock-parser.js' if needed. +// export { createBedrockParser } from './adapters/bedrock-parser.js'; +export { + FakePdfParser, + createSimpleFakeParser, +} from './adapters/fake-parser.js'; + +// Parser factory functions (Node.js only - createProductionPdfParser uses bedrock) +// NOTE: These are not exported to avoid pulling Node.js-only code into browser bundles. +// Import directly from './context.js' if needed in Node.js environments. +// export { +// createProductionPdfParser, +// createTestPdfParser, +// createNoopPdfParser, +// } from './context.js'; + +// PDF generation (browser-safe) export * from './generate.js'; export { generateDummyPDF } from './generate-dummy.js'; +// Legacy types export type PDFDocument = { type: 'pdf'; fields: PDFField[]; @@ -28,14 +51,3 @@ export type PDFFieldType = | 'RadioGroup' | 'Paragraph' | 'RichText'; - -export type ParsePdf = ( - pdf: Uint8Array -) => Promise<{ parsedPdf: ParsedPdf; fields: DocumentFieldMap }>; - -export const parsePdf: ParsePdf = async (pdfBytes: Uint8Array) => { - const fields = await getDocumentFieldData(pdfBytes); - const apiResponse = await fetchPdfApiResponse(pdfBytes); - const parsedPdf = await processApiResponse(apiResponse); - return { parsedPdf, fields }; -}; diff --git a/packages/forms/src/documents/pdf/parsing-api.ts b/packages/forms/src/documents/pdf/parsing-api.ts index e1aa4131..1f23f01b 100644 --- a/packages/forms/src/documents/pdf/parsing-api.ts +++ b/packages/forms/src/documents/pdf/parsing-api.ts @@ -1,379 +1,44 @@ -import * as z from 'zod'; - -import { type FieldsetPattern } from '../../patterns/fieldset/config.js'; -import { type InputPattern } from '../../patterns/input/config.js'; -import { PagePattern } from '../../patterns/page/config.js'; -import { PageSetPattern } from '../../patterns/page-set/config.js'; -import { type ParagraphPattern } from '../../patterns/paragraph.js'; -import { type CheckboxPattern } from '../../patterns/checkbox.js'; -import { type RadioGroupPattern } from '../../patterns/radio-group.js'; -import { RichTextPattern } from '../../patterns/rich-text.js'; - -import { uint8ArrayToBase64 } from '../../util/base64.js'; -import { type DocumentFieldMap } from '../types.js'; -import { - createPattern, - FormConfig, - Pattern, - PatternId, - PatternMap, -} from '../../pattern.js'; -import { FormErrors } from '../../error.js'; -import { defaultFormConfig } from '../../patterns/index.js'; - -const FormSummary = z.object({ - component_type: z.literal('form_summary'), - title: z.string(), - description: z.string(), -}); - -const TxInput = z.object({ - component_type: z.literal('text_input'), - id: z.string(), - label: z.string(), - default_value: z.string(), - required: z.boolean(), - page: z.union([z.number(), z.string()]), -}); - -const Checkbox = z.object({ - component_type: z.literal('checkbox'), - id: z.string(), - label: z.string(), - default_checked: z.boolean(), - page: z.union([z.number(), z.string()]), -}); - -const RadioGroupOption = z.object({ - id: z.string(), - label: z.string(), - name: z.string(), - default_checked: z.boolean(), - page: z.union([z.number(), z.string()]), -}); - -const RadioGroup = z.object({ - id: z.string(), - component_type: z.literal('radio_group'), - legend: z.string(), - options: RadioGroupOption.array(), - page: z.union([z.number(), z.string()]), -}); - -const Paragraph = z.object({ - component_type: z.literal('paragraph'), - text: z.string(), - page: z.union([z.number(), z.string()]), -}); - -const RichText = z.object({ - component_type: z.literal('rich_text'), - text: z.string(), - page: z.union([z.number(), z.string()]), -}); - -const Fieldset = z.object({ - component_type: z.literal('fieldset'), - legend: z.string(), - fields: z.union([TxInput, Checkbox]).array(), - page: z.union([z.number(), z.string()]), -}); - -const ExtractedObject = z.object({ - raw_text: z.string(), - form_summary: FormSummary, - elements: z - .union([TxInput, Checkbox, RadioGroup, Paragraph, Fieldset, RichText]) - .array(), -}); - -type ExtractedObject = z.infer; - -export type ParsedPdf = { - patterns: PatternMap; - errors: { - type: Pattern['type']; - data: Pattern['data']; - errors: FormErrors; - }[]; - outputs: DocumentFieldMap; // to populate FormOutput - root: PatternId; - title: string; - description: string; -}; - -export type FetchPdfApiResponse = ( - rawData: Uint8Array, - url?: string -) => Promise; - -export const fetchPdfApiResponse: FetchPdfApiResponse = async ( - rawData: Uint8Array, - url: string = 'https://10x-atj-doc-automation-staging.app.cloud.gov/api/v2/parse' // 'http://localhost:5000/api/v2/parse' +import { parsePdfToPatterns } from './services/parse-pdf-to-patterns.js'; +import type { PdfParsingContext } from './services/context.js'; +import type { ParsedPdf } from './domain/pattern-mapper.js'; +import { getDocumentFieldData } from './extract.js'; +import type { DocumentFieldMap } from '../types.js'; + +// Re-export ParsedPdf for backward compatibility +export type { ParsedPdf }; + +/** + * Type for the parsePdf function. + * Returns both the parsed pattern structure and raw field data. + */ +export type ParsePdf = ( + context: PdfParsingContext, + pdf: Uint8Array +) => Promise<{ parsedPdf: ParsedPdf; fields: DocumentFieldMap }>; + +/** + * Parses a PDF into patterns and extracts field data. + * This is the primary API for PDF parsing. + * + * @param context - PDF parsing context with LLM services, parser, and form config + * @param pdfBytes - Raw PDF bytes + * @returns Object containing: + * - parsedPdf: Pattern structure for building form UI + * - fields: Raw PDF field data for filling PDF with user responses + */ +export const parsePdf: ParsePdf = async ( + context: PdfParsingContext, + pdfBytes: Uint8Array ) => { - const base64 = await uint8ArrayToBase64(rawData); - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - pdf: base64, - }), - }); - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return await response.json(); -}; - -export const processApiResponse = async (json: any): Promise => { - const extracted: ExtractedObject = ExtractedObject.parse(json.parsed_pdf); - const rootSequence: PatternId[] = []; - const pagePatterns: Record = {}; - const parsedPdf: ParsedPdf = { - patterns: {}, - errors: [], - outputs: {}, - root: 'root', - title: extracted.form_summary.title || 'Default Form Title', - description: - extracted.form_summary.description || 'Default Form Description', - }; - - const summary = processPatternData( - defaultFormConfig, - parsedPdf, - 'form-summary', - { - title: extracted.form_summary.title || 'Default Form Title', - description: - extracted.form_summary.description || 'Default Form Description', - } - ); - if (summary) { - rootSequence.push(summary.id); - } - - for (const element of extracted.elements) { - const fieldsetPatterns: PatternId[] = []; - // Add paragraph elements - if (element.component_type === 'paragraph') { - const paragraph = processPatternData( - defaultFormConfig, - parsedPdf, - 'paragraph', - { - text: element.text, - } - ); - if (paragraph) { - pagePatterns[element.page] = (pagePatterns[element.page] || []).concat( - paragraph.id - ); - } - continue; - } - - if (element.component_type === 'rich_text') { - const richText = processPatternData( - defaultFormConfig, - parsedPdf, - 'rich-text', - { - text: element.text, - } - ); - if (richText) { - pagePatterns[element.page] = (pagePatterns[element.page] || []).concat( - richText.id - ); - } - continue; - } - - if (element.component_type === 'checkbox') { - const checkboxPattern = processPatternData( - defaultFormConfig, - parsedPdf, - 'checkbox', - { - label: element.label, - defaultChecked: element.default_checked, - } - ); - if (checkboxPattern) { - pagePatterns[element.page] = (pagePatterns[element.page] || []).concat( - checkboxPattern.id - ); - parsedPdf.outputs[checkboxPattern.id] = { - type: 'CheckBox', - name: element.id, - label: element.label, - value: false, - required: true, - }; - } - continue; - } - - if (element.component_type === 'radio_group') { - const radioGroupPattern = processPatternData( - defaultFormConfig, - parsedPdf, - 'radio-group', - { - label: element.legend, - hint: '', - options: element.options.map(option => ({ - id: option.id, - label: option.label, - name: option.name, - defaultChecked: option.default_checked, - })), - required: false, - } - ); - if (radioGroupPattern) { - pagePatterns[element.page] = (pagePatterns[element.page] || []).concat( - radioGroupPattern.id - ); - parsedPdf.outputs[radioGroupPattern.id] = { - type: 'RadioGroup', - name: element.id, - label: element.legend, - options: element.options.map(option => ({ - id: option.id, - label: option.label, - name: option.name, - defaultChecked: option.default_checked, - })), - value: '', - required: true, - }; - } - continue; - } + // Extract raw field data (needed for filling PDF later) + const fields = await getDocumentFieldData(pdfBytes); - if (element.component_type === 'fieldset') { - for (const input of element.fields) { - if (input.component_type === 'text_input') { - const inputPattern = processPatternData( - defaultFormConfig, - parsedPdf, - 'input', - { - label: input.label, - required: false, - initial: '', - } - ); - if (inputPattern) { - fieldsetPatterns.push(inputPattern.id); - parsedPdf.outputs[inputPattern.id] = { - type: 'TextField', - name: input.id, - label: input.label, - value: '', - maxLength: 1024, - required: input.required, - }; - } - } - if (input.component_type === 'checkbox') { - const checkboxPattern = processPatternData( - defaultFormConfig, - parsedPdf, - 'checkbox', - { - label: input.label, - defaultChecked: false, - } - ); - if (checkboxPattern) { - fieldsetPatterns.push(checkboxPattern.id); - parsedPdf.outputs[checkboxPattern.id] = { - type: 'CheckBox', - name: input.id, - label: input.label, - value: false, - required: true, - }; - } - } - } - } + // Parse PDF to patterns using injected context + const result = await parsePdfToPatterns(context, pdfBytes); - // Add fieldset to parsedPdf.patterns and rootSequence - if (element.component_type === 'fieldset' && fieldsetPatterns.length > 0) { - const fieldset = processPatternData( - defaultFormConfig, - parsedPdf, - 'fieldset', - { - legend: element.legend, - patterns: fieldsetPatterns, - } - ); - if (fieldset) { - pagePatterns[element.page] = (pagePatterns[element.page] || []).concat( - fieldset.id - ); - } - } - } - - // Create a pattern for the single, first page. - const pages: PatternId[] = Object.entries(pagePatterns) - .map(([page, patterns], idx) => { - const pagePattern = processPatternData( - defaultFormConfig, - parsedPdf, - 'page', - { - title: `${page}`, - patterns, - }, - undefined, - idx - ); - return pagePattern?.id; - }) - .filter(page => page !== undefined) as PatternId[]; - - // Assign the page to the root page set. - const rootPattern = processPatternData( - defaultFormConfig, - parsedPdf, - 'page-set', - { - pages, - }, - 'root' - ); - if (rootPattern) { - parsedPdf.patterns['root'] = rootPattern; - } - return parsedPdf; -}; - -const processPatternData = ( - config: FormConfig, - parsedPdf: ParsedPdf, - patternType: T['type'], - patternData: T['data'], - patternId?: PatternId, - page?: number -) => { - const result = createPattern(config, patternType, patternData, patternId); if (!result.success) { - parsedPdf.errors.push({ - type: patternType, - data: patternData, - errors: result.error, - }); - return; + throw new Error(`PDF parsing failed: ${result.error.message}`); } - parsedPdf.patterns[result.data.id] = result.data; - return result.data; + + return { parsedPdf: result.data, fields }; }; diff --git a/packages/forms/src/documents/pdf/patterns/checkbox-group.ts b/packages/forms/src/documents/pdf/patterns/checkbox-group.ts new file mode 100644 index 00000000..3ed40f70 --- /dev/null +++ b/packages/forms/src/documents/pdf/patterns/checkbox-group.ts @@ -0,0 +1,64 @@ +import type { PatternId } from '../../../pattern.js'; +import type { FieldsetPattern } from '../../../patterns/fieldset/config.js'; +import type { CheckboxGroupComponent } from '../domain/schema.js'; +import type { DocumentFieldValue } from '../../types.js'; +import type { + PatternFieldHandler, + MappingContext, + MappingResult, +} from './types.js'; +import { checkboxPatternHandler } from './checkbox.js'; + +/** + * Handler for checkbox group components. + * Maps checkbox_group components from Bedrock to FieldsetPattern with nested checkboxes. + * Note: This maps to FieldsetPattern, not a separate checkbox-group pattern. + */ +export const checkboxGroupPatternHandler: PatternFieldHandler< + FieldsetPattern, + CheckboxGroupComponent +> = { + patternType: 'fieldset', // checkbox_group maps to fieldset pattern + + parse( + group: CheckboxGroupComponent, + context: MappingContext + ): MappingResult { + const checkboxPatternIds: PatternId[] = []; + const outputs: Array<[PatternId, DocumentFieldValue]> = []; + + for (const option of group.options) { + const result = checkboxPatternHandler.parse( + { + component_type: 'checkbox', + id: option.id, + label: option.label, + default_checked: option.default_checked, + }, + context + ); + + if (result.pattern) { + checkboxPatternIds.push(result.pattern.id); + if (result.output) { + outputs.push(result.output); + } + } + } + + if (checkboxPatternIds.length === 0) return {}; + + const fieldset = context.processPattern('fieldset', { + legend: group.legend, + patterns: checkboxPatternIds, + }); + + if (!fieldset) return {}; + + // Note: We can't return multiple outputs in MappingResult, + // so outputs are handled separately in pattern-mapper.ts + return { pattern: fieldset }; + }, + + // No fill method - checkbox group is a container, its children handle filling +}; diff --git a/packages/forms/src/documents/pdf/patterns/checkbox.ts b/packages/forms/src/documents/pdf/patterns/checkbox.ts new file mode 100644 index 00000000..46428931 --- /dev/null +++ b/packages/forms/src/documents/pdf/patterns/checkbox.ts @@ -0,0 +1,54 @@ +import type { PDFForm } from 'pdf-lib'; +import type { CheckboxPattern } from '../../../patterns/checkbox.js'; +import type { CheckboxComponent } from '../domain/schema.js'; +import type { + PatternFieldHandler, + MappingContext, + MappingResult, +} from './types.js'; + +/** + * Handler for checkbox patterns. + * Maps checkbox components from Bedrock to CheckboxPattern. + */ +export const checkboxPatternHandler: PatternFieldHandler< + CheckboxPattern, + CheckboxComponent +> = { + patternType: 'checkbox', + + parse( + checkbox: CheckboxComponent, + context: MappingContext + ): MappingResult { + const pattern = context.processPattern('checkbox', { + label: checkbox.label, + defaultChecked: checkbox.default_checked, + }); + + if (!pattern) return {}; + + return { + pattern, + output: [ + pattern.id, + { + type: 'CheckBox', + name: checkbox.id, + label: checkbox.label, + value: false, + required: true, + }, + ], + }; + }, + + fill(form: PDFForm, name: string, value: boolean) { + const field = form.getCheckBox(name); + if (value) { + field.check(); + } else { + field.uncheck(); + } + }, +}; diff --git a/packages/forms/src/documents/pdf/patterns/fieldset.ts b/packages/forms/src/documents/pdf/patterns/fieldset.ts new file mode 100644 index 00000000..98aa7b3a --- /dev/null +++ b/packages/forms/src/documents/pdf/patterns/fieldset.ts @@ -0,0 +1,65 @@ +import type { PatternId } from '../../../pattern.js'; +import type { FieldsetPattern } from '../../../patterns/fieldset/config.js'; +import type { FieldsetComponent } from '../domain/schema.js'; +import type { DocumentFieldValue } from '../../types.js'; +import type { + PatternFieldHandler, + MappingContext, + MappingResult, +} from './types.js'; +import { inputPatternHandler } from './input.js'; +import { checkboxPatternHandler } from './checkbox.js'; + +/** + * Handler for fieldset patterns. + * Maps fieldset components from Bedrock to FieldsetPattern. + * Processes nested fields using their respective handlers. + */ +export const fieldsetPatternHandler: PatternFieldHandler< + FieldsetPattern, + FieldsetComponent +> = { + patternType: 'fieldset', + + parse( + fieldset: FieldsetComponent, + context: MappingContext + ): MappingResult { + const fieldPatternIds: PatternId[] = []; + const outputs: Array<[PatternId, DocumentFieldValue]> = []; + + for (const field of fieldset.fields) { + let result: MappingResult; + + if (field.component_type === 'text_input') { + result = inputPatternHandler.parse(field, context); + } else if (field.component_type === 'checkbox') { + result = checkboxPatternHandler.parse(field, context); + } else { + continue; + } + + if (result.pattern) { + fieldPatternIds.push(result.pattern.id); + if (result.output) { + outputs.push(result.output); + } + } + } + + if (fieldPatternIds.length === 0) return {}; + + const pattern = context.processPattern('fieldset', { + legend: fieldset.legend, + patterns: fieldPatternIds, + }); + + if (!pattern) return {}; + + // Note: We return the pattern here, but outputs are handled separately + // in the main mapping logic (pattern-mapper.ts) + return { pattern }; + }, + + // No fill method - fieldset is a container, its children handle filling +}; diff --git a/packages/forms/src/documents/pdf/patterns/index.ts b/packages/forms/src/documents/pdf/patterns/index.ts new file mode 100644 index 00000000..19558b4b --- /dev/null +++ b/packages/forms/src/documents/pdf/patterns/index.ts @@ -0,0 +1,106 @@ +import type { PDFForm } from 'pdf-lib'; +import type { Pattern } from '../../../pattern.js'; +import type { PDFFieldType } from '../index.js'; +import type { PatternFieldHandler } from './types.js'; +import { inputPatternHandler } from './input.js'; +import { checkboxPatternHandler } from './checkbox.js'; +import { radioGroupPatternHandler } from './radio-group.js'; +import { paragraphPatternHandler } from './paragraph.js'; +import { richTextPatternHandler } from './rich-text.js'; +import { fieldsetPatternHandler } from './fieldset.js'; + +// Export individual handlers for use in pattern-mapper.ts +// These are exported to allow explicit usage in switch statements +export { inputPatternHandler } from './input.js'; +export { checkboxPatternHandler } from './checkbox.js'; +export { checkboxGroupPatternHandler } from './checkbox-group.js'; +export { radioGroupPatternHandler } from './radio-group.js'; +export { paragraphPatternHandler } from './paragraph.js'; +export { richTextPatternHandler } from './rich-text.js'; +export { fieldsetPatternHandler } from './fieldset.js'; + +// Export types +export type { + PatternFieldHandler, + MappingContext, + MappingResult, +} from './types.js'; + +/** + * Registry of all pattern field handlers. + * Maps pattern type to handler implementation. + */ +export const patternHandlerRegistry = { + input: inputPatternHandler, + checkbox: checkboxPatternHandler, + 'radio-group': radioGroupPatternHandler, + paragraph: paragraphPatternHandler, + 'rich-text': richTextPatternHandler, + fieldset: fieldsetPatternHandler, +} as const satisfies Record; + +/** + * Get a pattern handler by pattern type. + */ +export const getPatternHandler =

( + patternType: P['type'] +): PatternFieldHandler

| undefined => { + return patternHandlerRegistry[ + patternType as keyof typeof patternHandlerRegistry + ] as PatternFieldHandler

| undefined; +}; + +/** + * Mapping from PDF field type to pattern handler fill method. + * This is used during PDF generation to fill fields with user responses. + */ +const fieldTypeFillHandlers = { + TextField: inputPatternHandler.fill, + CheckBox: checkboxPatternHandler.fill, + RadioGroup: radioGroupPatternHandler.fill, + Dropdown: undefined, // Not yet implemented + OptionList: undefined, // Not yet implemented + Attachment: undefined, // Special case - uses dropdown logic + Paragraph: undefined, // Display-only, no fill + RichText: undefined, // Display-only, no fill +} as const satisfies Record< + PDFFieldType, + ((form: PDFForm, name: string, value: any) => void) | undefined +>; + +/** + * Fill a PDF field with a value based on its type. + * Dispatches to the appropriate pattern handler's fill method. + */ +export const fillPdfField = ( + form: PDFForm, + fieldType: PDFFieldType, + name: string, + value: any +): void => { + // Handle special cases + if (fieldType === 'Paragraph' || fieldType === 'RichText') { + return; // Display-only, no fill needed + } + + if (fieldType === 'Attachment') { + // Attachment uses dropdown logic + const field = form.getDropdown(name); + field.select(value); + return; + } + + if (fieldType === 'Dropdown' || fieldType === 'OptionList') { + // Fallback for dropdown/option list + const field = form.getDropdown(name); + field.select(value); + return; + } + + const fillHandler = fieldTypeFillHandlers[fieldType]; + if (!fillHandler) { + throw new Error(`Unknown field type: ${fieldType}`); + } + + fillHandler(form, name, value); +}; diff --git a/packages/forms/src/documents/pdf/patterns/input.ts b/packages/forms/src/documents/pdf/patterns/input.ts new file mode 100644 index 00000000..c42926a8 --- /dev/null +++ b/packages/forms/src/documents/pdf/patterns/input.ts @@ -0,0 +1,52 @@ +import type { PDFForm } from 'pdf-lib'; +import type { InputPattern } from '../../../patterns/input/config.js'; +import type { TxInputComponent } from '../domain/schema.js'; +import type { + PatternFieldHandler, + MappingContext, + MappingResult, +} from './types.js'; + +/** + * Handler for input (text field) patterns. + * Maps text_input components from Bedrock to InputPattern. + */ +export const inputPatternHandler: PatternFieldHandler< + InputPattern, + TxInputComponent +> = { + patternType: 'input', + + parse( + input: TxInputComponent, + context: MappingContext + ): MappingResult { + const pattern = context.processPattern('input', { + label: input.label, + required: input.required, + initial: input.default_value || '', + }); + + if (!pattern) return {}; + + return { + pattern, + output: [ + pattern.id, + { + type: 'TextField', + name: input.id, + label: input.label, + value: '', + maxLength: 1024, + required: input.required, + }, + ], + }; + }, + + fill(form: PDFForm, name: string, value: string) { + const field = form.getTextField(name); + field.setText(value); + }, +}; diff --git a/packages/forms/src/documents/pdf/patterns/paragraph.ts b/packages/forms/src/documents/pdf/patterns/paragraph.ts new file mode 100644 index 00000000..3b374909 --- /dev/null +++ b/packages/forms/src/documents/pdf/patterns/paragraph.ts @@ -0,0 +1,32 @@ +import type { ParagraphPattern } from '../../../patterns/paragraph.js'; +import type { ParagraphComponent } from '../domain/schema.js'; +import type { + PatternFieldHandler, + MappingContext, + MappingResult, +} from './types.js'; + +/** + * Handler for paragraph patterns. + * Maps paragraph components from Bedrock to ParagraphPattern. + * Paragraphs are display-only and have no corresponding PDF field to fill. + */ +export const paragraphPatternHandler: PatternFieldHandler< + ParagraphPattern, + ParagraphComponent +> = { + patternType: 'paragraph', + + parse( + paragraph: ParagraphComponent, + context: MappingContext + ): MappingResult { + const pattern = context.processPattern('paragraph', { + text: paragraph.text, + }); + + return pattern ? { pattern } : {}; + }, + + // No fill method - paragraphs are display-only +}; diff --git a/packages/forms/src/documents/pdf/patterns/radio-group.ts b/packages/forms/src/documents/pdf/patterns/radio-group.ts new file mode 100644 index 00000000..2cc3c79b --- /dev/null +++ b/packages/forms/src/documents/pdf/patterns/radio-group.ts @@ -0,0 +1,81 @@ +import { PDFForm, PDFName, createPDFAcroFields } from 'pdf-lib'; +import type { RadioGroupPattern } from '../../../patterns/radio-group.js'; +import type { RadioGroupComponent } from '../domain/schema.js'; +import type { + PatternFieldHandler, + MappingContext, + MappingResult, +} from './types.js'; + +/** + * Handler for radio group patterns. + * Maps radio_group components from Bedrock to RadioGroupPattern. + */ +export const radioGroupPatternHandler: PatternFieldHandler< + RadioGroupPattern, + RadioGroupComponent +> = { + patternType: 'radio-group', + + parse( + group: RadioGroupComponent, + context: MappingContext + ): MappingResult { + const pattern = context.processPattern('radio-group', { + label: group.legend, + hint: '', + options: group.options.map(option => ({ + id: option.id, + label: option.label, + name: option.name, + defaultChecked: option.default_checked, + })), + required: false, + }); + + if (!pattern) return {}; + + return { + pattern, + output: [ + pattern.id, + { + type: 'RadioGroup', + name: group.id, + label: group.legend, + options: group.options.map(option => ({ + id: option.id, + label: option.label, + name: option.name, + defaultChecked: option.default_checked, + })), + value: '', + required: true, + }, + ], + }; + }, + + fill(form: PDFForm, name: string, value: string) { + // TODO: harmonize the option ids between pdf-lib and the API at ingestion time + try { + const field = form.getRadioGroup(name); + field.select(value); + } catch (error: any) { + // This logic should work even if pdf-lib misidentifies the field type + // TODO: radioParent should contain the name, not the id + const [radioParent, radioChild] = value.split('.'); + if (radioChild) { + // TODO: resolve import failure when spaces are present in name, id + const radioChildWithSpace = radioChild.replace('_', ' '); + const field = form.getField(name); + const acroField = field.acroField; + acroField.dict.set(PDFName.of('V'), PDFName.of(radioChildWithSpace)); + const kids = createPDFAcroFields(acroField.Kids()).map(_ => _[0]); + kids.forEach(kid => { + kid.dict.set(PDFName.of('AS'), PDFName.of(radioChildWithSpace)); + }); + } + } + }, +}; diff --git a/packages/forms/src/documents/pdf/patterns/rich-text.ts b/packages/forms/src/documents/pdf/patterns/rich-text.ts new file mode 100644 index 00000000..4086fb80 --- /dev/null +++ b/packages/forms/src/documents/pdf/patterns/rich-text.ts @@ -0,0 +1,32 @@ +import type { RichTextPattern } from '../../../patterns/rich-text.js'; +import type { RichTextComponent } from '../domain/schema.js'; +import type { + PatternFieldHandler, + MappingContext, + MappingResult, +} from './types.js'; + +/** + * Handler for rich text patterns. + * Maps rich_text components from Bedrock to RichTextPattern. + * Rich text is display-only and has no corresponding PDF field to fill. + */ +export const richTextPatternHandler: PatternFieldHandler< + RichTextPattern, + RichTextComponent +> = { + patternType: 'rich-text', + + parse( + richText: RichTextComponent, + context: MappingContext + ): MappingResult { + const pattern = context.processPattern('rich-text', { + text: richText.text, + }); + + return pattern ? { pattern } : {}; + }, + + // No fill method - rich text is display-only +}; diff --git a/packages/forms/src/documents/pdf/patterns/types.ts b/packages/forms/src/documents/pdf/patterns/types.ts new file mode 100644 index 00000000..ebbacfc8 --- /dev/null +++ b/packages/forms/src/documents/pdf/patterns/types.ts @@ -0,0 +1,49 @@ +import type { PDFForm } from 'pdf-lib'; +import type { Pattern, FormConfig, PatternId } from '../../../pattern.js'; +import type { DocumentFieldValue } from '../../types.js'; + +/** + * Context passed to pattern parsing functions. + * Contains the form config and helper to process nested patterns. + */ +export type MappingContext = { + config: FormConfig; + processPattern: ( + type: string, + data: any, + id?: PatternId + ) => T | undefined; +}; + +/** + * Result of parsing a component into a pattern. + * Includes the created pattern and optional PDF field mapping. + */ +export type MappingResult

= { + pattern?: P; + output?: [PatternId, DocumentFieldValue]; +}; + +/** + * Generic handler for a pattern type. + * Encapsulates both parsing (Bedrock → Pattern) and filling (Pattern → PDF). + */ +export interface PatternFieldHandler< + P extends Pattern = Pattern, + BedrockComponent = any, +> { + /** Pattern type identifier */ + patternType: P['type']; + + /** + * Parse a Bedrock component into a Pattern and optional PDF field mapping. + * This is called during PDF ingestion to build the form structure. + */ + parse(component: BedrockComponent, context: MappingContext): MappingResult

; + + /** + * Fill a PDF form field with a user response value. + * Optional - some patterns (like paragraph, rich-text) don't have corresponding PDF fields. + */ + fill?(form: PDFForm, fieldName: string, value: any): void; +} diff --git a/packages/forms/src/documents/pdf/scripts/extract-fields.ts b/packages/forms/src/documents/pdf/scripts/extract-fields.ts new file mode 100644 index 00000000..131939c8 --- /dev/null +++ b/packages/forms/src/documents/pdf/scripts/extract-fields.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node +import { readFileSync } from 'fs'; +import { getDocumentFieldData } from '../extract.js'; + +async function main() { + const pdfPath = + process.argv[2] || + 'packages/forms/sample-documents/doj-pardon-marijuana/demo-application_for_certificate_of_pardon_for_simple_marijuana_possession.pdf'; + const pdfBytes = readFileSync(pdfPath); + const fields = await getDocumentFieldData(new Uint8Array(pdfBytes)); + + // Print radio groups only + const radioGroups = Object.entries(fields).filter( + ([_, field]) => field.type === 'RadioGroup' + ); + console.log('Radio Groups:', radioGroups.length); + radioGroups.forEach(([name, field]) => { + if (field.type === 'RadioGroup') { + console.log(`\n${name}:`); + console.log(` type: ${field.type}`); + console.log(` label: ${field.label}`); + console.log(` name: ${field.name}`); + console.log(` options:`, JSON.stringify(field.options, null, 4)); + } + }); +} + +main().catch(console.error); diff --git a/packages/forms/src/documents/pdf/scripts/parse-pdf-with-bedrock.ts b/packages/forms/src/documents/pdf/scripts/parse-pdf-with-bedrock.ts new file mode 100644 index 00000000..a4d3b69a --- /dev/null +++ b/packages/forms/src/documents/pdf/scripts/parse-pdf-with-bedrock.ts @@ -0,0 +1,73 @@ +#!/usr/bin/env node +/** + * Script to parse a PDF using AWS Bedrock and save the response to a JSON file. + * + * Usage: + * tsx packages/forms/src/documents/pdf/scripts/parse-pdf-with-bedrock.ts + * + * Example: + * tsx packages/forms/src/documents/pdf/scripts/parse-pdf-with-bedrock.ts \ + * packages/forms/src/documents/__tests__/sample-data/doj-pardon-marijuana/demo-application_for_certificate_of_pardon_for_simple_marijuana_possession.pdf \ + * output.json + */ + +import { readFile, writeFile } from 'fs/promises'; +import { createBedrockParser } from '../adapters/bedrock-parser.js'; +import { extractFieldMetadata } from '../domain/field-extractor.js'; +import { createTestLlmContext } from '../../../llm/services/context.js'; + +async function main() { + const args = process.argv.slice(2); + + if (args.length < 2) { + console.error( + 'Usage: tsx parse-pdf-with-bedrock.ts ' + ); + process.exit(1); + } + + const [pdfPath, outputPath] = args; + + console.log(`Reading PDF from: ${pdfPath}`); + const pdfBytes = await readFile(pdfPath); + + console.log('Extracting field metadata...'); + const metadataResult = await extractFieldMetadata(new Uint8Array(pdfBytes)); + + if (!metadataResult.success) { + console.error('Failed to extract metadata:', metadataResult.error); + process.exit(1); + } + + console.log(`Found ${metadataResult.data.length} fields`); + console.log('Invoking Bedrock...'); + const startTime = Date.now(); + + // Use test LLM context with filesystem caching (shared workspace root) + const llmContext = createTestLlmContext(); + const parser = createBedrockParser(llmContext); + const result = await parser.parse( + new Uint8Array(pdfBytes), + metadataResult.data + ); + + const duration = Date.now() - startTime; + console.log(`Bedrock invocation completed in ${duration}ms`); + + if (!result.success) { + console.error('Parsing failed:', result.error); + process.exit(1); + } + + console.log(`Writing output to: ${outputPath}`); + await writeFile(outputPath, JSON.stringify(result.data, null, 2), 'utf-8'); + + console.log('✓ Success!'); + console.log(`\nOutput saved to: ${outputPath}`); + console.log(`Duration: ${duration}ms`); +} + +main().catch(error => { + console.error('Error:', error); + process.exit(1); +}); diff --git a/packages/forms/src/documents/pdf/services/context.ts b/packages/forms/src/documents/pdf/services/context.ts new file mode 100644 index 00000000..a7047b0f --- /dev/null +++ b/packages/forms/src/documents/pdf/services/context.ts @@ -0,0 +1,13 @@ +import type { FormConfig } from '../../../pattern.js'; +import type { PdfParser } from './parser-interface.js'; + +/** + * Context for PDF parsing operations. + * Parser implementations encapsulate their own LLM context internally. + */ +export type PdfParsingContext = { + /** Parser implementation (Bedrock, fake, etc.) */ + parser: PdfParser; + /** Form configuration with pattern definitions */ + formConfig: FormConfig; +}; diff --git a/packages/forms/src/documents/pdf/services/parse-pdf-to-patterns.test.ts b/packages/forms/src/documents/pdf/services/parse-pdf-to-patterns.test.ts new file mode 100644 index 00000000..3d8dec1a --- /dev/null +++ b/packages/forms/src/documents/pdf/services/parse-pdf-to-patterns.test.ts @@ -0,0 +1,166 @@ +import { describe, it, expect, vi } from 'vitest'; +import { defaultFormConfig } from '../../../patterns/index.js'; +import { parsePdfToPatterns } from './parse-pdf-to-patterns.js'; +import { FakePdfParser } from '../adapters/fake-parser.js'; +import type { ExtractedForm } from '../domain/schema.js'; +import { success } from '@flexion/forms-common'; +import { createNoopLlmContext } from '../../../llm/services/context.js'; + +// Mock the field extractor to avoid needing a real PDF +vi.mock('../domain/field-extractor.js', () => ({ + extractFieldMetadata: async () => + success([ + { + id: 'firstName', + type: 'TextField', + label: 'First Name', + page: 0, + }, + { + id: 'lastName', + type: 'TextField', + label: 'Last Name', + page: 0, + }, + { + id: 'email', + type: 'TextField', + label: 'Email', + page: 0, + }, + { + id: 'newsletter', + type: 'CheckBox', + label: 'Newsletter', + page: 0, + }, + ]), +})); + +describe('parsePdfToPatterns', () => { + it('should parse PDF using injected fake parser', async () => { + // Arrange: Create a simple test form structure + const mockExtracted: ExtractedForm = { + form_summary: { + title: 'Test Application Form', + description: 'A simple test form for unit testing', + }, + pages: [ + { + title: 'Personal Information', + elements: [ + { + component_type: 'paragraph', + text: 'Please provide your personal information', + }, + { + component_type: 'fieldset', + legend: 'Name', + fields: [ + { + component_type: 'text_input', + id: 'firstName', + label: 'First Name', + required: true, + }, + { + component_type: 'text_input', + id: 'lastName', + label: 'Last Name', + required: true, + }, + ], + }, + ], + }, + { + title: 'Contact Details', + elements: [ + { + component_type: 'text_input', + id: 'email', + label: 'Email Address', + required: true, + }, + { + component_type: 'checkbox', + id: 'newsletter', + label: 'Subscribe to newsletter', + default_checked: false, + }, + ], + }, + ], + }; + + // Create fake parser with mock response + const fakeParser = new FakePdfParser(mockExtracted); + + // Create context with fake parser + const context = { + llm: createNoopLlmContext(), + parser: fakeParser, + formConfig: defaultFormConfig, + }; + + // Act: Parse PDF (no real API calls!) + const pdfBytes = new Uint8Array([0x25, 0x50, 0x44, 0x46]); // Fake PDF header + const result = await parsePdfToPatterns(context, pdfBytes); + + // Assert: Verify the result + expect(result.success).toBe(true); + + if (result.success) { + const parsedPdf = result.data; + + // Check basic structure + expect(parsedPdf.title).toBe('Test Application Form'); + expect(parsedPdf.description).toBe('A simple test form for unit testing'); + + // Check patterns were created + expect(parsedPdf.patterns).toBeDefined(); + expect(Object.keys(parsedPdf.patterns).length).toBeGreaterThan(0); + + // Check root pattern exists + expect(parsedPdf.patterns['root']).toBeDefined(); + expect(parsedPdf.patterns['root'].type).toBe('page-set'); + + // Check outputs were created + expect(parsedPdf.outputs).toBeDefined(); + expect(Object.keys(parsedPdf.outputs).length).toBeGreaterThan(0); + + // No errors should be present + expect(parsedPdf.errors).toHaveLength(0); + } + }); + + it('should handle parser errors gracefully', async () => { + // Arrange: Create a fake parser that returns an error + const errorParser: any = { + parse: async () => ({ + success: false, + error: { + code: 'PARSER_ERROR', + message: 'Simulated parser error', + }, + }), + }; + + const context = { + llm: createNoopLlmContext(), + parser: errorParser, + formConfig: defaultFormConfig, + }; + + // Act + const pdfBytes = new Uint8Array([0x25, 0x50, 0x44, 0x46]); + const result = await parsePdfToPatterns(context, pdfBytes); + + // Assert: Error is propagated correctly + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.code).toBe('PARSER_ERROR'); + expect(result.error.message).toBe('Simulated parser error'); + } + }); +}); diff --git a/packages/forms/src/documents/pdf/services/parse-pdf-to-patterns.ts b/packages/forms/src/documents/pdf/services/parse-pdf-to-patterns.ts new file mode 100644 index 00000000..9f19a20a --- /dev/null +++ b/packages/forms/src/documents/pdf/services/parse-pdf-to-patterns.ts @@ -0,0 +1,39 @@ +import { type Result } from '@flexion/forms-common'; +import { extractFieldMetadata } from '../domain/field-extractor.js'; +import { + mapExtractedObjectToPatterns, + type ParsedPdf, +} from '../domain/pattern-mapper.js'; +import type { ParseError } from '../domain/types.js'; +import type { PdfParsingContext } from './context.js'; + +/** + * Main service function for parsing PDFs into pattern-based forms. + * Orchestrates the three-step process: + * 1. Extract field metadata from PDF (domain) + * 2. Parse PDF using injected parser (infrastructure) + * 3. Map to internal pattern representation (domain) + * + * @param context - Injected dependencies (parser, config) + * @param pdfBytes - Raw PDF file bytes + * @returns Result containing ParsedPdf or error + */ +export const parsePdfToPatterns = async ( + context: PdfParsingContext, + pdfBytes: Uint8Array +): Promise> => { + // Step 1: Extract field metadata + const metadataResult = await extractFieldMetadata(pdfBytes); + if (!metadataResult.success) { + return metadataResult; + } + + // Step 2: Parse PDF using injected parser + const parseResult = await context.parser.parse(pdfBytes, metadataResult.data); + if (!parseResult.success) { + return parseResult; + } + + // Step 3: Map to patterns + return mapExtractedObjectToPatterns(context.formConfig, parseResult.data); +}; diff --git a/packages/forms/src/documents/pdf/services/parser-interface.ts b/packages/forms/src/documents/pdf/services/parser-interface.ts new file mode 100644 index 00000000..407ba507 --- /dev/null +++ b/packages/forms/src/documents/pdf/services/parser-interface.ts @@ -0,0 +1,21 @@ +import type { Result } from '@flexion/forms-common'; +import type { FieldMetadata, ParseError } from '../domain/types.js'; +import type { ExtractedForm } from '../domain/schema.js'; + +/** + * Interface for PDF parsers. + * All parsers must output the ExtractedForm schema format, regardless of the underlying LLM. + */ +export interface PdfParser { + /** + * Parse a PDF with form fields into a structured guided interview format. + * + * @param pdfBytes - Raw PDF file bytes + * @param metadata - Field metadata extracted from the PDF + * @returns Result containing structured form data or error + */ + parse( + pdfBytes: Uint8Array, + metadata: FieldMetadata[] + ): Promise>; +} diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts index 9312eb9c..cf79f766 100644 --- a/packages/forms/src/index.ts +++ b/packages/forms/src/index.ts @@ -11,6 +11,11 @@ export * from './types.js'; export * from './util/base64.js'; export * from './patterns/address/jurisdictions.js'; export { type FormService, createFormService } from './services/index.js'; +export { + type FormStatusResponse, + type GetFormStatusError, +} from './services/get-form-status.js'; +export { type FormListItem } from './services/get-form-list.js'; export { defaultFormConfig, attachmentFileTypeOptions, @@ -23,10 +28,6 @@ import { type SequencePattern } from './patterns/sequence.js'; import { FieldsetPattern } from './patterns/index.js'; import { type FormSummaryPattern } from './patterns/form-summary/form-summary.js'; import { RepeaterPattern } from './patterns/index.js'; -export { - type FormRepository, - createFormsRepository, -} from './repository/index.js'; export { type FormRoute, type RouteData, diff --git a/packages/forms/src/llm/cache/backends/database.test.ts b/packages/forms/src/llm/cache/backends/database.test.ts new file mode 100644 index 00000000..f31a7cd0 --- /dev/null +++ b/packages/forms/src/llm/cache/backends/database.test.ts @@ -0,0 +1,69 @@ +import { expect, it } from 'vitest'; +import { + type DbTestContext, + describeDatabase, +} from '@flexion/forms-database/testing'; +import { DatabaseCache } from './database.js'; +import { testCacheSpecification } from '../cache-spec.js'; + +describeDatabase('DatabaseCache', () => { + it('passes shared cache specification', async ({ db }) => { + const cache = new DatabaseCache(db.ctx); + await testCacheSpecification(cache); + }); + + it('tracks access statistics on cache hits', async ({ + db, + }) => { + const cache = new DatabaseCache(db.ctx); + await cache.set('key', 'value'); + + const kysely = await db.ctx.getKysely(); + const initial = await kysely + .selectFrom('llm_request_cache') + .select('access_count') + .where('cache_key', '=', 'key') + .executeTakeFirst(); + + expect(initial?.access_count).toBe(1); + + // Trigger cache hit (stats update is async) + await cache.get('key'); + await new Promise(resolve => setTimeout(resolve, 100)); + + const updated = await kysely + .selectFrom('llm_request_cache') + .select('access_count') + .where('cache_key', '=', 'key') + .executeTakeFirst(); + + expect(updated?.access_count).toBe(2); + }, 10000); + + it('handles concurrent writes to same key', async ({ db }) => { + const cache = new DatabaseCache(db.ctx); + const key = 'concurrent'; + + // SQLite: sequential writes; Postgres: concurrent writes + if (db.engine === 'sqlite') { + for (let i = 0; i < 5; i++) { + await cache.set(key, `v${i}`); + } + } else { + await Promise.all( + Array.from({ length: 5 }, (_, i) => cache.set(key, `v${i}`)) + ); + } + + // Should have exactly one entry + const kysely = await db.ctx.getKysely(); + const count = await kysely + .selectFrom('llm_request_cache') + .select(kysely.fn.count('id').as('count')) + .where('cache_key', '=', key) + .executeTakeFirst(); + + expect(Number(count?.count)).toBe(1); + expect(await cache.get(key)).toMatch(/^v[0-4]$/); + }, 10000); +}); diff --git a/packages/forms/src/llm/cache/backends/database.ts b/packages/forms/src/llm/cache/backends/database.ts new file mode 100644 index 00000000..96e03170 --- /dev/null +++ b/packages/forms/src/llm/cache/backends/database.ts @@ -0,0 +1,78 @@ +import { sql } from 'kysely'; + +import { type DatabaseContext, dateValue } from '@flexion/forms-database'; +import type { AiRequestCache } from '../types.js'; + +/** + * Database-backed cache for production use. + * Stores AI responses in Postgres/SQLite with metadata. + * Tracks access statistics for analytics and cache optimization. + */ +export class DatabaseCache implements AiRequestCache { + constructor(private db: DatabaseContext) {} + + async get(key: string): Promise { + const kysely = await this.db.getKysely(); + + const result = await kysely + .selectFrom('llm_request_cache') + .select('response_data') + .where('cache_key', '=', key) + .executeTakeFirst(); + + if (result) { + // Update access statistics asynchronously (don't await to avoid blocking) + this.updateAccessStats(key).catch(error => { + console.warn('Failed to update cache access stats:', error); + }); + + return JSON.parse(result.response_data) as T; + } + + return null; + } + + async set(key: string, value: T): Promise { + const kysely = await this.db.getKysely(); + const now = dateValue(this.db.engine, new Date()); + + await kysely + .insertInto('llm_request_cache') + .values({ + cache_key: key, + response_data: JSON.stringify(value), + created_at: now, + accessed_at: now, + access_count: 1, + }) + .onConflict(oc => + oc.column('cache_key').doUpdateSet({ + response_data: JSON.stringify(value), + accessed_at: now, + }) + ) + .execute(); + } + + async clear(): Promise { + const kysely = await this.db.getKysely(); + await kysely.deleteFrom('llm_request_cache').execute(); + } + + /** + * Updates access statistics for a cache entry. + * Called asynchronously when a cache hit occurs. + */ + private async updateAccessStats(key: string): Promise { + const kysely = await this.db.getKysely(); + + await kysely + .updateTable('llm_request_cache') + .set({ + accessed_at: dateValue(this.db.engine, new Date()), + access_count: sql`access_count + 1`, + }) + .where('cache_key', '=', key) + .execute(); + } +} diff --git a/packages/forms/src/llm/cache/backends/filesystem.test.ts b/packages/forms/src/llm/cache/backends/filesystem.test.ts new file mode 100644 index 00000000..682e8c22 --- /dev/null +++ b/packages/forms/src/llm/cache/backends/filesystem.test.ts @@ -0,0 +1,81 @@ +import fs from 'fs/promises'; +import os from 'os'; +import path from 'path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { FilesystemCache } from './filesystem.js'; +import { testCacheSpecification } from '../cache-spec.js'; + +describe('FilesystemCache', () => { + let tempDir: string; + let cache: FilesystemCache; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cache-test-')); + cache = new FilesystemCache(tempDir); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + it('passes shared cache specification', async () => { + await testCacheSpecification(cache); + }); + + it('organizes cache files by prefix', async () => { + await cache.set('abcdef123', 'test-value'); + + const filePath = path.join(tempDir, 'ab', 'abcdef123.json'); + const exists = await fs + .access(filePath) + .then(() => true) + .catch(() => false); + + expect(exists).toBe(true); + }); + + it('stores JSON in minified format by default', async () => { + const key = 'minified'; + const value = { foo: 'bar', nested: { value: 123 } }; + + await cache.set(key, value); + + const filePath = path.join(tempDir, key.slice(0, 2), `${key}.json`); + const content = await fs.readFile(filePath, 'utf-8'); + + expect(content).not.toContain('\n '); + expect(JSON.parse(content)).toEqual(value); + }); + + it('stores JSON in pretty format when enabled', async () => { + const prettyCache = new FilesystemCache(tempDir, { pretty: true }); + const key = 'pretty'; + const value = { foo: 'bar' }; + + await prettyCache.set(key, value); + + const filePath = path.join(tempDir, key.slice(0, 2), `${key}.json`); + const content = await fs.readFile(filePath, 'utf-8'); + + expect(content).toContain('\n '); + expect(JSON.parse(content)).toEqual(value); + }); + + it('clear succeeds even if directory does not exist', async () => { + const nonExistentCache = new FilesystemCache( + path.join(tempDir, 'non-existent') + ); + + await expect(nonExistentCache.clear()).resolves.toBeUndefined(); + }); + + it('throws error for corrupt cache files', async () => { + const key = 'corrupt'; + const filePath = path.join(tempDir, key.slice(0, 2), `${key}.json`); + + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, 'invalid json', 'utf-8'); + + await expect(cache.get(key)).rejects.toThrow(SyntaxError); + }); +}); diff --git a/packages/forms/src/llm/cache/backends/filesystem.ts b/packages/forms/src/llm/cache/backends/filesystem.ts new file mode 100644 index 00000000..d152999d --- /dev/null +++ b/packages/forms/src/llm/cache/backends/filesystem.ts @@ -0,0 +1,77 @@ +import fs from 'fs/promises'; +import path from 'path'; +import type { AiRequestCache } from '../types.js'; + +/** + * Options for configuring filesystem cache behavior. + */ +export type FilesystemCacheOptions = { + /** + * Pretty-print JSON files for human readability. + * Useful for debugging and version control. + */ + pretty?: boolean; +}; + +/** + * Filesystem-backed cache for testing (VCR pattern). + * Stores responses as JSON files, keyed by request hash. + * Enables "record once, replay forever" testing workflow. + * + * Files are organized into subdirectories based on the first 2 characters + * of the cache key for better filesystem performance with many entries. + */ +export class FilesystemCache implements AiRequestCache { + constructor( + private basePath: string, + private options: FilesystemCacheOptions = {} + ) {} + + async get(key: string): Promise { + const filePath = this.getFilePath(key); + + try { + const data = await fs.readFile(filePath, 'utf-8'); + return JSON.parse(data) as T; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return null; + } + throw error; + } + } + + async set(key: string, value: T): Promise { + const filePath = this.getFilePath(key); + + // Ensure directory exists + await fs.mkdir(path.dirname(filePath), { recursive: true }); + + // Write with optional pretty formatting for readability + const json = this.options.pretty + ? JSON.stringify(value, null, 2) + : JSON.stringify(value); + + await fs.writeFile(filePath, json, 'utf-8'); + } + + async clear(): Promise { + try { + await fs.rm(this.basePath, { recursive: true, force: true }); + } catch (error) { + // Ignore if directory doesn't exist + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } + } + } + + /** + * Generates file path for a cache key. + * Organizes by first 2 chars for better filesystem performance. + */ + private getFilePath(key: string): string { + const prefix = key.slice(0, 2); + return path.join(this.basePath, prefix, `${key}.json`); + } +} diff --git a/packages/forms/src/llm/cache/backends/noop.ts b/packages/forms/src/llm/cache/backends/noop.ts new file mode 100644 index 00000000..5d5a0b6c --- /dev/null +++ b/packages/forms/src/llm/cache/backends/noop.ts @@ -0,0 +1,19 @@ +import type { AiRequestCache } from '../types.js'; + +/** + * No-operation cache that never stores or retrieves values. + * Use when caching should be disabled. + */ +export class NoOpCache implements AiRequestCache { + async get(_key: string): Promise { + return null; + } + + async set(_key: string, _value: T): Promise { + // No-op + } + + async clear(): Promise { + // No-op + } +} diff --git a/packages/forms/src/llm/cache/cache-spec.ts b/packages/forms/src/llm/cache/cache-spec.ts new file mode 100644 index 00000000..0bb6c497 --- /dev/null +++ b/packages/forms/src/llm/cache/cache-spec.ts @@ -0,0 +1,52 @@ +import { expect } from 'vitest'; +import type { AiRequestCache } from './types.js'; + +/** + * Shared specification for all cache implementations. + * Tests core functionality: get, set, clear, and basic edge cases. + */ +export async function testCacheSpecification(cache: AiRequestCache) { + // Non-existent key + expect(await cache.get('non-existent')).toBeNull(); + + // Basic string value + await cache.set('key1', 'value1'); + expect(await cache.get('key1')).toBe('value1'); + + // Complex object (tests JSON serialization) + const complexValue = { + text: 'Hello', + nested: { array: [1, 2, 3], nullValue: null, bool: true }, + }; + await cache.set('complex', complexValue); + expect(await cache.get('complex')).toEqual(complexValue); + + // Overwriting existing value + await cache.set('key1', 'updated'); + expect(await cache.get('key1')).toBe('updated'); + + // Multiple independent keys + await cache.set('a', 'value-a'); + await cache.set('b', 'value-b'); + expect(await cache.get('a')).toBe('value-a'); + expect(await cache.get('b')).toBe('value-b'); + + // Edge cases: empty string, null, zero, booleans + await cache.set('empty', ''); + await cache.set('null', null); + await cache.set('zero', 0); + await cache.set('true', true); + await cache.set('false', false); + + expect(await cache.get('empty')).toBe(''); + expect(await cache.get('null')).toBeNull(); + expect(await cache.get('zero')).toBe(0); + expect(await cache.get('true')).toBe(true); + expect(await cache.get('false')).toBe(false); + + // Clear removes all entries + await cache.clear(); + expect(await cache.get('key1')).toBeNull(); + expect(await cache.get('a')).toBeNull(); + expect(await cache.get('complex')).toBeNull(); +} diff --git a/packages/forms/src/llm/cache/hash.ts b/packages/forms/src/llm/cache/hash.ts new file mode 100644 index 00000000..92d12d6e --- /dev/null +++ b/packages/forms/src/llm/cache/hash.ts @@ -0,0 +1,107 @@ +import type { generateObject } from 'ai'; + +/** + * Computes a deterministic cache key for generateObject requests. + * Hashes all relevant parameters including file contents. + * + * @param params - Parameters passed to AI SDK's generateObject + * @returns SHA-256 hash string (64 hex characters) + */ +export const computeObjectCacheKey = async ( + params: Parameters[0] +): Promise => { + const keyComponents = { + model: extractModelId(params.model), + system: 'system' in params ? params.system : undefined, + messages: + 'messages' in params + ? await hashMessages(params.messages ?? []) + : undefined, + schema: 'schema' in params ? await hashSchema(params.schema) : undefined, + schemaName: 'schemaName' in params ? params.schemaName : undefined, + schemaDescription: + 'schemaDescription' in params ? params.schemaDescription : undefined, + temperature: 'temperature' in params ? params.temperature : undefined, + topP: 'topP' in params ? params.topP : undefined, + maxTokens: 'maxTokens' in params ? params.maxTokens : undefined, + }; + + return await sha256(JSON.stringify(keyComponents)); +}; + +/** + * Hashes messages, including file content hashes for determinism. + */ +const hashMessages = async (messages: any[]): Promise => { + const hashed = await Promise.all( + messages.map(async msg => { + if (Array.isArray(msg.content)) { + return { + role: msg.role, + content: await Promise.all( + msg.content.map(async (part: any) => { + if (part.type === 'file') { + return { + type: 'file', + hash: await sha256Buffer(part.data), + mimeType: part.mimeType, + }; + } + return part; + }) + ), + }; + } + return msg; + }) + ); + + return await sha256(JSON.stringify(hashed)); +}; + +/** + * Computes SHA-256 hash of a string using Web Crypto API. + */ +const sha256 = async (data: string): Promise => { + const buffer = new TextEncoder().encode(data); + const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', buffer); + return Array.from(new Uint8Array(hashBuffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +}; + +/** + * Computes SHA-256 hash of a buffer using Web Crypto API. + */ +const sha256Buffer = async ( + data: Uint8Array | ArrayBuffer | ArrayBufferLike +): Promise => { + // Create a new Uint8Array to ensure proper ArrayBuffer backing + const bytes = data instanceof Uint8Array ? data : new Uint8Array(data); + const normalizedData = new Uint8Array(bytes); + const hashBuffer = await globalThis.crypto.subtle.digest( + 'SHA-256', + normalizedData + ); + return Array.from(new Uint8Array(hashBuffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +}; + +/** + * Extracts model ID from AI SDK model object. + */ +const extractModelId = (model: any): string => { + // AI SDK model objects have modelId property + return model.modelId || model.id || String(model); +}; + +/** + * Hashes a Zod schema for cache key generation. + * Uses JSON representation for determinism. + */ +const hashSchema = async (schema: any): Promise => { + // Zod schemas have a _def property that contains the schema definition + // We use JSON.stringify on the entire schema object for simplicity + return await sha256(JSON.stringify(schema)); +}; diff --git a/packages/forms/src/llm/cache/index.ts b/packages/forms/src/llm/cache/index.ts new file mode 100644 index 00000000..d356c18f --- /dev/null +++ b/packages/forms/src/llm/cache/index.ts @@ -0,0 +1,5 @@ +export type { AiRequestCache, CacheMetadata } from './types.js'; +export { computeObjectCacheKey } from './hash.js'; +export { DatabaseCache } from './backends/database.js'; +export { FilesystemCache } from './backends/filesystem.js'; +export { NoOpCache } from './backends/noop.js'; diff --git a/packages/forms/src/llm/cache/types.ts b/packages/forms/src/llm/cache/types.ts new file mode 100644 index 00000000..b9f0f567 --- /dev/null +++ b/packages/forms/src/llm/cache/types.ts @@ -0,0 +1,36 @@ +/** + * Generic cache interface for any key-value storage. + * Implementations handle serialization/storage details. + */ +export interface AiRequestCache { + /** + * Retrieves a cached value by key. + * @param key - Cache key (typically a hash of request parameters) + * @returns The cached value, or null if not found + */ + get(key: string): Promise; + + /** + * Stores a value in the cache. + * @param key - Cache key (typically a hash of request parameters) + * @param value - Value to cache (will be JSON serialized) + */ + set(key: string, value: T): Promise; + + /** + * Clears all cached entries. + * Primarily intended for testing. + */ + clear(): Promise; +} + +/** + * Metadata stored alongside cached responses for debugging and analytics. + */ +export type CacheMetadata = { + modelId: string; + requestHash: string; + createdAt: Date; + accessedAt: Date; + accessCount: number; +}; diff --git a/packages/forms/src/llm/index.ts b/packages/forms/src/llm/index.ts new file mode 100644 index 00000000..41428c9b --- /dev/null +++ b/packages/forms/src/llm/index.ts @@ -0,0 +1,24 @@ +// Cache implementations +export type { AiRequestCache, CacheMetadata } from './cache/index.js'; +export { + DatabaseCache, + FilesystemCache, + NoOpCache, + computeObjectCacheKey, +} from './cache/index.js'; + +// Provider factories +export { + createBedrockModel, + DEFAULT_BEDROCK_CONFIG, + type BedrockConfig, +} from './providers/index.js'; + +// Services and context +export { + type LlmContext, + createProductionLlmContext, + createTestLlmContext, + createNoopLlmContext, + generateObjectCached, +} from './services/index.js'; diff --git a/packages/forms/src/llm/providers/bedrock.ts b/packages/forms/src/llm/providers/bedrock.ts new file mode 100644 index 00000000..23526a66 --- /dev/null +++ b/packages/forms/src/llm/providers/bedrock.ts @@ -0,0 +1,52 @@ +import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; +import type { AwsCredentialIdentityProvider } from '@aws-sdk/types'; +import type { LanguageModel } from 'ai'; + +/** + * Configuration for AWS Bedrock provider. + */ +export type BedrockConfig = { + modelId: string; + region: string; + /** + * Optional credential provider for AWS authentication. + * Defaults to fromNodeProviderChain() which automatically handles: + * - Environment variables (local development) + * - IAM roles (App Runner, ECS, EKS, EC2) + * - Shared credentials file + */ + credentialProvider?: AwsCredentialIdentityProvider; +}; + +/** + * Default Bedrock configuration using Claude Sonnet 4.5. + */ +export const DEFAULT_BEDROCK_CONFIG: BedrockConfig = { + modelId: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', + region: 'us-east-1', +}; + +/** + * Creates a Bedrock language model configured for the specified model. + * + * @param config - Optional partial configuration (merged with defaults) + * @returns AI SDK LanguageModel instance + * + * @example + * ```typescript + * const model = createBedrockModel(); + * const result = await generateObject({ model, ... }); + * ``` + */ +export const createBedrockModel = ( + config: Partial = {} +): LanguageModel => { + const { + modelId, + region, + credentialProvider = fromNodeProviderChain(), + } = { ...DEFAULT_BEDROCK_CONFIG, ...config }; + const bedrock = createAmazonBedrock({ region, credentialProvider }); + return bedrock(modelId); +}; diff --git a/packages/forms/src/llm/providers/index.ts b/packages/forms/src/llm/providers/index.ts new file mode 100644 index 00000000..885b62ef --- /dev/null +++ b/packages/forms/src/llm/providers/index.ts @@ -0,0 +1,5 @@ +export { + createBedrockModel, + DEFAULT_BEDROCK_CONFIG, + type BedrockConfig, +} from './bedrock.js'; diff --git a/packages/forms/src/llm/services/context.ts b/packages/forms/src/llm/services/context.ts new file mode 100644 index 00000000..baab547d --- /dev/null +++ b/packages/forms/src/llm/services/context.ts @@ -0,0 +1,76 @@ +import type { DatabaseContext } from '@flexion/forms-database'; +import type { AiRequestCache } from '../cache/types.js'; +import { DatabaseCache } from '../cache/backends/database.js'; +import { FilesystemCache } from '../cache/backends/filesystem.js'; +import { NoOpCache } from '../cache/backends/noop.js'; +import { getDefaultCachePath } from '../../util/workspace-root.js'; + +/** + * Context for LLM operations. + * Provides cache and configuration for all LLM service calls. + */ +export type LlmContext = { + /** + * Cache implementation for storing and retrieving LLM responses. + */ + cache: AiRequestCache; +}; + +/** + * Creates a production LLM context with database-backed caching. + * + * @param dbContext - Database context for persistent storage + * @returns LlmContext configured for production use + * + * @example + * ```typescript + * const dbContext = await createPostgresDatabaseContext(config); + * const llmContext = createProductionLlmContext(dbContext); + * ``` + */ +export const createProductionLlmContext = ( + dbContext: DatabaseContext +): LlmContext => ({ + cache: new DatabaseCache(dbContext), +}); + +/** + * Creates a test LLM context with filesystem-backed caching (VCR pattern). + * Enables "record once, replay forever" testing workflow. + * + * By default, uses a shared cache directory at the workspace root to ensure + * all tests and CLI tools can share cached responses. + * + * @param cachePath - Directory path for storing cached responses (defaults to workspace root) + * @param pretty - Whether to pretty-print JSON files (default: true) + * @returns LlmContext configured for testing + * + * @example + * ```typescript + * const llmContext = createTestLlmContext(); + * // First run: records live API response to workspace root cache + * // Subsequent runs: replays from shared cache, no API calls + * ``` + */ +export const createTestLlmContext = ( + cachePath?: string, + pretty: boolean = true +): LlmContext => ({ + cache: new FilesystemCache(cachePath ?? getDefaultCachePath(), { pretty }), +}); + +/** + * Creates an LLM context with caching disabled. + * Useful for scenarios where caching is undesirable. + * + * @returns LlmContext with no-op cache + * + * @example + * ```typescript + * const llmContext = createNoopLlmContext(); + * // Every call results in a live API request + * ``` + */ +export const createNoopLlmContext = (): LlmContext => ({ + cache: new NoOpCache(), +}); diff --git a/packages/forms/src/llm/services/generate-object.ts b/packages/forms/src/llm/services/generate-object.ts new file mode 100644 index 00000000..1c39a157 --- /dev/null +++ b/packages/forms/src/llm/services/generate-object.ts @@ -0,0 +1,56 @@ +import { generateObject } from 'ai'; +import type { LlmContext } from './context.js'; +import { computeObjectCacheKey } from '../cache/hash.js'; +import type { ZodType } from 'zod'; + +/** + * Cached wrapper around AI SDK's generateObject. + * Automatically checks cache before making live LLM requests. + * + * Cache keys are computed from all request parameters including: + * - Model ID + * - System prompt + * - Messages (with file content hashes) + * - Schema definition + * - Generation parameters (temperature, topP, etc.) + * + * @param context - LLM context with cache implementation + * @param params - Parameters to pass to AI SDK's generateObject + * @returns Generated object result (from cache or live API) + * + * @example + * ```typescript + * const result = await generateObjectCached(llmContext, { + * model: bedrockModel, + * schema: MySchema, + * messages: [{ role: 'user', content: 'Extract this data...' }], + * }); + * ``` + */ +export const generateObjectCached = async ( + context: LlmContext, + params: Parameters>[0] +): Promise>>> => { + // Compute deterministic cache key from all parameters + const cacheKey = await computeObjectCacheKey(params); + + // Try cache first + const cached = + await context.cache.get>>>( + cacheKey + ); + + if (cached) { + console.log('[LLM Cache] Hit:', cacheKey.slice(0, 16)); + return cached; + } + + // Cache miss - make live request + console.log('[LLM Cache] Miss:', cacheKey.slice(0, 16)); + const result = await generateObject(params); + + // Store for future requests + await context.cache.set(cacheKey, result); + + return result; +}; diff --git a/packages/forms/src/llm/services/index.ts b/packages/forms/src/llm/services/index.ts new file mode 100644 index 00000000..df3ee805 --- /dev/null +++ b/packages/forms/src/llm/services/index.ts @@ -0,0 +1,7 @@ +export { + type LlmContext, + createProductionLlmContext, + createTestLlmContext, + createNoopLlmContext, +} from './context.js'; +export { generateObjectCached } from './generate-object.js'; diff --git a/packages/forms/src/pattern.ts b/packages/forms/src/pattern.ts index 11da11c0..606ee8f3 100644 --- a/packages/forms/src/pattern.ts +++ b/packages/forms/src/pattern.ts @@ -1,5 +1,4 @@ -import * as r from '@gsa-tts/forms-common'; -import set from 'set-value'; +import * as r from '@flexion/forms-common'; import { type CreatePrompt } from './components.js'; import { type FormError, type FormErrors } from './error.js'; @@ -249,6 +248,9 @@ export const getFirstPattern = ( ): Pattern => { if (!pattern) { pattern = form.patterns[form.root]; + if (!pattern) { + throw new Error(`Root pattern with id ${form.root} not found`); + } } const elemConfig = getPatternConfig(config, pattern.type); const children = elemConfig.getChildren(pattern, form.patterns); diff --git a/packages/forms/src/patterns/attachment/config.ts b/packages/forms/src/patterns/attachment/config.ts index 0533e4c8..29d61401 100644 --- a/packages/forms/src/patterns/attachment/config.ts +++ b/packages/forms/src/patterns/attachment/config.ts @@ -1,16 +1,23 @@ import { z } from 'zod'; -import { enLocale as message } from '@gsa-tts/forms-common'; +import { enLocale as message } from '@flexion/forms-common'; import { ParsePatternConfigData, type Pattern } from '../../pattern.js'; import { safeZodParseFormErrors } from '../../util/zod.js'; -import { attachmentFileTypeMimes } from './file-type-options'; +import { attachmentFileTypeMimes } from './file-type-options.js'; export type AttachmentPattern = Pattern; export const configSchema = z.object({ label: z.string().min(1, message.patterns.attachment.fieldLabelRequired), required: z.boolean(), - maxAttachments: z.coerce.number().int().gt(0), - maxFileSizeMB: z.coerce.number().int().gt(1).lte(10), + maxAttachments: z.coerce + .number() + .int() + .gt(0, { message: 'Number must be greater than 0' }), + maxFileSizeMB: z.coerce + .number() + .int() + .gt(1) + .lte(10, { message: 'Number must be less than or equal to 10' }), allowedFileTypes: z.union([ z .array( diff --git a/packages/forms/src/patterns/attachment/index.ts b/packages/forms/src/patterns/attachment/index.ts index e89c17a7..f1c887b6 100644 --- a/packages/forms/src/patterns/attachment/index.ts +++ b/packages/forms/src/patterns/attachment/index.ts @@ -1,11 +1,11 @@ -import { enLocale as message } from '@gsa-tts/forms-common'; +import { enLocale as message } from '@flexion/forms-common'; import { type PatternConfig } from '../../pattern.js'; import { parseConfigData, type AttachmentPattern } from './config.js'; import { createPrompt } from './prompt.js'; import { type AttachmentPatternOutput, parseUserInput } from './response.js'; -import { attachmentFileTypeMimes } from './file-type-options'; +import { attachmentFileTypeMimes } from './file-type-options.js'; export const attachmentConfig: PatternConfig< AttachmentPattern, diff --git a/packages/forms/src/patterns/input/config.ts b/packages/forms/src/patterns/input/config.ts index 5ab8f627..94691012 100644 --- a/packages/forms/src/patterns/input/config.ts +++ b/packages/forms/src/patterns/input/config.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { enLocale as message } from '@gsa-tts/forms-common'; +import { enLocale as message } from '@flexion/forms-common'; import { type ParsePatternConfigData, type Pattern } from '../../pattern.js'; import { safeZodParseFormErrors } from '../../util/zod.js'; diff --git a/packages/forms/src/patterns/input/index.ts b/packages/forms/src/patterns/input/index.ts index c45ae93e..f19726a7 100644 --- a/packages/forms/src/patterns/input/index.ts +++ b/packages/forms/src/patterns/input/index.ts @@ -1,4 +1,4 @@ -import { enLocale as message } from '@gsa-tts/forms-common'; +import { enLocale as message } from '@flexion/forms-common'; import { type PatternConfig } from '../../pattern.js'; diff --git a/packages/forms/src/patterns/name/name.test.ts b/packages/forms/src/patterns/name/name.test.ts index 75ac92d2..856c62e2 100644 --- a/packages/forms/src/patterns/name/name.test.ts +++ b/packages/forms/src/patterns/name/name.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createNameSchema, nameConfig, type NamePattern } from './index'; +import { createNameSchema, nameConfig, type NamePattern } from './index.js'; describe('NamePattern tests', () => { describe('createNameSchema', () => { diff --git a/packages/forms/src/patterns/package-download/submit.test.ts b/packages/forms/src/patterns/package-download/submit.test.ts index 26f19fab..dde8c08e 100644 --- a/packages/forms/src/patterns/package-download/submit.test.ts +++ b/packages/forms/src/patterns/package-download/submit.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { failure, success } from '@gsa-tts/forms-common'; +import { failure, success } from '@flexion/forms-common'; import { type Blueprint, type FormSession, defaultFormConfig } from '../..'; diff --git a/packages/forms/src/patterns/package-download/submit.ts b/packages/forms/src/patterns/package-download/submit.ts index 36d223e3..2db6040f 100644 --- a/packages/forms/src/patterns/package-download/submit.ts +++ b/packages/forms/src/patterns/package-download/submit.ts @@ -1,11 +1,11 @@ -import { failure, success, type Result } from '@gsa-tts/forms-common'; +import { failure, success, type Result } from '@flexion/forms-common'; -import { type Blueprint, type FormOutput } from '../..'; -import { createFormOutputFieldData, fillPDF } from '../../documents'; -import { sessionIsComplete } from '../../session'; -import { type SubmitHandler } from '../../submission'; +import { type FormOutput } from '../../index.js'; +import { createFormOutputFieldData, fillPDF } from '../../documents/index.js'; +import { sessionIsComplete } from '../../session.js'; +import { type SubmitHandler } from '../../submission.js'; -import { type PackageDownloadPattern } from './index'; +import { type PackageDownloadPattern } from './index.js'; export const downloadPackageHandler: SubmitHandler< PackageDownloadPattern diff --git a/packages/forms/src/patterns/page-set/submit.test.ts b/packages/forms/src/patterns/page-set/submit.test.ts index 24b9cc17..458e49af 100644 --- a/packages/forms/src/patterns/page-set/submit.test.ts +++ b/packages/forms/src/patterns/page-set/submit.test.ts @@ -7,7 +7,7 @@ import { createFormSession } from '../../session'; import { PageSet } from './builder'; import { submitPage } from './submit'; -import { success } from '@gsa-tts/forms-common'; +import { success } from '@flexion/forms-common'; describe('Page-set submission', () => { it('stores session data for valid page data', async () => { diff --git a/packages/forms/src/patterns/page-set/submit.ts b/packages/forms/src/patterns/page-set/submit.ts index c9d082b4..dad1eb6d 100644 --- a/packages/forms/src/patterns/page-set/submit.ts +++ b/packages/forms/src/patterns/page-set/submit.ts @@ -1,14 +1,14 @@ -import { failure, success } from '@gsa-tts/forms-common'; +import { failure, success } from '@flexion/forms-common'; import { getPatternConfig, getPatternSafely, aggregatePatternSessionValues, } from '../../pattern.js'; -import { type FormSession } from '../../session'; -import { type SubmitHandler } from '../../submission'; -import { type PagePattern } from '../page/config'; -import { type PageSetPattern } from './config'; +import { type FormSession } from '../../session.js'; +import { type SubmitHandler } from '../../submission.js'; +import { type PagePattern } from '../page/config.js'; +import { type PageSetPattern } from './config.js'; const getPage = (formSession: FormSession) => { const page = formSession.route?.params.page?.toString(); diff --git a/packages/forms/src/patterns/phone-number/phone-number.ts b/packages/forms/src/patterns/phone-number/phone-number.ts index 4804f5f4..4e2f948e 100644 --- a/packages/forms/src/patterns/phone-number/phone-number.ts +++ b/packages/forms/src/patterns/phone-number/phone-number.ts @@ -22,19 +22,37 @@ export type PhoneNumberPatternOutput = z.infer< export const createPhoneSchema = (data: PhoneNumberPattern['data']) => { const phoneSchema = z .string() - .regex(/^(\d{3}-\d{3}-\d{4}|\d{10})$/, 'Invalid phone number format') - .transform(value => { + .superRefine((value, ctx) => { + // Allow empty string if not required + if (value === '' && !data.required) { + return; + } + + // Validate format + if (!/^(\d{3}-\d{3}-\d{4}|\d{10})$/.test(value)) { + ctx.addIssue({ + code: 'custom', + message: 'Invalid phone number format', + }); + return; + } + + // Validate length const digits = value.replace(/[^\d]/g, ''); - return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`; + if (digits.length !== 10) { + ctx.addIssue({ + code: 'custom', + message: 'Invalid phone number format', + }); + } }) - .refine(value => { + .transform(value => { + if (value === '') { + return value; + } const digits = value.replace(/[^\d]/g, ''); - return digits.length === 10; - }, 'Phone number must contain exactly 10 digits'); - - if (!data.required) { - return z.union([z.literal(''), phoneSchema]); - } + return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`; + }); return phoneSchema; }; @@ -62,7 +80,7 @@ export const phoneNumberConfig: PatternConfig< return []; }, - createPrompt(_, session, pattern, options) { + createPrompt(_, session, pattern) { const sessionValue = getFormSessionValue(session, pattern.id); const sessionError = getFormSessionError(session, pattern.id); diff --git a/packages/forms/src/patterns/radio-group.ts b/packages/forms/src/patterns/radio-group.ts index 89bd8110..d23c5797 100644 --- a/packages/forms/src/patterns/radio-group.ts +++ b/packages/forms/src/patterns/radio-group.ts @@ -1,6 +1,6 @@ import * as z from 'zod'; -import { Result } from '@gsa-tts/forms-common'; +import { Result } from '@flexion/forms-common'; import { type RadioGroupProps } from '../components.js'; import { type FormError } from '../error.js'; diff --git a/packages/forms/src/patterns/repeater/prompt.ts b/packages/forms/src/patterns/repeater/prompt.ts index 1ef47ee2..b7750a52 100644 --- a/packages/forms/src/patterns/repeater/prompt.ts +++ b/packages/forms/src/patterns/repeater/prompt.ts @@ -1,6 +1,10 @@ import { type RepeaterPattern } from './index.js'; import { getFormSessionError } from '../../session.js'; -import { createPromptForPattern, type CreatePrompt, type RepeaterProps } from '../../components.js'; +import { + createPromptForPattern, + type CreatePrompt, + type RepeaterProps, +} from '../../components.js'; import { getPattern } from '../../pattern.js'; export const createPrompt: CreatePrompt = ( diff --git a/packages/forms/src/patterns/repeater/submit.ts b/packages/forms/src/patterns/repeater/submit.ts index 53d7139a..18fd875a 100644 --- a/packages/forms/src/patterns/repeater/submit.ts +++ b/packages/forms/src/patterns/repeater/submit.ts @@ -1,7 +1,7 @@ -import { success } from '@gsa-tts/forms-common'; +import { success } from '@flexion/forms-common'; -import { type RepeaterPattern } from '../..'; -import { type SubmitHandler } from '../../submission'; +import { type RepeaterPattern } from '../../index.js'; +import { type SubmitHandler } from '../../submission.js'; export const repeaterAddRowHandler: SubmitHandler = async ( context, @@ -15,7 +15,7 @@ export const repeaterAddRowHandler: SubmitHandler = async ( : []; const initialRepeaterRowData = opts.pattern.data.patterns.reduce( - (acc, patternId: string) => { + (acc, patternId) => { // THIS requires all the patterns to have object not string input values // acc[patternId] = {}; diff --git a/packages/forms/src/patterns/text-area/text-area.ts b/packages/forms/src/patterns/text-area/text-area.ts index 1d17b55a..4796fd7d 100644 --- a/packages/forms/src/patterns/text-area/text-area.ts +++ b/packages/forms/src/patterns/text-area/text-area.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { enLocale as message } from '@gsa-tts/forms-common'; +import { enLocale as message } from '@flexion/forms-common'; import { PatternBuilder, type Pattern, diff --git a/packages/forms/src/repository/add-document.test.ts b/packages/forms/src/repository/add-document.test.ts index a0a77daf..216d1152 100644 --- a/packages/forms/src/repository/add-document.test.ts +++ b/packages/forms/src/repository/add-document.test.ts @@ -3,7 +3,7 @@ import { beforeAll, expect, it, vi } from 'vitest'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { addDocument } from './add-document.js'; import type { ParsedPdf } from '../documents/pdf/parsing-api.js'; import type { DocumentFieldMap } from '../documents/types.js'; diff --git a/packages/forms/src/repository/add-document.ts b/packages/forms/src/repository/add-document.ts index 22259e80..b7ad6849 100644 --- a/packages/forms/src/repository/add-document.ts +++ b/packages/forms/src/repository/add-document.ts @@ -1,15 +1,15 @@ -import { type Result, failure, success } from '@gsa-tts/forms-common'; +import { type Result, failure, success } from '@flexion/forms-common'; -import type { ParsedPdf } from '../documents/pdf/parsing-api'; -import type { DocumentFieldMap } from '../documents/types'; -import type { FormRepositoryContext } from '.'; +import type { ParsedPdf } from '../documents/pdf/parsing-api.js'; +import type { DocumentFieldMap } from '../documents/types.js'; +import type { FormRepositoryContext } from './index.js'; export type AddDocument = ( ctx: FormRepositoryContext, document: { fileName: string; data: Uint8Array; - extract: { + extract?: { parsedPdf: ParsedPdf; fields: DocumentFieldMap; }; @@ -30,7 +30,7 @@ export const addDocument: AddDocument = async (ctx, document) => { type: 'pdf', file_name: document.fileName, data: Buffer.from(document.data), - extract: JSON.stringify(document.extract), + extract: document.extract ? JSON.stringify(document.extract) : '', }) .execute() .then(() => diff --git a/packages/forms/src/repository/add-form.test.ts b/packages/forms/src/repository/add-form.test.ts index 8d68e15a..1a894a5a 100644 --- a/packages/forms/src/repository/add-form.test.ts +++ b/packages/forms/src/repository/add-form.test.ts @@ -3,7 +3,7 @@ import { beforeAll, expect, it, vi } from 'vitest'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { addForm } from './add-form.js'; import { defaultFormConfig } from '../patterns/index.js'; diff --git a/packages/forms/src/repository/add-form.ts b/packages/forms/src/repository/add-form.ts index ed975bfc..3a98edbc 100644 --- a/packages/forms/src/repository/add-form.ts +++ b/packages/forms/src/repository/add-form.ts @@ -1,4 +1,4 @@ -import { type Result, failure, success } from '@gsa-tts/forms-common'; +import { type Result, failure, success } from '@flexion/forms-common'; import { type Blueprint } from '../index.js'; import type { FormRepositoryContext } from './index.js'; diff --git a/packages/forms/src/repository/delete-form.test.ts b/packages/forms/src/repository/delete-form.test.ts index eae216e8..d916c659 100644 --- a/packages/forms/src/repository/delete-form.test.ts +++ b/packages/forms/src/repository/delete-form.test.ts @@ -1,10 +1,10 @@ import { beforeAll, expect, it, vi } from 'vitest'; -import type { Result } from '@gsa-tts/forms-common'; +import type { Result } from '@flexion/forms-common'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { createTestBlueprint } from '../builder/builder.test.js'; import type { Blueprint } from '../types.js'; diff --git a/packages/forms/src/repository/delete-form.ts b/packages/forms/src/repository/delete-form.ts index a90bb091..a28243ab 100644 --- a/packages/forms/src/repository/delete-form.ts +++ b/packages/forms/src/repository/delete-form.ts @@ -1,7 +1,7 @@ -import { type VoidResult, failure, voidSuccess } from '@gsa-tts/forms-common'; +import { type VoidResult, failure, voidSuccess } from '@flexion/forms-common'; -import type { FormOutput } from '../types'; -import type { FormRepositoryContext } from '.'; +import type { FormOutput } from '../types.js'; +import type { FormRepositoryContext } from './index.js'; export type DeleteForm = ( ctx: FormRepositoryContext, @@ -39,7 +39,7 @@ export const deleteForm: DeleteForm = async (ctx, formId) => { .where('id', 'in', documentIds) .execute() .then(_ => voidSuccess) - .catch((error: Error) => { + .catch(error => { return failure({ message: error.message, code: 'unknown' as const }); }); }); diff --git a/packages/forms/src/repository/form-jobs.test.ts b/packages/forms/src/repository/form-jobs.test.ts new file mode 100644 index 00000000..8a02f9d4 --- /dev/null +++ b/packages/forms/src/repository/form-jobs.test.ts @@ -0,0 +1,273 @@ +import { beforeAll, expect, it, vi } from 'vitest'; + +import { + type DbTestContext, + describeDatabase, +} from '@flexion/forms-database/testing'; +import { createFormJob } from './jobs/create-form-job.js'; +import { completeFormJob } from './jobs/complete-form-job.js'; +import { failFormJob } from './jobs/fail-form-job.js'; +import { getLatestFormJob } from './jobs/get-latest-form-job.js'; +import { getFormJobs } from './jobs/get-form-jobs.js'; +import { addForm } from './add-form.js'; +import { defaultFormConfig } from '../patterns/index.js'; + +describeDatabase('form jobs repository', getDb => { + const today = new Date(2000, 1, 1); + + beforeAll(async () => { + vi.setSystemTime(today); + }); + + it('creates a job in processing state', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + // Create a form first + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + // Create a job + const jobResult = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + metadata: { + documentId: 'doc-1', + fileName: 'test.pdf', + userId: 'user-1', + }, + }); + + if (!jobResult.success) { + console.error('Job creation failed:', jobResult.error); + expect.fail(`createFormJob failed: ${jobResult.error}`); + } + expect(jobResult.success).toBe(true); + + expect(jobResult.data.status).toBe('processing'); + expect(jobResult.data.jobType).toBe('import-pdf'); + expect(jobResult.data.formId).toBe(formResult.data.id); + expect(jobResult.data.metadata).toEqual({ + documentId: 'doc-1', + fileName: 'test.pdf', + userId: 'user-1', + }); + }); + + it('completes a job with result data', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + // Create a form + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + // Create a job + const jobResult = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + if (!jobResult.success) { + expect.fail('createFormJob failed'); + } + + // Complete the job + const completeResult = await completeFormJob(ctx, jobResult.data.id, { + patternsAdded: 5, + fieldsExtracted: 12, + documentId: 'doc-1', + }); + + expect(completeResult.success).toBe(true); + + // Verify the job was updated + const latestResult = await getLatestFormJob( + ctx, + formResult.data.id, + 'import-pdf' + ); + if (!latestResult.success || !latestResult.data) { + expect.fail('getLatestFormJob failed'); + } + + expect(latestResult.data.status).toBe('completed'); + expect(latestResult.data.result).toEqual({ + patternsAdded: 5, + fieldsExtracted: 12, + documentId: 'doc-1', + }); + expect(latestResult.data.completedAt).toBeDefined(); + }); + + it('fails a job with error message', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + // Create a form + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + // Create a job + const jobResult = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + if (!jobResult.success) { + expect.fail('createFormJob failed'); + } + + // Fail the job + const failResult = await failFormJob(ctx, jobResult.data.id, { + message: 'PDF parsing failed', + stack: 'Error stack trace', + }); + + expect(failResult.success).toBe(true); + + // Verify the job was updated + const latestResult = await getLatestFormJob( + ctx, + formResult.data.id, + 'import-pdf' + ); + if (!latestResult.success || !latestResult.data) { + expect.fail('getLatestFormJob failed'); + } + + expect(latestResult.data.status).toBe('failed'); + expect(latestResult.data.errorMessage).toBe('PDF parsing failed'); + expect(latestResult.data.errorStack).toBe('Error stack trace'); + expect(latestResult.data.completedAt).toBeDefined(); + }); + + it('returns null when no job exists', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + // Create a form + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + // Try to get a job that doesn't exist + const latestResult = await getLatestFormJob( + ctx, + formResult.data.id, + 'import-pdf' + ); + + expect(latestResult.success).toBe(true); + if (!latestResult.success) { + expect.fail('getLatestFormJob failed'); + } + expect(latestResult.data).toBeNull(); + }); + + it('returns job history in chronological order', async ({ + db, + }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + // Create a form + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + // Create 3 jobs with different timestamps + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 0)); + const job1 = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + expect(job1.success).toBe(true); + + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 1)); + const job2 = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + expect(job2.success).toBe(true); + + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 2)); + const job3 = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'validate-schema', + metadata: { + validatorVersion: '1.0', + userId: 'user-1', + }, + }); + expect(job3.success).toBe(true); + + // Get all jobs + const historyResult = await getFormJobs(ctx, formResult.data.id); + if (!historyResult.success) { + expect.fail('getFormJobs failed'); + } + + expect(historyResult.data.length).toBe(3); + + // Should be ordered newest first + expect(historyResult.data[0].createdAt.getTime()).toBeGreaterThan( + historyResult.data[1].createdAt.getTime() + ); + expect(historyResult.data[1].createdAt.getTime()).toBeGreaterThan( + historyResult.data[2].createdAt.getTime() + ); + + // Verify the last one is the validate-schema job + expect(historyResult.data[0].jobType).toBe('validate-schema'); + }); + + it('gets only the latest job of a specific type', async ({ + db, + }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + // Create a form + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + // Create 2 import-pdf jobs + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 0)); + await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 1)); + const job2 = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + if (!job2.success) { + expect.fail('createFormJob failed'); + } + + // Get latest import-pdf job + const latestResult = await getLatestFormJob( + ctx, + formResult.data.id, + 'import-pdf' + ); + if (!latestResult.success || !latestResult.data) { + expect.fail('getLatestFormJob failed'); + } + + // Should be the second job + expect(latestResult.data.id).toBe(job2.data.id); + }); +}); + +const testForm = { + summary: { title: 'Test form', description: 'Test description' }, + root: 'root', + patterns: { root: { type: 'sequence', id: 'root', data: { patterns: [] } } }, + outputs: [], +}; diff --git a/packages/forms/src/repository/form-jobs.ts b/packages/forms/src/repository/form-jobs.ts new file mode 100644 index 00000000..cd5d4d3d --- /dev/null +++ b/packages/forms/src/repository/form-jobs.ts @@ -0,0 +1,249 @@ +import { type Result, success, failure } from '@flexion/forms-common'; +import type { FormRepositoryContext } from './index.js'; +import { dateValue } from '@flexion/forms-database'; + +// Type-safe job metadata by job type +export type JobMetadata = { + 'import-pdf': { + documentId: string; + fileName: string; + userId: string; + }; + 'validate-schema': { + validatorVersion: string; + userId: string; + }; + publish: { + targetEnvironment: 'staging' | 'production'; + publisherId: string; + }; +}; + +// Type-safe job results by job type +export type JobResult = { + 'import-pdf': { + patternsAdded: number; + fieldsExtracted: number; + documentId: string; + }; + 'validate-schema': { + errorsFound: number; + warningsFound: number; + issues: Array<{ field: string; message: string }>; + }; + publish: { + publishedAt: string; + url: string; + }; +}; + +export type JobType = keyof JobMetadata; +export type JobStatus = 'pending' | 'processing' | 'completed' | 'failed'; + +export type FormJob = { + id: string; + formId: string; + jobType: T; + status: JobStatus; + createdAt: Date; + startedAt?: Date; + completedAt?: Date; + errorMessage?: string; + errorStack?: string; + metadata?: JobMetadata[T]; + result?: JobResult[T]; +}; + +/** + * Create a new job record in 'processing' state. + * Call this before starting async work. + */ +export type CreateFormJob = ( + ctx: FormRepositoryContext, + params: { + formId: string; + jobType: T; + metadata?: JobMetadata[T]; + } +) => Promise, string>>; + +export const createFormJob: CreateFormJob = async (ctx, params) => { + const uuid = crypto.randomUUID(); + const db = await ctx.db.getKysely(); + + try { + const now = new Date(); + await db + .insertInto('form_jobs') + .values({ + id: uuid, + form_id: params.formId, + job_type: params.jobType, + status: 'processing', + created_at: dateValue(ctx.db.engine, now), + started_at: dateValue(ctx.db.engine, now), + metadata: params.metadata ? JSON.stringify(params.metadata) : null, + }) + .execute(); + + return success({ + id: uuid, + formId: params.formId, + jobType: params.jobType, + status: 'processing' as JobStatus, + createdAt: new Date(), + startedAt: new Date(), + metadata: params.metadata, + }) as any; + } catch (err) { + return failure(`Failed to create job: ${(err as Error).message}`); + } +}; + +/** + * Mark a job as completed with result data. + */ +export type CompleteFormJob = ( + ctx: FormRepositoryContext, + jobId: string, + result?: JobResult[T] +) => Promise>; + +export const completeFormJob: CompleteFormJob = async (ctx, jobId, result) => { + const db = await ctx.db.getKysely(); + + try { + await db + .updateTable('form_jobs') + .set({ + status: 'completed', + completed_at: new Date().toISOString() as any, + result: result ? JSON.stringify(result) : null, + }) + .where('id', '=', jobId) + .execute(); + + return success(undefined); + } catch (err) { + return failure(`Failed to complete job: ${(err as Error).message}`); + } +}; + +/** + * Mark a job as failed with error information. + */ +export type FailFormJob = ( + ctx: FormRepositoryContext, + jobId: string, + error: { message: string; stack?: string } +) => Promise>; + +export const failFormJob: FailFormJob = async (ctx, jobId, error) => { + const db = await ctx.db.getKysely(); + + try { + await db + .updateTable('form_jobs') + .set({ + status: 'failed', + completed_at: new Date().toISOString() as any, + error_message: error.message, + error_stack: error.stack || null, + }) + .where('id', '=', jobId) + .execute(); + + return success(undefined); + } catch (err) { + return failure(`Failed to fail job: ${(err as Error).message}`); + } +}; + +/** + * Get the latest job for a form of a specific type. + * Useful for status polling: "What's the current import-pdf job status?" + */ +export type GetLatestFormJob = ( + ctx: FormRepositoryContext, + formId: string, + jobType: T +) => Promise | null, string>>; + +export const getLatestFormJob: GetLatestFormJob = async ( + ctx, + formId, + jobType +) => { + const db = await ctx.db.getKysely(); + + try { + const row = await db + .selectFrom('form_jobs') + .selectAll() + .where('form_id', '=', formId) + .where('job_type', '=', jobType) + .orderBy('created_at', 'desc') + .limit(1) + .executeTakeFirst(); + + if (!row) { + return success(null); + } + + return success({ + id: row.id, + formId: row.form_id, + jobType: row.job_type as JobType, + status: row.status as JobStatus, + createdAt: new Date(row.created_at), + startedAt: row.started_at ? new Date(row.started_at) : undefined, + completedAt: row.completed_at ? new Date(row.completed_at) : undefined, + errorMessage: row.error_message || undefined, + errorStack: row.error_stack || undefined, + metadata: row.metadata ? JSON.parse(row.metadata) : undefined, + result: row.result ? JSON.parse(row.result) : undefined, + }) as any; + } catch (err) { + return failure(`Failed to get latest job: ${(err as Error).message}`); + } +}; + +/** + * Get all jobs for a form (full history). + * Useful for debugging and audit logs. + */ +export type GetFormJobs = ( + ctx: FormRepositoryContext, + formId: string +) => Promise>; + +export const getFormJobs: GetFormJobs = async (ctx, formId) => { + const db = await ctx.db.getKysely(); + + try { + const rows = await db + .selectFrom('form_jobs') + .selectAll() + .where('form_id', '=', formId) + .orderBy('created_at', 'desc') + .execute(); + + const jobs = rows.map(row => ({ + id: row.id, + formId: row.form_id, + jobType: row.job_type as JobType, + status: row.status as JobStatus, + createdAt: new Date(row.created_at), + startedAt: row.started_at ? new Date(row.started_at) : undefined, + completedAt: row.completed_at ? new Date(row.completed_at) : undefined, + errorMessage: row.error_message || undefined, + errorStack: row.error_stack || undefined, + metadata: row.metadata ? JSON.parse(row.metadata) : undefined, + result: row.result ? JSON.parse(row.result) : undefined, + })); + + return success(jobs as FormJob[]); + } catch (err) { + return failure(`Failed to get jobs: ${(err as Error).message}`); + } +}; diff --git a/packages/forms/src/repository/get-document.test.ts b/packages/forms/src/repository/get-document.test.ts index dba4a9aa..223edf40 100644 --- a/packages/forms/src/repository/get-document.test.ts +++ b/packages/forms/src/repository/get-document.test.ts @@ -3,7 +3,7 @@ import { beforeAll, expect, it, vi } from 'vitest'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { addDocument } from './add-document'; import type { DocumentFieldMap } from '../documents/types'; import type { ParsedPdf } from '../documents/pdf/parsing-api'; diff --git a/packages/forms/src/repository/get-document.ts b/packages/forms/src/repository/get-document.ts index d537d96c..f614ae9f 100644 --- a/packages/forms/src/repository/get-document.ts +++ b/packages/forms/src/repository/get-document.ts @@ -1,8 +1,8 @@ -import { type Result, failure, success } from '@gsa-tts/forms-common'; +import { type Result, failure, success } from '@flexion/forms-common'; -import type { ParsedPdf } from '../documents/pdf/parsing-api'; -import type { DocumentFieldMap } from '../documents/types'; -import type { FormRepositoryContext } from '.'; +import type { ParsedPdf } from '../documents/pdf/parsing-api.js'; +import type { DocumentFieldMap } from '../documents/types.js'; +import type { FormRepositoryContext } from './index.js'; export type GetDocument = ( ctx: FormRepositoryContext, @@ -29,8 +29,11 @@ export const getDocument: GetDocument = async (ctx, id) => { .where('id', '=', id) .executeTakeFirstOrThrow() .then(data => { + // Handle documents without extract (stored during async processing initialization) const extract: { parsedPdf: ParsedPdf; fields: DocumentFieldMap } = - JSON.parse(data.extract); + data.extract && data.extract !== '' + ? JSON.parse(data.extract) + : { parsedPdf: {} as ParsedPdf, fields: {} }; return success({ id: data.id, data: data.data, diff --git a/packages/forms/src/repository/get-form-list.test.ts b/packages/forms/src/repository/get-form-list.test.ts index d52d21c9..c6159956 100644 --- a/packages/forms/src/repository/get-form-list.test.ts +++ b/packages/forms/src/repository/get-form-list.test.ts @@ -3,7 +3,7 @@ import { expect, it } from 'vitest'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { getForm } from './get-form.js'; import { getFormList } from './get-form-list.js'; import { defaultFormConfig } from '../patterns/index.js'; diff --git a/packages/forms/src/repository/get-form-list.ts b/packages/forms/src/repository/get-form-list.ts index 77822733..fa95d2a4 100644 --- a/packages/forms/src/repository/get-form-list.ts +++ b/packages/forms/src/repository/get-form-list.ts @@ -1,27 +1,70 @@ -import type { FormRepositoryContext } from '.'; +import type { FormRepositoryContext } from './index.js'; +import type { JobStatus } from './jobs/types.js'; -export type GetFormList = (ctx: FormRepositoryContext) => Promise< - | { - id: string; - title: string; - description: string; - }[] - | null ->; +export type FormListItem = { + id: string; + title: string; + description: string; + latestJob?: { + id: string; + jobType: string; + status: JobStatus; + createdAt: Date; + completedAt?: Date; + errorMessage?: string; + }; +}; + +export type GetFormList = ( + ctx: FormRepositoryContext +) => Promise; /** - * Retrieves a list of forms from the database. + * Retrieves a list of forms from the database with their latest job status. */ export const getFormList: GetFormList = async ctx => { const db = await ctx.db.getKysely(); - const rows = await db.selectFrom('forms').select(['id', 'data']).execute(); - - return rows.map(row => { - const form = JSON.parse(row.data); - return { - id: row.id, - title: form.summary.title, - description: form.summary.description, - }; - }); + + // Get all forms + const forms = await db.selectFrom('forms').select(['id', 'data']).execute(); + + // For each form, get the latest import-pdf job + const formList = await Promise.all( + forms.map(async row => { + const form = JSON.parse(row.data); + + // Get latest import-pdf job for this form + const latestJob = await db + .selectFrom('form_jobs') + .selectAll() + .where('form_id', '=', row.id) + .where('job_type', '=', 'import-pdf') + .orderBy('created_at', 'desc') + .limit(1) + .executeTakeFirst(); + + const formListItem: FormListItem = { + id: row.id, + title: form.summary.title, + description: form.summary.description, + }; + + if (latestJob) { + formListItem.latestJob = { + id: latestJob.id, + jobType: latestJob.job_type, + status: latestJob.status as JobStatus, + createdAt: new Date(latestJob.created_at), + completedAt: latestJob.completed_at + ? new Date(latestJob.completed_at) + : undefined, + errorMessage: latestJob.error_message || undefined, + }; + } + + return formListItem; + }) + ); + + return formList; }; diff --git a/packages/forms/src/repository/get-form-session.test.ts b/packages/forms/src/repository/get-form-session.test.ts index bc785d2a..dbc0e093 100644 --- a/packages/forms/src/repository/get-form-session.test.ts +++ b/packages/forms/src/repository/get-form-session.test.ts @@ -3,7 +3,7 @@ import { expect, it } from 'vitest'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { createTestBlueprint } from '../builder/builder.test'; import { addForm } from './add-form'; diff --git a/packages/forms/src/repository/get-form-session.ts b/packages/forms/src/repository/get-form-session.ts index 87c36e40..a2dfef39 100644 --- a/packages/forms/src/repository/get-form-session.ts +++ b/packages/forms/src/repository/get-form-session.ts @@ -1,6 +1,6 @@ -import { type Result, failure, success } from '@gsa-tts/forms-common'; -import { type FormSession, type FormSessionId } from '../session'; -import type { FormRepositoryContext } from '.'; +import { type Result, failure, success } from '@flexion/forms-common'; +import { type FormSession, type FormSessionId } from '../session.js'; +import type { FormRepositoryContext } from './index.js'; export type GetFormSession = ( ctx: FormRepositoryContext, diff --git a/packages/forms/src/repository/get-form.test.ts b/packages/forms/src/repository/get-form.test.ts index 2a46748a..f5824511 100644 --- a/packages/forms/src/repository/get-form.test.ts +++ b/packages/forms/src/repository/get-form.test.ts @@ -3,7 +3,7 @@ import { expect, it } from 'vitest'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { defaultFormConfig, type Blueprint } from '../index.js'; import { getForm } from './get-form.js'; diff --git a/packages/forms/src/repository/get-form.ts b/packages/forms/src/repository/get-form.ts index 9625bc34..daac9fb3 100644 --- a/packages/forms/src/repository/get-form.ts +++ b/packages/forms/src/repository/get-form.ts @@ -1,4 +1,4 @@ -import { failure, success, type Result } from '@gsa-tts/forms-common'; +import { failure, success, type Result } from '@flexion/forms-common'; import { parseFormString } from '../builder/parse-form.js'; import { type Blueprint } from '../index.js'; import type { FormRepositoryContext } from './index.js'; diff --git a/packages/forms/src/repository/index.ts b/packages/forms/src/repository/index.ts index c213942d..e70bc403 100644 --- a/packages/forms/src/repository/index.ts +++ b/packages/forms/src/repository/index.ts @@ -1,7 +1,8 @@ -import { type ServiceMethod, createService } from '@gsa-tts/forms-common'; -import { type DatabaseContext } from '@gsa-tts/forms-database'; +import { createService } from '@flexion/forms-common'; +import { type DatabaseContext } from '@flexion/forms-database'; import type { FormConfig } from '../pattern.js'; +import type { FormRepository } from './types.js'; import { type AddDocument, addDocument } from './add-document.js'; import { type AddForm, addForm } from './add-form.js'; @@ -15,24 +16,28 @@ import { type UpsertFormSession, upsertFormSession, } from './upsert-form-session.js'; +import { type CreateFormJob, createFormJob } from './jobs/create-form-job.js'; +import { + type CompleteFormJob, + completeFormJob, +} from './jobs/complete-form-job.js'; +import { type FailFormJob, failFormJob } from './jobs/fail-form-job.js'; +import { + type GetLatestFormJob, + getLatestFormJob, +} from './jobs/get-latest-form-job.js'; +import { type GetFormJobs, getFormJobs } from './jobs/get-form-jobs.js'; +import { type FormJob, type JobType, type JobStatus } from './jobs/types.js'; -export interface FormRepository { - addDocument: ServiceMethod; - addForm: ServiceMethod; - deleteForm: ServiceMethod; - getDocument: ServiceMethod; - getForm: ServiceMethod; - getFormSession: ServiceMethod; - getFormList: ServiceMethod; - saveForm: ServiceMethod; - upsertFormSession: ServiceMethod; -} +export type { FormRepository }; export type FormRepositoryContext = { db: DatabaseContext; formConfig: FormConfig; }; +export { type FormJob, type JobType, type JobStatus } from './jobs/types.js'; + export const createFormsRepository = ( ctx: FormRepositoryContext ): FormRepository => @@ -46,4 +51,9 @@ export const createFormsRepository = ( getForm, saveForm, upsertFormSession, + createFormJob, + completeFormJob, + failFormJob, + getLatestFormJob, + getFormJobs, }); diff --git a/packages/forms/src/repository/jobs/complete-form-job.test.ts b/packages/forms/src/repository/jobs/complete-form-job.test.ts new file mode 100644 index 00000000..02b4db80 --- /dev/null +++ b/packages/forms/src/repository/jobs/complete-form-job.test.ts @@ -0,0 +1,104 @@ +import { beforeAll, expect, it, vi } from 'vitest'; + +import { + type DbTestContext, + describeDatabase, +} from '@flexion/forms-database/testing'; +import { completeFormJob } from './complete-form-job.js'; +import { createFormJob } from './create-form-job.js'; +import { getLatestFormJob } from './get-latest-form-job.js'; +import { addForm } from '../add-form.js'; +import { defaultFormConfig } from '../../patterns/index.js'; + +describeDatabase('completeFormJob', () => { + const today = new Date(2000, 1, 1); + + beforeAll(async () => { + vi.setSystemTime(today); + }); + + it('completes a job with result data', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + const jobResult = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + if (!jobResult.success) { + expect.fail('createFormJob failed'); + } + + const result = await completeFormJob(ctx, jobResult.data.id, { + patternsAdded: 5, + fieldsExtracted: 12, + documentId: 'doc-1', + }); + + expect(result.success).toBe(true); + + // Verify the job was updated + const latestResult = await getLatestFormJob( + ctx, + formResult.data.id, + 'import-pdf' + ); + if (!latestResult.success || !latestResult.data) { + expect.fail('getLatestFormJob failed'); + } + + expect(latestResult.data.status).toBe('completed'); + expect(latestResult.data.completedAt).toBeDefined(); + expect(latestResult.data.result).toEqual({ + patternsAdded: 5, + fieldsExtracted: 12, + documentId: 'doc-1', + }); + }); + + it('completes a job without result data', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + const jobResult = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + if (!jobResult.success) { + expect.fail('createFormJob failed'); + } + + const result = await completeFormJob(ctx, jobResult.data.id); + + expect(result.success).toBe(true); + + // Verify the job was updated + const latestResult = await getLatestFormJob( + ctx, + formResult.data.id, + 'import-pdf' + ); + if (!latestResult.success || !latestResult.data) { + expect.fail('getLatestFormJob failed'); + } + + expect(latestResult.data.status).toBe('completed'); + expect(latestResult.data.completedAt).toBeDefined(); + expect(latestResult.data.result).toBeUndefined(); + }); +}); + +const testForm = { + summary: { title: 'Test form', description: 'Test description' }, + root: 'root', + patterns: { root: { type: 'sequence', id: 'root', data: { patterns: [] } } }, + outputs: [], +}; diff --git a/packages/forms/src/repository/jobs/complete-form-job.ts b/packages/forms/src/repository/jobs/complete-form-job.ts new file mode 100644 index 00000000..f90874c1 --- /dev/null +++ b/packages/forms/src/repository/jobs/complete-form-job.ts @@ -0,0 +1,32 @@ +import { type Result, success, failure } from '@flexion/forms-common'; +import type { FormRepositoryContext } from '../index.js'; +import { type JobResult, type JobType } from './types.js'; + +/** + * Mark a job as completed with result data. + */ +export type CompleteFormJob = ( + ctx: FormRepositoryContext, + jobId: string, + result?: JobResult[T] +) => Promise>; + +export const completeFormJob: CompleteFormJob = async (ctx, jobId, result) => { + const db = await ctx.db.getKysely(); + + try { + await db + .updateTable('form_jobs') + .set({ + status: 'completed', + completed_at: new Date().toISOString() as any, + result: result ? JSON.stringify(result) : null, + }) + .where('id', '=', jobId) + .execute(); + + return success(undefined); + } catch (err) { + return failure(`Failed to complete job: ${(err as Error).message}`); + } +}; diff --git a/packages/forms/src/repository/jobs/create-form-job.test.ts b/packages/forms/src/repository/jobs/create-form-job.test.ts new file mode 100644 index 00000000..494ff19b --- /dev/null +++ b/packages/forms/src/repository/jobs/create-form-job.test.ts @@ -0,0 +1,108 @@ +import { beforeAll, expect, it, vi } from 'vitest'; + +import { + type DbTestContext, + describeDatabase, +} from '@flexion/forms-database/testing'; +import { createFormJob } from './create-form-job.js'; +import { addForm } from '../add-form.js'; +import { defaultFormConfig } from '../../patterns/index.js'; + +describeDatabase('createFormJob', () => { + const today = new Date(2000, 1, 1); + + beforeAll(async () => { + vi.setSystemTime(today); + }); + + it('creates a job in processing state', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + const result = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + metadata: { + documentId: 'doc-1', + fileName: 'test.pdf', + userId: 'user-1', + }, + }); + + if (!result.success) { + expect.fail(`createFormJob failed: ${result.error}`); + } + + expect(result.data.status).toBe('processing'); + expect(result.data.jobType).toBe('import-pdf'); + expect(result.data.formId).toBe(formResult.data.id); + expect(result.data.createdAt).toEqual(today); + expect(result.data.startedAt).toEqual(today); + expect(result.data.metadata).toEqual({ + documentId: 'doc-1', + fileName: 'test.pdf', + userId: 'user-1', + }); + expect(result.data.id).toBeDefined(); + }); + + it('creates a job without metadata', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + const result = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + + if (!result.success) { + expect.fail(`createFormJob failed: ${result.error}`); + } + + expect(result.data.status).toBe('processing'); + expect(result.data.metadata).toBeUndefined(); + }); + + it('creates jobs with different types', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + const validateResult = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'validate-schema', + metadata: { + validatorVersion: '1.0', + userId: 'user-1', + }, + }); + + if (!validateResult.success) { + expect.fail('createFormJob failed'); + } + + expect(validateResult.data.jobType).toBe('validate-schema'); + expect(validateResult.data.metadata).toEqual({ + validatorVersion: '1.0', + userId: 'user-1', + }); + }); +}); + +const testForm = { + summary: { title: 'Test form', description: 'Test description' }, + root: 'root', + patterns: { root: { type: 'sequence', id: 'root', data: { patterns: [] } } }, + outputs: [], +}; diff --git a/packages/forms/src/repository/jobs/create-form-job.ts b/packages/forms/src/repository/jobs/create-form-job.ts new file mode 100644 index 00000000..604d025c --- /dev/null +++ b/packages/forms/src/repository/jobs/create-form-job.ts @@ -0,0 +1,55 @@ +import { type Result, success, failure } from '@flexion/forms-common'; +import type { FormRepositoryContext } from '../index.js'; +import { + type FormJob, + type JobMetadata, + type JobStatus, + type JobType, +} from './types.js'; +import { dateValue } from '@flexion/forms-database'; + +/** + * Create a new job record in 'processing' state. + * Call this before starting async work. + */ +export type CreateFormJob = ( + ctx: FormRepositoryContext, + params: { + formId: string; + jobType: T; + metadata?: JobMetadata[T]; + } +) => Promise, string>>; + +export const createFormJob: CreateFormJob = async (ctx, params) => { + const uuid = crypto.randomUUID(); + const db = await ctx.db.getKysely(); + + try { + const now = new Date(); + await db + .insertInto('form_jobs') + .values({ + id: uuid, + form_id: params.formId, + job_type: params.jobType, + status: 'processing', + created_at: dateValue(ctx.db.engine, now), + started_at: dateValue(ctx.db.engine, now), + metadata: params.metadata ? JSON.stringify(params.metadata) : null, + }) + .execute(); + + return success({ + id: uuid, + formId: params.formId, + jobType: params.jobType, + status: 'processing' as JobStatus, + createdAt: now, + startedAt: now, + metadata: params.metadata, + }) as any; + } catch (err) { + return failure(`Failed to create job: ${(err as Error).message}`); + } +}; diff --git a/packages/forms/src/repository/jobs/fail-form-job.test.ts b/packages/forms/src/repository/jobs/fail-form-job.test.ts new file mode 100644 index 00000000..1faea1d3 --- /dev/null +++ b/packages/forms/src/repository/jobs/fail-form-job.test.ts @@ -0,0 +1,107 @@ +import { beforeAll, expect, it, vi } from 'vitest'; + +import { + type DbTestContext, + describeDatabase, +} from '@flexion/forms-database/testing'; +import { failFormJob } from './fail-form-job.js'; +import { createFormJob } from './create-form-job.js'; +import { getLatestFormJob } from './get-latest-form-job.js'; +import { addForm } from '../add-form.js'; +import { defaultFormConfig } from '../../patterns/index.js'; + +describeDatabase('failFormJob', () => { + const today = new Date(2000, 1, 1); + + beforeAll(async () => { + vi.setSystemTime(today); + }); + + it('fails a job with error message and stack', async ({ + db, + }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + const jobResult = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + if (!jobResult.success) { + expect.fail('createFormJob failed'); + } + + const result = await failFormJob(ctx, jobResult.data.id, { + message: 'PDF parsing failed', + stack: 'Error: PDF parsing failed\n at parsePDF', + }); + + expect(result.success).toBe(true); + + // Verify the job was updated + const latestResult = await getLatestFormJob( + ctx, + formResult.data.id, + 'import-pdf' + ); + if (!latestResult.success || !latestResult.data) { + expect.fail('getLatestFormJob failed'); + } + + expect(latestResult.data.status).toBe('failed'); + expect(latestResult.data.completedAt).toBeDefined(); + expect(latestResult.data.errorMessage).toBe('PDF parsing failed'); + expect(latestResult.data.errorStack).toBe( + 'Error: PDF parsing failed\n at parsePDF' + ); + }); + + it('fails a job without stack trace', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + const jobResult = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'validate-schema', + }); + if (!jobResult.success) { + expect.fail('createFormJob failed'); + } + + const result = await failFormJob(ctx, jobResult.data.id, { + message: 'Validation failed', + }); + + expect(result.success).toBe(true); + + // Verify the job was updated + const latestResult = await getLatestFormJob( + ctx, + formResult.data.id, + 'validate-schema' + ); + if (!latestResult.success || !latestResult.data) { + expect.fail('getLatestFormJob failed'); + } + + expect(latestResult.data.status).toBe('failed'); + expect(latestResult.data.completedAt).toBeDefined(); + expect(latestResult.data.errorMessage).toBe('Validation failed'); + expect(latestResult.data.errorStack).toBeUndefined(); + }); +}); + +const testForm = { + summary: { title: 'Test form', description: 'Test description' }, + root: 'root', + patterns: { root: { type: 'sequence', id: 'root', data: { patterns: [] } } }, + outputs: [], +}; diff --git a/packages/forms/src/repository/jobs/fail-form-job.ts b/packages/forms/src/repository/jobs/fail-form-job.ts new file mode 100644 index 00000000..10577d1f --- /dev/null +++ b/packages/forms/src/repository/jobs/fail-form-job.ts @@ -0,0 +1,32 @@ +import { type Result, success, failure } from '@flexion/forms-common'; +import type { FormRepositoryContext } from '../index.js'; + +/** + * Mark a job as failed with error information. + */ +export type FailFormJob = ( + ctx: FormRepositoryContext, + jobId: string, + error: { message: string; stack?: string } +) => Promise>; + +export const failFormJob: FailFormJob = async (ctx, jobId, error) => { + const db = await ctx.db.getKysely(); + + try { + await db + .updateTable('form_jobs') + .set({ + status: 'failed', + completed_at: new Date().toISOString() as any, + error_message: error.message, + error_stack: error.stack || null, + }) + .where('id', '=', jobId) + .execute(); + + return success(undefined); + } catch (err) { + return failure(`Failed to fail job: ${(err as Error).message}`); + } +}; diff --git a/packages/forms/src/repository/jobs/get-form-jobs.test.ts b/packages/forms/src/repository/jobs/get-form-jobs.test.ts new file mode 100644 index 00000000..58db29d0 --- /dev/null +++ b/packages/forms/src/repository/jobs/get-form-jobs.test.ts @@ -0,0 +1,126 @@ +import { beforeAll, expect, it, vi } from 'vitest'; + +import { + type DbTestContext, + describeDatabase, +} from '@flexion/forms-database/testing'; +import { getFormJobs } from './get-form-jobs.js'; +import { createFormJob } from './create-form-job.js'; +import { addForm } from '../add-form.js'; +import { defaultFormConfig } from '../../patterns/index.js'; + +describeDatabase('getFormJobs', () => { + const today = new Date(2000, 1, 1); + + beforeAll(async () => { + vi.setSystemTime(today); + }); + + it('returns empty array when no jobs exist', async ({ + db, + }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + const result = await getFormJobs(ctx, formResult.data.id); + + if (!result.success) { + expect.fail('getFormJobs failed'); + } + + expect(result.data).toEqual([]); + }); + + it('returns all jobs in descending order', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + // Create 3 jobs with different timestamps + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 0)); + const job1 = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + expect(job1.success).toBe(true); + + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 1)); + const job2 = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'validate-schema', + }); + expect(job2.success).toBe(true); + + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 2)); + const job3 = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'publish', + }); + expect(job3.success).toBe(true); + + const result = await getFormJobs(ctx, formResult.data.id); + + if (!result.success) { + expect.fail('getFormJobs failed'); + } + + expect(result.data.length).toBe(3); + + // Should be ordered newest first + expect(result.data[0].createdAt.getTime()).toBeGreaterThan( + result.data[1].createdAt.getTime() + ); + expect(result.data[1].createdAt.getTime()).toBeGreaterThan( + result.data[2].createdAt.getTime() + ); + + // Verify order of job types + expect(result.data[0].jobType).toBe('publish'); + expect(result.data[1].jobType).toBe('validate-schema'); + expect(result.data[2].jobType).toBe('import-pdf'); + }); + + it('returns jobs for specific form only', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + // Create two forms + const form1Result = await addForm(ctx, testForm); + const form2Result = await addForm(ctx, testForm); + if (!form1Result.success || !form2Result.success) { + expect.fail('addForm failed'); + } + + // Create jobs for both forms + await createFormJob(ctx, { + formId: form1Result.data.id, + jobType: 'import-pdf', + }); + await createFormJob(ctx, { + formId: form2Result.data.id, + jobType: 'import-pdf', + }); + + const result = await getFormJobs(ctx, form1Result.data.id); + + if (!result.success) { + expect.fail('getFormJobs failed'); + } + + expect(result.data.length).toBe(1); + expect(result.data[0].formId).toBe(form1Result.data.id); + }); +}); + +const testForm = { + summary: { title: 'Test form', description: 'Test description' }, + root: 'root', + patterns: { root: { type: 'sequence', id: 'root', data: { patterns: [] } } }, + outputs: [], +}; diff --git a/packages/forms/src/repository/jobs/get-form-jobs.ts b/packages/forms/src/repository/jobs/get-form-jobs.ts new file mode 100644 index 00000000..6effe49f --- /dev/null +++ b/packages/forms/src/repository/jobs/get-form-jobs.ts @@ -0,0 +1,31 @@ +import { type Result, success, failure } from '@flexion/forms-common'; +import type { FormRepositoryContext } from '../index.js'; +import { type FormJob, rowToFormJob } from './types.js'; + +/** + * Get all jobs for a form (full history). + * Useful for debugging and audit logs. + */ +export type GetFormJobs = ( + ctx: FormRepositoryContext, + formId: string +) => Promise>; + +export const getFormJobs: GetFormJobs = async (ctx, formId) => { + const db = await ctx.db.getKysely(); + + try { + const rows = await db + .selectFrom('form_jobs') + .selectAll() + .where('form_id', '=', formId) + .orderBy('created_at', 'desc') + .execute(); + + const jobs = rows.map(rowToFormJob); + + return success(jobs as FormJob[]); + } catch (err) { + return failure(`Failed to get jobs: ${(err as Error).message}`); + } +}; diff --git a/packages/forms/src/repository/jobs/get-latest-form-job.test.ts b/packages/forms/src/repository/jobs/get-latest-form-job.test.ts new file mode 100644 index 00000000..8e535901 --- /dev/null +++ b/packages/forms/src/repository/jobs/get-latest-form-job.test.ts @@ -0,0 +1,162 @@ +import { beforeAll, expect, it, vi } from 'vitest'; + +import { + type DbTestContext, + describeDatabase, +} from '@flexion/forms-database/testing'; +import { getLatestFormJob } from './get-latest-form-job.js'; +import { createFormJob } from './create-form-job.js'; +import { addForm } from '../add-form.js'; +import { defaultFormConfig } from '../../patterns/index.js'; + +describeDatabase('getLatestFormJob', () => { + const today = new Date(2000, 1, 1); + + beforeAll(async () => { + vi.setSystemTime(today); + }); + + it('returns null when no job exists', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + const result = await getLatestFormJob( + ctx, + formResult.data.id, + 'import-pdf' + ); + + if (!result.success) { + expect.fail('getLatestFormJob failed'); + } + + expect(result.data).toBeNull(); + }); + + it('returns the latest job of specific type', async ({ + db, + }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + // Create two jobs of the same type + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 0)); + const job1 = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + expect(job1.success).toBe(true); + + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 1)); + const job2 = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + if (!job2.success) { + expect.fail('createFormJob failed'); + } + + const result = await getLatestFormJob( + ctx, + formResult.data.id, + 'import-pdf' + ); + + if (!result.success || !result.data) { + expect.fail('getLatestFormJob failed'); + } + + // Should return the second (newer) job + expect(result.data.id).toBe(job2.data.id); + }); + + it('returns job of specific type only', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + // Create jobs of different types + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 0)); + const importJob = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'import-pdf', + }); + if (!importJob.success) { + expect.fail('createFormJob failed'); + } + + vi.setSystemTime(new Date(2000, 1, 1, 0, 0, 1)); + await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'validate-schema', + }); + + // Should return import-pdf job, not the newer validate-schema + const result = await getLatestFormJob( + ctx, + formResult.data.id, + 'import-pdf' + ); + + if (!result.success || !result.data) { + expect.fail('getLatestFormJob failed'); + } + + expect(result.data.jobType).toBe('import-pdf'); + expect(result.data.id).toBe(importJob.data.id); + }); + + it('returns job with metadata and result', async ({ db }) => { + const ctx = { db: db.ctx, formConfig: defaultFormConfig }; + + const formResult = await addForm(ctx, testForm); + if (!formResult.success) { + expect.fail('addForm failed'); + } + + const jobResult = await createFormJob(ctx, { + formId: formResult.data.id, + jobType: 'validate-schema', + metadata: { + validatorVersion: '2.0', + userId: 'user-123', + }, + }); + if (!jobResult.success) { + expect.fail('createFormJob failed'); + } + + const result = await getLatestFormJob( + ctx, + formResult.data.id, + 'validate-schema' + ); + + if (!result.success || !result.data) { + expect.fail('getLatestFormJob failed'); + } + + expect(result.data.metadata).toEqual({ + validatorVersion: '2.0', + userId: 'user-123', + }); + }); +}); + +const testForm = { + summary: { title: 'Test form', description: 'Test description' }, + root: 'root', + patterns: { root: { type: 'sequence', id: 'root', data: { patterns: [] } } }, + outputs: [], +}; diff --git a/packages/forms/src/repository/jobs/get-latest-form-job.ts b/packages/forms/src/repository/jobs/get-latest-form-job.ts new file mode 100644 index 00000000..651a3a94 --- /dev/null +++ b/packages/forms/src/repository/jobs/get-latest-form-job.ts @@ -0,0 +1,40 @@ +import { type Result, success, failure } from '@flexion/forms-common'; +import type { FormRepositoryContext } from '../index.js'; +import { type FormJob, type JobType, rowToFormJob } from './types.js'; + +/** + * Get the latest job for a form of a specific type. + * Useful for status polling: "What's the current import-pdf job status?" + */ +export type GetLatestFormJob = ( + ctx: FormRepositoryContext, + formId: string, + jobType: T +) => Promise | null, string>>; + +export const getLatestFormJob: GetLatestFormJob = async ( + ctx, + formId, + jobType +) => { + const db = await ctx.db.getKysely(); + + try { + const row = await db + .selectFrom('form_jobs') + .selectAll() + .where('form_id', '=', formId) + .where('job_type', '=', jobType) + .orderBy('created_at', 'desc') + .limit(1) + .executeTakeFirst(); + + if (!row) { + return success(null); + } + + return success(rowToFormJob(row)) as any; + } catch (err) { + return failure(`Failed to get latest job: ${(err as Error).message}`); + } +}; diff --git a/packages/forms/src/repository/jobs/types.ts b/packages/forms/src/repository/jobs/types.ts new file mode 100644 index 00000000..9af83d49 --- /dev/null +++ b/packages/forms/src/repository/jobs/types.ts @@ -0,0 +1,69 @@ +// Type-safe job metadata by job type +export type JobMetadata = { + 'import-pdf': { + documentId: string; + fileName: string; + userId: string; + }; + 'validate-schema': { + validatorVersion: string; + userId: string; + }; + publish: { + targetEnvironment: 'staging' | 'production'; + publisherId: string; + }; +}; + +// Type-safe job results by job type +export type JobResult = { + 'import-pdf': { + patternsAdded: number; + fieldsExtracted: number; + documentId: string; + }; + 'validate-schema': { + errorsFound: number; + warningsFound: number; + issues: Array<{ field: string; message: string }>; + }; + publish: { + publishedAt: string; + url: string; + }; +}; + +export type JobType = keyof JobMetadata; +export type JobStatus = 'pending' | 'processing' | 'completed' | 'failed'; + +export type FormJob = { + id: string; + formId: string; + jobType: T; + status: JobStatus; + createdAt: Date; + startedAt?: Date; + completedAt?: Date; + errorMessage?: string; + errorStack?: string; + metadata?: JobMetadata[T]; + result?: JobResult[T]; +}; + +/** + * Helper to convert database row to FormJob object. + * Handles date conversions and JSON parsing. + */ +export const rowToFormJob = (row: any): FormJob => ({ + id: row.id, + formId: row.form_id, + jobType: row.job_type as JobType, + status: row.status as JobStatus, + createdAt: new Date(row.created_at), + startedAt: row.started_at ? new Date(row.started_at) : undefined, + completedAt: row.completed_at ? new Date(row.completed_at) : undefined, + errorMessage: row.error_message || undefined, + errorStack: row.error_stack || undefined, + metadata: row.metadata ? JSON.parse(row.metadata) : undefined, + result: row.result ? JSON.parse(row.result) : undefined, +}); diff --git a/packages/forms/src/repository/save-form.test.ts b/packages/forms/src/repository/save-form.test.ts index caa31075..206fcb44 100644 --- a/packages/forms/src/repository/save-form.test.ts +++ b/packages/forms/src/repository/save-form.test.ts @@ -3,7 +3,7 @@ import { expect, it } from 'vitest'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { defaultFormConfig, type Blueprint } from '../index.js'; import { saveForm } from './save-form.js'; diff --git a/packages/forms/src/repository/save-form.ts b/packages/forms/src/repository/save-form.ts index e64e924d..332c0c7f 100644 --- a/packages/forms/src/repository/save-form.ts +++ b/packages/forms/src/repository/save-form.ts @@ -1,4 +1,4 @@ -import { type VoidResult, failure, success } from '@gsa-tts/forms-common'; +import { type VoidResult, failure, success } from '@flexion/forms-common'; import { type Blueprint } from '../index.js'; import type { FormRepositoryContext } from './index.js'; diff --git a/packages/forms/src/repository/types.ts b/packages/forms/src/repository/types.ts new file mode 100644 index 00000000..9291a6b8 --- /dev/null +++ b/packages/forms/src/repository/types.ts @@ -0,0 +1,37 @@ +import type { ServiceMethod } from '@flexion/forms-common'; + +import type { AddDocument } from './add-document.js'; +import type { AddForm } from './add-form.js'; +import type { DeleteForm } from './delete-form.js'; +import type { GetDocument } from './get-document.js'; +import type { GetForm } from './get-form.js'; +import type { GetFormList } from './get-form-list.js'; +import type { GetFormSession } from './get-form-session.js'; +import type { SaveForm } from './save-form.js'; +import type { UpsertFormSession } from './upsert-form-session.js'; +import type { CreateFormJob } from './jobs/create-form-job.js'; +import type { CompleteFormJob } from './jobs/complete-form-job.js'; +import type { FailFormJob } from './jobs/fail-form-job.js'; +import type { GetLatestFormJob } from './jobs/get-latest-form-job.js'; +import type { GetFormJobs } from './jobs/get-form-jobs.js'; + +/** + * Interface for the forms repository. + * Contains methods for persisting and retrieving forms, documents, and sessions. + */ +export interface FormRepository { + addDocument: ServiceMethod; + addForm: ServiceMethod; + deleteForm: ServiceMethod; + getDocument: ServiceMethod; + getForm: ServiceMethod; + getFormSession: ServiceMethod; + getFormList: ServiceMethod; + saveForm: ServiceMethod; + upsertFormSession: ServiceMethod; + createFormJob: ServiceMethod; + completeFormJob: ServiceMethod; + failFormJob: ServiceMethod; + getLatestFormJob: ServiceMethod; + getFormJobs: ServiceMethod; +} diff --git a/packages/forms/src/repository/upsert-form-session.test.ts b/packages/forms/src/repository/upsert-form-session.test.ts index 8fea4597..c3c7ab01 100644 --- a/packages/forms/src/repository/upsert-form-session.test.ts +++ b/packages/forms/src/repository/upsert-form-session.test.ts @@ -3,7 +3,7 @@ import { beforeEach, expect, it } from 'vitest'; import { type DbTestContext, describeDatabase, -} from '@gsa-tts/forms-database/testing'; +} from '@flexion/forms-database/testing'; import { defaultFormConfig, type Blueprint } from '..'; import { createTestBlueprint } from '../builder/builder.test'; diff --git a/packages/forms/src/repository/upsert-form-session.ts b/packages/forms/src/repository/upsert-form-session.ts index f0819ffe..76930205 100644 --- a/packages/forms/src/repository/upsert-form-session.ts +++ b/packages/forms/src/repository/upsert-form-session.ts @@ -1,6 +1,6 @@ -import { type Result, failure, success } from '@gsa-tts/forms-common'; -import { type FormSession } from '../session'; -import type { FormRepositoryContext } from '.'; +import { type Result, failure, success } from '@flexion/forms-common'; +import { type FormSession } from '../session.js'; +import type { FormRepositoryContext } from './index.js'; export type UpsertFormSession = ( ctx: FormRepositoryContext, diff --git a/packages/forms/src/response.ts b/packages/forms/src/response.ts index 96200c92..e8c8c50f 100644 --- a/packages/forms/src/response.ts +++ b/packages/forms/src/response.ts @@ -1,4 +1,4 @@ -import { type Result } from '@gsa-tts/forms-common'; +import { type Result } from '@flexion/forms-common'; import { type PromptAction } from './components.js'; import { diff --git a/packages/forms/src/services/add-form.ts b/packages/forms/src/services/add-form.ts index f049d729..d2f539de 100644 --- a/packages/forms/src/services/add-form.ts +++ b/packages/forms/src/services/add-form.ts @@ -1,4 +1,4 @@ -import { type Result, failure } from '@gsa-tts/forms-common'; +import { type Result, failure } from '@flexion/forms-common'; import { Blueprint } from '../index.js'; import { type FormServiceContext } from '../context/index.js'; diff --git a/packages/forms/src/services/delete-form.ts b/packages/forms/src/services/delete-form.ts index e1f709c9..c1f64888 100644 --- a/packages/forms/src/services/delete-form.ts +++ b/packages/forms/src/services/delete-form.ts @@ -1,4 +1,4 @@ -import { type VoidResult, failure } from '@gsa-tts/forms-common'; +import { type VoidResult, failure } from '@flexion/forms-common'; import { type FormServiceContext } from '../context/index.js'; diff --git a/packages/forms/src/services/get-form-list.test.ts b/packages/forms/src/services/get-form-list.test.ts index 058704e7..26d0f71c 100644 --- a/packages/forms/src/services/get-form-list.test.ts +++ b/packages/forms/src/services/get-form-list.test.ts @@ -16,7 +16,7 @@ describe('getFormList', () => { success: false, error: { status: 401, - message: 'You must be logged in to delete a form', + message: 'You must be logged in to get form list', }, }); }); diff --git a/packages/forms/src/services/get-form-list.ts b/packages/forms/src/services/get-form-list.ts index 40eafdb8..0626524d 100644 --- a/packages/forms/src/services/get-form-list.ts +++ b/packages/forms/src/services/get-form-list.ts @@ -1,12 +1,10 @@ -import { type Result, failure, success } from '@gsa-tts/forms-common'; +import { type Result, failure, success } from '@flexion/forms-common'; import { type FormServiceContext } from '../context/index.js'; +import type { FormListItem as RepositoryFormListItem } from '../repository/get-form-list.js'; + +export type FormListItem = RepositoryFormListItem; -export type FormListItem = { - id: string; - title: string; - description: string; -}; type FormListError = { status: number; message: string; @@ -24,7 +22,7 @@ export const getFormList: GetFormList = async ctx => { if (!ctx.isUserLoggedIn()) { return failure({ status: 401, - message: 'You must be logged in to delete a form', + message: 'You must be logged in to get form list', }); } const forms = await ctx.repository.getFormList(); diff --git a/packages/forms/src/services/get-form-session.ts b/packages/forms/src/services/get-form-session.ts index 02b90546..c9ed03bf 100644 --- a/packages/forms/src/services/get-form-session.ts +++ b/packages/forms/src/services/get-form-session.ts @@ -1,4 +1,4 @@ -import { type Result, failure, success } from '@gsa-tts/forms-common'; +import { type Result, failure, success } from '@flexion/forms-common'; import { type FormServiceContext } from '../context/index.js'; import { type FormRoute } from '../route-data.js'; diff --git a/packages/forms/src/services/get-form-status.test.ts b/packages/forms/src/services/get-form-status.test.ts new file mode 100644 index 00000000..e1a91feb --- /dev/null +++ b/packages/forms/src/services/get-form-status.test.ts @@ -0,0 +1,210 @@ +import { describe, expect, it } from 'vitest'; + +import { createForm } from '../index.js'; +import { createTestFormServiceContext } from '../testing.js'; +import { getFormStatus } from './get-form-status.js'; + +const TEST_FORM = createForm({ title: 'Form Title', description: '' }); + +const TEST_FORM_WITH_PATTERNS = { + summary: { title: 'Test form', description: 'Test description' }, + root: 'root', + patterns: { + root: { type: 'sequence', id: 'root', data: { patterns: ['page1'] } }, + page1: { + type: 'page', + id: 'page1', + data: { title: 'Page 1', patterns: [] }, + }, + }, + outputs: [], +}; + +describe('getFormStatus', () => { + it('returns 404 for non-existent form', async () => { + const ctx = await createTestFormServiceContext({ + isUserLoggedIn: () => false, + }); + + const result = await getFormStatus(ctx, 'non-existent-id'); + + expect(result).toEqual({ + success: false, + error: { + status: 404, + message: 'Form not found', + }, + }); + }); + + it('returns draft status for empty form with no job', async () => { + const ctx = await createTestFormServiceContext({ + isUserLoggedIn: () => false, + }); + + const addResult = await ctx.repository.addForm(TEST_FORM); + if (!addResult.success) { + expect.fail('Failed to add form'); + } + + const result = await getFormStatus(ctx, addResult.data.id); + + if (!result.success) { + expect.fail(`Failed to get form status: ${JSON.stringify(result.error)}`); + } + + expect(result.data.formStatus).toBe('draft'); + expect(result.data.latestJob).toBeUndefined(); + }); + + it('returns ready status for form with patterns and no job', async () => { + const ctx = await createTestFormServiceContext({ + isUserLoggedIn: () => false, + }); + + const addResult = await ctx.repository.addForm(TEST_FORM_WITH_PATTERNS); + if (!addResult.success) { + expect.fail('Failed to add form'); + } + + const result = await getFormStatus(ctx, addResult.data.id); + + if (!result.success) { + expect.fail(`Failed to get form status: ${JSON.stringify(result.error)}`); + } + + expect(result.data.formStatus).toBe('ready'); + expect(result.data.latestJob).toBeUndefined(); + }); + + it('returns draft status when job is processing (race condition fix)', async () => { + const ctx = await createTestFormServiceContext({ + isUserLoggedIn: () => false, + }); + + // Create form with patterns (simulating mid-processing state where patterns exist) + const addResult = await ctx.repository.addForm(TEST_FORM_WITH_PATTERNS); + if (!addResult.success) { + expect.fail('Failed to add form'); + } + + // Create a processing job + const jobResult = await ctx.repository.createFormJob({ + formId: addResult.data.id, + jobType: 'import-pdf', + metadata: { + documentId: 'doc-1', + fileName: 'test.pdf', + userId: 'user-1', + }, + }); + if (!jobResult.success) { + expect.fail('Failed to create job'); + } + + // Get form status - should be 'draft' even though patterns exist + // because the job is still processing + const result = await getFormStatus(ctx, addResult.data.id); + + if (!result.success) { + expect.fail(`Failed to get form status: ${JSON.stringify(result.error)}`); + } + + // Key assertion: form status should be 'draft' because job is still processing + expect(result.data.formStatus).toBe('draft'); + expect(result.data.latestJob).toBeDefined(); + expect(result.data.latestJob?.status).toBe('processing'); + }); + + it('returns ready status after job completes successfully', async () => { + const ctx = await createTestFormServiceContext({ + isUserLoggedIn: () => false, + }); + + // Create form + const addResult = await ctx.repository.addForm(TEST_FORM); + if (!addResult.success) { + expect.fail('Failed to add form'); + } + + // Create and complete a job + const jobResult = await ctx.repository.createFormJob({ + formId: addResult.data.id, + jobType: 'import-pdf', + metadata: { + documentId: 'doc-1', + fileName: 'test.pdf', + userId: 'user-1', + }, + }); + if (!jobResult.success) { + expect.fail('Failed to create job'); + } + + // Add patterns to form (simulating job completion) + await ctx.repository.saveForm( + addResult.data.id, + TEST_FORM_WITH_PATTERNS as any + ); + + // Complete the job + await ctx.repository.completeFormJob(jobResult.data.id, { + patternsAdded: 1, + fieldsExtracted: 5, + documentId: 'doc-1', + }); + + // Get form status - should be 'ready' now + const result = await getFormStatus(ctx, addResult.data.id); + + if (!result.success) { + expect.fail(`Failed to get form status: ${JSON.stringify(result.error)}`); + } + + expect(result.data.formStatus).toBe('ready'); + expect(result.data.latestJob).toBeDefined(); + expect(result.data.latestJob?.status).toBe('completed'); + }); + + it('returns draft status when job fails', async () => { + const ctx = await createTestFormServiceContext({ + isUserLoggedIn: () => false, + }); + + // Create form + const addResult = await ctx.repository.addForm(TEST_FORM); + if (!addResult.success) { + expect.fail('Failed to add form'); + } + + // Create and fail a job + const jobResult = await ctx.repository.createFormJob({ + formId: addResult.data.id, + jobType: 'import-pdf', + metadata: { + documentId: 'doc-1', + fileName: 'test.pdf', + userId: 'user-1', + }, + }); + if (!jobResult.success) { + expect.fail('Failed to create job'); + } + + await ctx.repository.failFormJob(jobResult.data.id, { + message: 'Processing failed', + }); + + // Get form status - should be 'draft' because no patterns and job failed + const result = await getFormStatus(ctx, addResult.data.id); + + if (!result.success) { + expect.fail(`Failed to get form status: ${JSON.stringify(result.error)}`); + } + + expect(result.data.formStatus).toBe('draft'); + expect(result.data.latestJob).toBeDefined(); + expect(result.data.latestJob?.status).toBe('failed'); + expect(result.data.latestJob?.errorMessage).toBe('Processing failed'); + }); +}); diff --git a/packages/forms/src/services/get-form-status.ts b/packages/forms/src/services/get-form-status.ts new file mode 100644 index 00000000..4d7cd241 --- /dev/null +++ b/packages/forms/src/services/get-form-status.ts @@ -0,0 +1,92 @@ +import { type Result, success, failure } from '@flexion/forms-common'; +import type { InternalFormServiceContext } from '../context/index.js'; +import type { JobStatus } from '../repository/jobs/types.js'; + +export type FormStatusResponse = { + formId: string; + formStatus: 'draft' | 'ready'; // Usability status + latestJob?: { + id: string; + jobType: string; + status: JobStatus; + createdAt: string; + completedAt?: string; + errorMessage?: string; + }; +}; + +export type GetFormStatusError = { + status: number; + message: string; +}; + +export type GetFormStatus = ( + ctx: InternalFormServiceContext, + formId: string +) => Promise>; + +/** + * Get form status and latest import-pdf job status. + * Used by frontend polling to check processing progress. + */ +export const getFormStatus: GetFormStatus = async (ctx, formId) => { + // Get form to check if it exists + const formResult = await ctx.repository.getForm(formId); + if (!formResult.success) { + return failure({ + status: 404, + message: 'Form not found', + }); + } + + const form = formResult.data; + if (!form) { + return failure({ + status: 404, + message: 'Form not found', + }); + } + + // Get latest import-pdf job (if any) + const latestJobResult = await ctx.repository.getLatestFormJob( + formId, + 'import-pdf' + ); + + if (!latestJobResult.success) { + return failure({ + status: 500, + message: 'Failed to get job status', + }); + } + + const job = latestJobResult.data; + + // Determine form status based on job state first to avoid race conditions + // If a job is actively processing, keep status as 'draft' even if patterns exist + let formStatus: 'draft' | 'ready'; + + if (job && (job.status === 'pending' || job.status === 'processing')) { + // Job is still active - form is not ready yet + formStatus = 'draft'; + } else { + // No active job - determine status based on content + const hasContent = Object.keys(form.patterns).length > 1; // >1 because root pattern always exists + formStatus = hasContent ? 'ready' : 'draft'; + } + + return success({ + formId, + formStatus, + latestJob: job + ? { + id: job.id, + jobType: job.jobType, + status: job.status, + createdAt: job.createdAt.toISOString(), + completedAt: job.completedAt?.toISOString(), + errorMessage: job.errorMessage, + } + : undefined, + }); +}; diff --git a/packages/forms/src/services/get-form.ts b/packages/forms/src/services/get-form.ts index 3b52dfd3..d3bef99a 100644 --- a/packages/forms/src/services/get-form.ts +++ b/packages/forms/src/services/get-form.ts @@ -1,4 +1,4 @@ -import { type Result, failure, success } from '@gsa-tts/forms-common'; +import { type Result, failure, success } from '@flexion/forms-common'; import { parseForm } from '../builder/parse-form.js'; import { type FormServiceContext } from '../context/index.js'; diff --git a/packages/forms/src/services/index.ts b/packages/forms/src/services/index.ts index db82a813..7c35be75 100644 --- a/packages/forms/src/services/index.ts +++ b/packages/forms/src/services/index.ts @@ -1,12 +1,17 @@ -import { type ServiceMethod, createService } from '@gsa-tts/forms-common'; +import { type ServiceMethod, createService } from '@flexion/forms-common'; -import { type FormServiceContext } from '../context/index.js'; +import { + type FormServiceContext, + type InternalFormServiceContext, +} from '../context/index.js'; +import { parsePdf as parsePdfCore } from '../documents/pdf/parsing-api.js'; import { type AddForm, addForm } from './add-form.js'; import { type DeleteForm, deleteForm } from './delete-form.js'; import { type GetForm, getForm } from './get-form.js'; import { type GetFormList, getFormList } from './get-form-list.js'; import { type GetFormSession, getFormSession } from './get-form-session.js'; +import { type GetFormStatus, getFormStatus } from './get-form-status.js'; import { type InitializeForm, initializeForm } from './initialize-form.js'; import { type SaveForm, saveForm } from './save-form.js'; import { type SubmitForm, submitForm } from './submit-form.js'; @@ -16,17 +21,26 @@ import { type SubmitForm, submitForm } from './submit-form.js'; * * @param {FormServiceContext} ctx - The context required to initialize the form service. */ -export const createFormService = (ctx: FormServiceContext) => - createService(ctx, { +export const createFormService = (ctx: FormServiceContext): FormService => { + // Create parsePdf wrapper that binds parser and config from context + const parsePdf = (pdfBytes: Uint8Array) => + parsePdfCore({ parser: ctx.parser, formConfig: ctx.config }, pdfBytes); + + // Augment context with parsePdf for internal use + const internalCtx: InternalFormServiceContext = { ...ctx, parsePdf }; + + return createService(internalCtx, { addForm, deleteForm, getForm, getFormList, getFormSession, + getFormStatus, initializeForm, saveForm, submitForm, }); +}; export type FormService = { addForm: ServiceMethod; @@ -34,6 +48,7 @@ export type FormService = { getForm: ServiceMethod; getFormList: ServiceMethod; getFormSession: ServiceMethod; + getFormStatus: ServiceMethod; initializeForm: ServiceMethod; saveForm: ServiceMethod; submitForm: ServiceMethod; diff --git a/packages/forms/src/services/initialize-form.test.ts b/packages/forms/src/services/initialize-form.test.ts index a1c994ab..efedf88a 100644 --- a/packages/forms/src/services/initialize-form.test.ts +++ b/packages/forms/src/services/initialize-form.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { createTestFormServiceContext } from '../testing.js'; @@ -31,36 +31,248 @@ describe('initializeForm', () => { data: { timestamp: expect.any(String), id: expect.any(String), + status: 'ready', }, }); }); - it('initializes successfully with document when user is logged in', async () => { + it('initializes and returns immediately with document (async processing)', async () => { + const mockParsedPdf = { + title: 'Parsed Form Title', + description: 'This form was parsed from a PDF', + root: 'page1', + patterns: { + page1: { + type: 'page', + id: 'page1', + data: { + title: 'Page 1', + patterns: ['paragraph1', 'input1'], + }, + }, + paragraph1: { + type: 'paragraph', + id: 'paragraph1', + data: { + text: 'Welcome to the parsed form', + }, + }, + input1: { + type: 'input', + id: 'input1', + data: { + label: 'First Name', + required: true, + }, + }, + }, + outputs: { + firstName: { type: 'text' as const, name: 'firstName', value: '' }, + }, + errors: [], + }; + + const mockFields = { + firstName: { + type: 'TextField' as const, + name: 'firstName', + label: 'First Name', + value: '', + required: true, + }, + }; + const ctx = await createTestFormServiceContext({ isUserLoggedIn: () => true, - parsedPdf: async () => ({ - parsedPdf: { - text: 'test', - title: '', - root: 'root', - description: '', - patterns: {}, - errors: [], - outputs: {}, - }, - fields: {}, - }), }); + + // Mock parsePdf to avoid needing a real PDF + ctx.parsePdf = async () => ({ + parsedPdf: mockParsedPdf as any, + fields: mockFields, + }); + + // Base64 encoded "This is test PDF data" + const testPdfData = 'VGhpcyBpcyB0ZXN0IFBERiBkYXRh'; + const result = await initializeForm(ctx, { - summary, - document: { fileName: 'test.pdf', data: 'VGhpcyBpcyBub3QgYSBQREYu' }, + document: { fileName: 'test.pdf', data: testPdfData }, }); + + // Should return immediately with processing status expect(result).toEqual({ success: true, data: { timestamp: expect.any(String), id: expect.any(String), + jobId: expect.any(String), + status: 'processing', + }, + }); + + if (!result.success) return; + + const { id: formId, jobId } = result.data; + + // Wait for async processing to complete + await vi.waitFor( + async () => { + const job = await ctx.repository.getLatestFormJob(formId, 'import-pdf'); + expect(job.success).toBe(true); + if (!job.success) throw new Error('Job not found'); + expect(job.data?.status).toBe('completed'); + }, + { timeout: 5000 } + ); + + // Verify job was completed successfully + const jobResult = await ctx.repository.getLatestFormJob( + formId, + 'import-pdf' + ); + expect(jobResult.success).toBe(true); + if (!jobResult.success) return; + + const job = jobResult.data!; + expect(job.status).toBe('completed'); + expect(job.completedAt).toBeDefined(); + expect(job.result).toEqual({ + patternsAdded: expect.any(Number), + fieldsExtracted: expect.any(Number), + documentId: expect.any(String), + }); + + // Verify form was updated with parsed content + const formResult = await ctx.repository.getForm(formId); + expect(formResult.success).toBe(true); + if (!formResult.success) return; + + const form = formResult.data!; + + // Form should have been updated with parsed title + expect(form.summary.title).toBe('Parsed Form Title'); + expect(form.summary.description).toBe('This form was parsed from a PDF'); + + // Form should have patterns added (more than just root) + expect(Object.keys(form.patterns).length).toBeGreaterThan(1); + }); + + it('handles async processing errors gracefully', async () => { + const ctx = await createTestFormServiceContext({ + isUserLoggedIn: () => true, + }); + + // Mock parsePdf to throw an error + ctx.parsePdf = async () => { + throw new Error('Parser failed to process PDF'); + }; + + const testPdfData = 'VGhpcyBpcyB0ZXN0IFBERiBkYXRh'; + + const result = await initializeForm(ctx, { + document: { fileName: 'failing-test.pdf', data: testPdfData }, + }); + + // Should still return successfully (processing happens async) + expect(result.success).toBe(true); + if (!result.success) return; + + const { id: formId } = result.data; + + // Wait for async processing to fail + await vi.waitFor( + async () => { + const job = await ctx.repository.getLatestFormJob(formId, 'import-pdf'); + expect(job.success).toBe(true); + if (!job.success) throw new Error('Job not found'); + expect(job.data?.status).toBe('failed'); }, + { timeout: 5000 } + ); + + // Verify job failed with error message + const jobResult = await ctx.repository.getLatestFormJob( + formId, + 'import-pdf' + ); + expect(jobResult.success).toBe(true); + if (!jobResult.success) return; + + const job = jobResult.data!; + expect(job.status).toBe('failed'); + expect(job.errorMessage).toContain('Parser failed to process PDF'); + expect(job.completedAt).toBeDefined(); + expect(job.result).toBeUndefined(); + }); + + it('validates document data is valid base64', async () => { + const ctx = await createTestFormServiceContext({ + isUserLoggedIn: () => true, }); + + const result = await initializeForm(ctx, { + document: { fileName: 'test.pdf', data: 'not-valid-base64!!!' }, + }); + + expect(result).toEqual({ + success: false, + error: { + status: 400, + message: 'Invalid options', + }, + }); + }); + + it('uses filename as title when no summary provided', async () => { + const mockParsedPdf = { + title: 'Override Title', + description: 'Description from PDF', + root: 'root', + patterns: { + root: { + type: 'page', + id: 'root', + data: { title: 'Page 1', patterns: [] }, + }, + }, + outputs: {}, + errors: [], + }; + + const ctx = await createTestFormServiceContext({ + isUserLoggedIn: () => true, + }); + + // Mock parsePdf + ctx.parsePdf = async () => ({ + parsedPdf: mockParsedPdf as any, + fields: {}, + }); + + const result = await initializeForm(ctx, { + document: { fileName: 'my-form.pdf', data: 'VGVzdA==' }, + }); + + expect(result.success).toBe(true); + if (!result.success) return; + + const { id: formId } = result.data; + + // Wait for processing + await vi.waitFor( + async () => { + const job = await ctx.repository.getLatestFormJob(formId, 'import-pdf'); + if (!job.success) throw new Error('Job not found'); + expect(job.data?.status).toBe('completed'); + }, + { timeout: 5000 } + ); + + // Check that the parsed title was used (not the filename) + const formResult = await ctx.repository.getForm(formId); + expect(formResult.success).toBe(true); + if (!formResult.success) return; + + expect(formResult.data!.summary.title).toBe('Override Title'); }); }); diff --git a/packages/forms/src/services/initialize-form.ts b/packages/forms/src/services/initialize-form.ts index 5b87aa66..cd5d35e7 100644 --- a/packages/forms/src/services/initialize-form.ts +++ b/packages/forms/src/services/initialize-form.ts @@ -1,9 +1,9 @@ import * as z from 'zod'; -import { type Result, failure, success } from '@gsa-tts/forms-common'; +import { type Result, failure, success } from '@flexion/forms-common'; import { BlueprintBuilder } from '../builder/index.js'; -import { type FormServiceContext } from '../context/index.js'; +import { type InternalFormServiceContext } from '../context/index.js'; import type { FormSummary } from '../types.js'; import { base64ToUint8Array } from '../util/base64.js'; @@ -14,10 +14,12 @@ type InitializeFormError = { type InitializeFormResult = { timestamp: string; id: string; + jobId?: string; + status: 'ready' | 'processing'; }; export type InitializeForm = ( - ctx: FormServiceContext, + ctx: InternalFormServiceContext, opts: | unknown | { @@ -50,8 +52,9 @@ const optionSchema = z.object({ }); /** - * Asynchronously initializes a new form based on the provided context and options. Handles schema validation, - * document import (parses uploaded PDF), builds a Blueprint, and saves to the repository. + * Asynchronously initializes a new form based on the provided context and options. + * If a document is provided, creates the form immediately and processes the PDF asynchronously. + * Otherwise, creates a form with the provided summary. */ export const initializeForm: InitializeForm = async (ctx, opts) => { if (!ctx.isUserLoggedIn()) { @@ -63,6 +66,7 @@ export const initializeForm: InitializeForm = async (ctx, opts) => { const parseResult = optionSchema.safeParse(opts); if (!parseResult.success) { + console.error('Invalid options:', parseResult.error); return failure({ status: 400, message: 'Invalid options', @@ -70,56 +74,164 @@ export const initializeForm: InitializeForm = async (ctx, opts) => { } const { document, summary } = parseResult.data; + // Create empty blueprint const builder = new BlueprintBuilder(ctx.config); - if (document !== undefined) { - const parsePdfResult = await ctx - .parsePdf(document.data) - .then(result => success(result)) - .catch(err => - failure({ - status: 400, - message: `Failed to parse PDF: ${err.message}`, - }) - ); - if (!parsePdfResult.success) { - return parsePdfResult; - } - const { parsedPdf } = parsePdfResult.data; + if (summary) { + builder.setFormSummary(summary); + } else if (document) { builder.setFormSummary({ - title: parsedPdf.title || document.fileName, - description: parsedPdf.description, + title: document.fileName, + description: '', + }); + } + + // Step 1: Create form in database (empty, draft state) + const formResult = await ctx.repository.addForm(builder.form); + if (!formResult.success) { + console.error('Failed to add form:', formResult.error); + return failure({ + status: 500, + message: formResult.error, }); + } + const formId = formResult.data.id; + + // Step 2: If document provided, store it and initiate async processing + if (document !== undefined) { const fileName = document.fileName.split('/').pop() || 'my-form.pdf'; + + // Store document (without extract, will be filled by job processing) const addDocumentResult = await ctx.repository.addDocument({ fileName, data: document.data, - extract: parsePdfResult.data, + extract: undefined, }); + if (!addDocumentResult.success) { return failure({ status: 500, message: `Failed to add document: ${addDocumentResult.error}`, }); } + + const documentId = addDocumentResult.data.id; + + // Create job record (status: 'processing') + const jobResult = await ctx.repository.createFormJob({ + formId, + jobType: 'import-pdf', + metadata: { + documentId, + fileName, + userId: ctx.getUserId?.() || 'system', + }, + }); + + if (!jobResult.success) { + return failure({ + status: 500, + message: 'Failed to create processing job', + }); + } + + const job = jobResult.data; + + // Step 3: Fire async processing (don't await!) + processFormDocumentAsync(ctx, formId, job.id, documentId).catch(err => { + console.error('Async form processing failed:', err); + // Error already logged to database by processFormDocumentAsync + }); + + // Step 4: Return immediately + return success({ + id: formId, + timestamp: formResult.data.timestamp, + jobId: job.id, + status: 'processing', + }); + } + + // No document, form is ready immediately + return success({ + id: formId, + timestamp: formResult.data.timestamp, + status: 'ready', + }); +}; + +/** + * Async function that processes PDF and updates form + job. + * Runs in background, not awaited by HTTP request. + */ +async function processFormDocumentAsync( + ctx: InternalFormServiceContext, + formId: string, + jobId: string, + documentId: string +): Promise { + try { + // Get document data + const documentResult = await ctx.repository.getDocument(documentId); + if (!documentResult.success) { + await ctx.repository.failFormJob(jobId, { + message: `Document not found: ${documentResult.error}`, + }); + return; + } + + // Parse PDF via Bedrock + const parsePdfResult = await ctx.parsePdf(documentResult.data.data); + const { parsedPdf, fields } = parsePdfResult; + + // Get current form + const formResult = await ctx.repository.getForm(formId); + if (!formResult.success || !formResult.data) { + await ctx.repository.failFormJob(jobId, { + message: 'Form not found', + }); + return; + } + + // Build updated form with parsed patterns + const builder = new BlueprintBuilder(ctx.config, formResult.data); + + // Update summary from parsed PDF + builder.setFormSummary({ + title: parsedPdf.title || documentResult.data.path || 'Untitled', + description: parsedPdf.description || '', + }); + + // Add document reference await builder.addDocumentRef({ - id: addDocumentResult.data.id, + id: documentId, extract: parsedPdf, }); - } - if (summary) { - builder.setFormSummary(summary); - } + // Save updated form + const saveResult = await ctx.repository.saveForm(formId, builder.form); + if (!saveResult.success) { + await ctx.repository.failFormJob(jobId, { + message: `Failed to save form: ${saveResult.error}`, + }); + return; + } - const result = await ctx.repository.addForm(builder.form); - if (!result.success) { - console.error('Failed to add form:', result.error); - return failure({ - status: 500, - message: result.error, + // Mark job as completed + await ctx.repository.completeFormJob(jobId, { + patternsAdded: Object.keys(builder.form.patterns).length, + fieldsExtracted: Object.keys(fields).length, + documentId, + }); + + console.log(`Form ${formId} processed successfully`); + } catch (err) { + // Catch any unexpected errors + console.error('Unexpected error in processFormDocumentAsync:', err); + await ctx.repository.failFormJob(jobId, { + message: (err as Error).message, + stack: (err as Error).stack, }); } - return result; -}; +} diff --git a/packages/forms/src/services/save-form.test.ts b/packages/forms/src/services/save-form.test.ts index e4964cf1..e6486b11 100644 --- a/packages/forms/src/services/save-form.test.ts +++ b/packages/forms/src/services/save-form.test.ts @@ -4,7 +4,7 @@ import { createForm, generatePatternId } from '../index.js'; import { createTestFormServiceContext } from '../testing.js'; import { saveForm } from './save-form.js'; -import { success } from '@gsa-tts/forms-common'; +import { success } from '@flexion/forms-common'; const TEST_FORM = createForm({ title: 'Form Title', description: '' }); const formSummaryId = generatePatternId(); diff --git a/packages/forms/src/services/save-form.ts b/packages/forms/src/services/save-form.ts index 454c9310..256fbf30 100644 --- a/packages/forms/src/services/save-form.ts +++ b/packages/forms/src/services/save-form.ts @@ -1,4 +1,4 @@ -import { type Result, failure, success } from '@gsa-tts/forms-common'; +import { type Result, failure, success } from '@flexion/forms-common'; import { type FormServiceContext } from '../context/index.js'; import { type Blueprint } from '../types.js'; diff --git a/packages/forms/src/services/submit-form.ts b/packages/forms/src/services/submit-form.ts index 550b2f04..009a2cd5 100644 --- a/packages/forms/src/services/submit-form.ts +++ b/packages/forms/src/services/submit-form.ts @@ -1,14 +1,14 @@ -import { failure, success, type Result } from '@gsa-tts/forms-common'; +import { failure, success, type Result } from '@flexion/forms-common'; import { type FormServiceContext } from '../context/index.js'; -import { submitPage } from '../patterns/page-set/submit'; -import { downloadPackageHandler } from '../patterns/package-download/submit'; +import { submitPage } from '../patterns/page-set/submit.js'; +import { downloadPackageHandler } from '../patterns/package-download/submit.js'; import { repeaterAddRowHandler, repeaterDeleteRowHandler, -} from '../patterns/repeater/submit'; +} from '../patterns/repeater/submit.js'; import { type FormRoute } from '../route-data.js'; -import { SubmissionRegistry } from '../submission'; +import { SubmissionRegistry } from '../submission.js'; import { createFormSession, type FormSession, diff --git a/packages/forms/src/submission.ts b/packages/forms/src/submission.ts index cb79c107..1a81f2f5 100644 --- a/packages/forms/src/submission.ts +++ b/packages/forms/src/submission.ts @@ -1,15 +1,15 @@ import * as z from 'zod'; -import { type Result, failure, success } from '@gsa-tts/forms-common'; +import { type Result, failure, success } from '@flexion/forms-common'; import { type FormConfig, type Pattern, type PatternId, getPattern, -} from './pattern'; -import { type FormSession } from './session'; -import { type Blueprint, type DocumentFieldMap } from '.'; +} from './pattern.js'; +import { type FormSession } from './session.js'; +import { type Blueprint, type DocumentFieldMap } from './index.js'; export type SubmitHandlerContext = { config: FormConfig; diff --git a/packages/forms/src/testing.ts b/packages/forms/src/testing.ts index 02575637..77007aa5 100644 --- a/packages/forms/src/testing.ts +++ b/packages/forms/src/testing.ts @@ -1,29 +1,36 @@ -import { type DatabaseContext } from '@gsa-tts/forms-database'; -import { createInMemoryDatabaseContext } from '@gsa-tts/forms-database/context'; +import { type DatabaseContext } from '@flexion/forms-database'; +import { createInMemoryDatabaseContext } from '@flexion/forms-database/context'; -import type { FormServiceContext } from './context'; -import { type ParsePdf, parsePdf } from './documents'; +import type { InternalFormServiceContext } from './context'; +import type { PdfParser } from './documents/pdf/services/parser-interface'; +import { parsePdf as parsePdfCore } from './documents/pdf'; +import { createTestPdfParser } from './documents/pdf/context'; import { defaultFormConfig } from './patterns'; import { createFormsRepository } from './repository'; type Options = { isUserLoggedIn: () => boolean; - parsedPdf: ParsePdf; + parser: PdfParser; }; export const createTestFormServiceContext = async ( opts?: Partial -): Promise => { +): Promise => { const db: DatabaseContext = await createInMemoryDatabaseContext(); const repository = createFormsRepository({ db, formConfig: defaultFormConfig, }); + const parser = opts?.parser || createTestPdfParser(); + const parsePdf = (pdfBytes: Uint8Array) => + parsePdfCore({ parser, formConfig: defaultFormConfig }, pdfBytes); + return { repository, config: defaultFormConfig, isUserLoggedIn: opts?.isUserLoggedIn || (() => true), - parsePdf: opts?.parsedPdf || parsePdf, + parser, + parsePdf, }; }; diff --git a/packages/forms/src/types.ts b/packages/forms/src/types.ts index d27d6bc9..1a06bdcc 100644 --- a/packages/forms/src/types.ts +++ b/packages/forms/src/types.ts @@ -1,5 +1,5 @@ -import { type DocumentFieldMap } from './documents/types'; -import { type PatternId, type PatternMap } from './pattern'; +import { type DocumentFieldMap } from './documents/types.js'; +import { type PatternId, type PatternMap } from './pattern.js'; export type Blueprint = { summary: FormSummary; diff --git a/packages/forms/src/util/workspace-root.ts b/packages/forms/src/util/workspace-root.ts new file mode 100644 index 00000000..c7afc92e --- /dev/null +++ b/packages/forms/src/util/workspace-root.ts @@ -0,0 +1,36 @@ +import { existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +/** + * Finds the workspace root by looking for pnpm-workspace.yaml. + * Walks up the directory tree from the current file location. + * + * @returns Absolute path to the workspace root + * @throws Error if workspace root cannot be found + */ +export const findWorkspaceRoot = (): string => { + // Start from the directory containing this file + let currentDir = dirname(fileURLToPath(import.meta.url)); + + // Walk up the directory tree looking for pnpm-workspace.yaml + while (currentDir !== dirname(currentDir)) { + const workspaceFile = join(currentDir, 'pnpm-workspace.yaml'); + if (existsSync(workspaceFile)) { + return currentDir; + } + currentDir = dirname(currentDir); + } + + throw new Error('Could not find workspace root (pnpm-workspace.yaml)'); +}; + +/** + * Gets the default shared cache directory path for the workspace. + * All tests and CLI tools should use this to ensure caches are shared. + * + * @returns Absolute path to __fixtures__/ai-cache at workspace root + */ +export const getDefaultCachePath = (): string => { + return join(findWorkspaceRoot(), 'packages', 'forms', 'fixtures', 'ai-cache'); +}; diff --git a/packages/forms/src/util/zod.ts b/packages/forms/src/util/zod.ts index 3f92f858..c1885cef 100644 --- a/packages/forms/src/util/zod.ts +++ b/packages/forms/src/util/zod.ts @@ -1,6 +1,6 @@ import * as z from 'zod'; -import * as r from '@gsa-tts/forms-common'; +import * as r from '@flexion/forms-common'; import { type FormError, type FormErrors, type Pattern } from '../index.js'; @@ -59,12 +59,18 @@ export const convertZodErrorToFormErrors = ( zodError: z.ZodError ): FormErrors => { const formErrors: FormErrors = {}; - zodError.errors.forEach(error => { + zodError.issues.forEach((error: z.ZodIssue) => { const path = error.path.join('.'); if (error.code === 'too_small' && error.minimum === 1) { + // Replace default Zod message with consistent custom message + const message = + error.message === 'String must contain at least 1 character(s)' || + error.message.startsWith('Too small') + ? 'String must contain at least 1 character(s)' + : error.message; formErrors[path] = { type: 'required', - message: error.message, + message, }; } else { formErrors[path] = { @@ -82,6 +88,8 @@ export const convertZodErrorToFormErrors = ( const convertZodErrorToFormError = (zodError: z.ZodError): FormError => { return { type: 'custom', - message: zodError.errors.map(error => error.message).join(', '), + message: zodError.issues + .map((error: z.ZodIssue) => error.message) + .join(', '), }; }; diff --git a/packages/forms/vitest.config.ts b/packages/forms/vitest.config.ts index d89d7593..6428888e 100644 --- a/packages/forms/vitest.config.ts +++ b/packages/forms/vitest.config.ts @@ -1,6 +1,6 @@ import { defineConfig, mergeConfig } from 'vitest/config'; -import { getVitestDatabaseContainerGlobalSetupPath } from '@gsa-tts/forms-database'; +import { getVitestDatabaseContainerGlobalSetupPath } from '@flexion/forms-database'; import sharedTestConfig from '../../vitest.shared'; export default mergeConfig( diff --git a/packages/server/CHANGELOG.md b/packages/server/CHANGELOG.md index c0451958..1415f353 100644 --- a/packages/server/CHANGELOG.md +++ b/packages/server/CHANGELOG.md @@ -1,5 +1,53 @@ # @gsa-tts/forms-server +## 0.2.3 + +### Patch Changes + +- Updated dependencies [82bb94d] +- Updated dependencies [f3bc441] + - @flexion/forms-design@0.2.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies + - @flexion/forms-design@0.2.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies + - @flexion/forms-design@0.2.1 + +## 0.2.0 + +### Minor Changes + +- 0a171f1: Initial Flexion Forms release + +### Patch Changes + +- Updated dependencies [0a171f1] + - @flexion/forms-auth@0.2.0 + - @flexion/forms-common@0.2.0 + - @flexion/forms-database@0.2.0 + - @flexion/forms-design@0.2.0 + - @flexion/forms-core@0.2.0 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [bbd065d] + - @flexion/forms-common@0.1.3 + - @flexion/forms-design@0.1.3 + - @flexion/forms-auth@0.1.3 + - @flexion/forms-database@0.1.3 + - @flexion/forms-core@0.1.3 + ## 0.1.2 ### Patch Changes diff --git a/packages/server/README.md b/packages/server/README.md index 18c1bc39..ef15366c 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -1,4 +1,4 @@ -# @gsa-tts/forms-server +# @flexion/forms-server The Forms Platform web server. @@ -21,7 +21,7 @@ pnpm dev To start the provided Express server: ```typescript -import { createServer } from '@gsa-tts/forms-server'; +import { createServer } from '@flexion/forms-server'; const port = process.env.PORT || 4321; diff --git a/packages/server/astro.config.mjs b/packages/server/astro.config.mjs index 85e2b0d6..1a53737c 100644 --- a/packages/server/astro.config.mjs +++ b/packages/server/astro.config.mjs @@ -29,6 +29,11 @@ export default defineConfig({ define: { 'import.meta.env.GITHUB': JSON.stringify(githubRepository), }, + resolve: { + conditions: process.env.NODE_ENV === 'production' + ? ['production', 'import', 'module', 'browser', 'default'] + : ['development', 'import', 'module', 'browser', 'default'], + }, }, }); diff --git a/packages/server/package.json b/packages/server/package.json index c7f3f6f8..975d52ad 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,12 +1,15 @@ { - "name": "@gsa-tts/forms-server", + "name": "@flexion/forms-server", "type": "module", - "version": "0.1.2", + "version": "0.2.3", "main": "dist/handler.js", "types": "handler.ts", "files": [ "dist" ], + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "scripts": { "astro": "astro", "build": "astro check && astro build && pnpm build:handler", @@ -20,11 +23,11 @@ "@astrojs/check": "^0.9.4", "@astrojs/node": "^9.0.0", "@astrojs/react": "^4.1.2", - "@gsa-tts/forms-auth": "workspace:^", - "@gsa-tts/forms-common": "workspace:*", - "@gsa-tts/forms-database": "workspace:*", - "@gsa-tts/forms-design": "workspace:*", - "@gsa-tts/forms-core": "workspace:*", + "@flexion/forms-auth": "workspace:^", + "@flexion/forms-common": "workspace:*", + "@flexion/forms-database": "workspace:*", + "@flexion/forms-design": "workspace:*", + "@flexion/forms-core": "workspace:*", "astro": "^5.1.3", "express": "^4.21.0", "jwt-decode": "^4.0.0", diff --git a/packages/server/src/components/AppAvailableFormList.tsx b/packages/server/src/components/AppAvailableFormList.tsx index 191b72aa..19b291d6 100644 --- a/packages/server/src/components/AppAvailableFormList.tsx +++ b/packages/server/src/components/AppAvailableFormList.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import { AvailableFormList } from '@gsa-tts/forms-design'; +import { AvailableFormList } from '@flexion/forms-design'; import { type AppContext } from '../config/context.js'; import { getFormManagerUrlById, getFormUrl } from '../routes.js'; diff --git a/packages/server/src/components/AppForm.tsx b/packages/server/src/components/AppForm.tsx index 3a937c0d..b6af32f0 100644 --- a/packages/server/src/components/AppForm.tsx +++ b/packages/server/src/components/AppForm.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { defaultPatternComponents, Form } from '@gsa-tts/forms-design'; -import { type FormSession, defaultFormConfig } from '@gsa-tts/forms-core'; +import { defaultPatternComponents, Form } from '@flexion/forms-design'; +import { type FormSession, defaultFormConfig } from '@flexion/forms-core'; type AppFormProps = { uswdsRoot: `${string}/`; diff --git a/packages/server/src/components/AppFormManager.tsx b/packages/server/src/components/AppFormManager.tsx index 88297e4c..618a4a22 100644 --- a/packages/server/src/components/AppFormManager.tsx +++ b/packages/server/src/components/AppFormManager.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { FormManager } from '@gsa-tts/forms-design'; +import { FormManager } from '@flexion/forms-design'; import { FormServiceClient } from '../lib/api-client.js'; import { type AppContext } from '../config/context.js'; diff --git a/packages/server/src/components/Header.astro b/packages/server/src/components/Header.astro index 9d6bc966..16c6a32b 100644 --- a/packages/server/src/components/Header.astro +++ b/packages/server/src/components/Header.astro @@ -1,6 +1,6 @@ --- -import closeSvg from '@gsa-tts/forms-design/static/uswds/img/usa-icons/close.svg'; -import logoSvg from '@gsa-tts/forms-design/images/logo.svg'; +import closeSvg from '@flexion/forms-design/static/uswds/img/usa-icons/close.svg'; +import logoSvg from '@flexion/forms-design/images/logo.svg'; import { getServerContext, getUserSession } from '../config/astro.js'; import * as routes from '../routes'; diff --git a/packages/server/src/components/UsaBanner.astro b/packages/server/src/components/UsaBanner.astro index 6fbfd61e..2dd3f656 100644 --- a/packages/server/src/components/UsaBanner.astro +++ b/packages/server/src/components/UsaBanner.astro @@ -1,7 +1,7 @@ --- -import iconDotGov from '@gsa-tts/forms-design/static/uswds/img/icon-dot-gov.svg'; -import iconHttps from '@gsa-tts/forms-design/static/uswds/img/icon-https.svg'; -import usFlagSmall from '@gsa-tts/forms-design/static/uswds/img/us_flag_small.png'; +import iconDotGov from '@flexion/forms-design/static/uswds/img/icon-dot-gov.svg'; +import iconHttps from '@flexion/forms-design/static/uswds/img/icon-https.svg'; +import usFlagSmall from '@flexion/forms-design/static/uswds/img/us_flag_small.png'; ---

diff --git a/packages/server/src/config/astro.ts b/packages/server/src/config/astro.ts index 8cf541b8..053d1489 100644 --- a/packages/server/src/config/astro.ts +++ b/packages/server/src/config/astro.ts @@ -4,8 +4,8 @@ import { type AuthRepository, type LoginGovOptions, createAuthRepository, -} from '@gsa-tts/forms-auth'; -import { defaultFormConfig } from '@gsa-tts/forms-core'; +} from '@flexion/forms-auth'; +import { defaultFormConfig } from '@flexion/forms-core'; import { type AppContext } from './context.js'; import { type ServerOptions, createDevServerOptions } from './options.js'; @@ -65,7 +65,7 @@ const createDefaultAuthContext = async ({ loginGovOptions: LoginGovOptions; isUserAuthorized: (email: string) => Promise; }) => { - const { LoginGov, BaseAuthContext } = await import('@gsa-tts/forms-auth'); + const { LoginGov, BaseAuthContext } = await import('@flexion/forms-auth'); return new BaseAuthContext( authRepository, new LoginGov({ diff --git a/packages/server/src/config/context.ts b/packages/server/src/config/context.ts index 25d67923..9d46048b 100644 --- a/packages/server/src/config/context.ts +++ b/packages/server/src/config/context.ts @@ -1,5 +1,5 @@ -import { type AuthServiceContext } from '@gsa-tts/forms-auth'; -import { type FormConfig, type FormService } from '@gsa-tts/forms-core'; +import { type AuthServiceContext } from '@flexion/forms-auth'; +import { type FormConfig, type FormService } from '@flexion/forms-core'; import { type GithubRepository } from '../lib/github.js'; diff --git a/packages/server/src/config/options.ts b/packages/server/src/config/options.ts index 2c4fcf67..4bc90a40 100644 --- a/packages/server/src/config/options.ts +++ b/packages/server/src/config/options.ts @@ -1,8 +1,8 @@ import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; -import { type LoginGovOptions } from '@gsa-tts/forms-auth'; -import { DatabaseContext } from '@gsa-tts/forms-database'; +import { type LoginGovOptions } from '@flexion/forms-auth'; +import { DatabaseContext } from '@flexion/forms-database'; export type ServerOptions = { agencyBranding: boolean; @@ -14,7 +14,7 @@ export type ServerOptions = { export const createDevServerOptions = async (): Promise => { const { createFilesystemDatabaseContext } = await import( - '@gsa-tts/forms-database/context' + '@flexion/forms-database/context' ); const db = await createFilesystemDatabaseContext( join(dirname(fileURLToPath(import.meta.url)), '../main.db') @@ -38,7 +38,7 @@ export const createDevServerOptions = async (): Promise => { export const createTestServerOptions = async (): Promise => { const { createInMemoryDatabaseContext } = await import( - '@gsa-tts/forms-database/context' + '@flexion/forms-database/context' ); const db = await createInMemoryDatabaseContext(); return { diff --git a/packages/server/src/config/services.ts b/packages/server/src/config/services.ts index f6d5804c..df43ed6a 100644 --- a/packages/server/src/config/services.ts +++ b/packages/server/src/config/services.ts @@ -1,10 +1,10 @@ import { type FormService, createFormService, - createFormsRepository, defaultFormConfig, - parsePdf, -} from '@gsa-tts/forms-core'; +} from '@flexion/forms-core'; +import { createFormsRepository } from '@flexion/forms-core/repository'; +import { createProductionPdfParser } from '@flexion/forms-core/documents/pdf/context'; import { type ServerOptions } from './options.js'; export const createServerFormService = ( @@ -18,6 +18,6 @@ export const createServerFormService = ( }), config: defaultFormConfig, isUserLoggedIn: ctx.isUserLoggedIn, - parsePdf, + parser: createProductionPdfParser(options.db), }); }; diff --git a/packages/server/src/lib/api-client.ts b/packages/server/src/lib/api-client.ts index 3b8e7bb7..fbdb9f26 100644 --- a/packages/server/src/lib/api-client.ts +++ b/packages/server/src/lib/api-client.ts @@ -1,4 +1,4 @@ -import { type Result } from '@gsa-tts/forms-common'; +import { type Result } from '@flexion/forms-common'; import { type FormRoute, type FormSession, @@ -6,8 +6,10 @@ import { type Blueprint, type FormService, type FormSummary, -} from '@gsa-tts/forms-core'; -import { type FormServiceContext } from '@gsa-tts/forms-core/context'; + type FormStatusResponse, + type GetFormStatusError, +} from '@flexion/forms-core'; +import { type FormServiceContext } from '@flexion/forms-core/context'; type FormServiceClientContext = { baseUrl: string; @@ -37,7 +39,12 @@ export class FormServiceClient implements FormService { } ): Promise< Result< - { timestamp: string; id: string }, + { + timestamp: string; + id: string; + jobId?: string; + status: 'ready' | 'processing'; + }, { status: number; message: string } > > { @@ -136,6 +143,15 @@ export class FormServiceClient implements FormService { throw new Error('Not implemented'); } + async getFormStatus( + formId: string + ): Promise> { + const response = await fetch( + `${this.ctx.baseUrl}api/forms/${formId}/status` + ); + return await response.json(); + } + getContext() { return {} as unknown as FormServiceContext; } diff --git a/packages/server/src/lib/initialize.ts b/packages/server/src/lib/initialize.ts index 2c9f2d73..ddb01758 100644 --- a/packages/server/src/lib/initialize.ts +++ b/packages/server/src/lib/initialize.ts @@ -1,4 +1,4 @@ /** * Global initialization script. */ -import '@gsa-tts/forms-design'; +import '@flexion/forms-design'; diff --git a/packages/server/src/middleware.ts b/packages/server/src/middleware.ts index 66385300..32d4a4bb 100644 --- a/packages/server/src/middleware.ts +++ b/packages/server/src/middleware.ts @@ -1,6 +1,6 @@ import { defineMiddleware } from 'astro/middleware'; -import { processSessionCookie } from '@gsa-tts/forms-auth'; +import { processSessionCookie } from '@flexion/forms-auth'; import { getServerContext } from './config/astro.js'; diff --git a/packages/server/src/pages/api/forms/[id]/status.ts b/packages/server/src/pages/api/forms/[id]/status.ts new file mode 100644 index 00000000..a86e2645 --- /dev/null +++ b/packages/server/src/pages/api/forms/[id]/status.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from 'astro'; +import { getServerContext } from '../../../../config/astro.js'; + +export const GET: APIRoute = async context => { + const ctx = await getServerContext(context); + const formId = context.params.id; + + if (!formId) { + return new Response('Form ID is required', { status: 400 }); + } + + const result = await ctx.formService.getFormStatus(formId); + + return new Response(JSON.stringify(result), { + headers: { + 'Content-Type': 'application/json', + }, + status: result.success ? 200 : result.error.status, + }); +}; diff --git a/packages/server/src/pages/api/forms/index.ts b/packages/server/src/pages/api/forms/index.ts index 15780209..3e583d6c 100644 --- a/packages/server/src/pages/api/forms/index.ts +++ b/packages/server/src/pages/api/forms/index.ts @@ -17,6 +17,7 @@ export const POST: APIRoute = async context => { const ctx = await getServerContext(context); //const result = await ctx.formService.addForm(form); const result = await ctx.formService.initializeForm(input); + console.log(result.success ? 'Form initialized' : result.error); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json', diff --git a/packages/server/src/pages/forms/[id].astro b/packages/server/src/pages/forms/[id].astro index 4923beb9..16b6d971 100644 --- a/packages/server/src/pages/forms/[id].astro +++ b/packages/server/src/pages/forms/[id].astro @@ -2,7 +2,7 @@ import { type FormRoute, getRouteDataFromQueryString, -} from '@gsa-tts/forms-core'; +} from '@flexion/forms-core'; import { AppForm } from '../../components/AppForm'; import { @@ -15,13 +15,13 @@ import ContentLayout from '../../layouts/ContentLayout.astro'; const { id: formId } = getAstroRouteParams(Astro, ['id']); const ctx = await getServerContext(Astro); -const sessionId = Astro.cookies.get('form_session_id')?.value; +const sessionId = Astro.cookies.get(`form_session_id_${formId}`)?.value; const setFormSessionCookie = (sessionId?: string) => { if (sessionId) { - Astro.cookies.set('form_session_id', sessionId); + Astro.cookies.set(`form_session_id_${formId}`, sessionId); } else { - Astro.cookies.delete('form_session_id'); + Astro.cookies.delete(`form_session_id_${formId}`); } }; diff --git a/packages/server/src/pages/forms/[id].test.ts b/packages/server/src/pages/forms/[id].test.ts index a6f15c14..6c9ff8db 100644 --- a/packages/server/src/pages/forms/[id].test.ts +++ b/packages/server/src/pages/forms/[id].test.ts @@ -11,7 +11,7 @@ import { type PagePattern, type PageSetPattern, createForm, -} from '@gsa-tts/forms-core'; +} from '@flexion/forms-core'; import { type ServerOptions, @@ -235,7 +235,7 @@ const submitForm = async ( request: new Request(`http://localhost/forms/${formId}`, { method: 'POST', body: formData, - headers: sessionId ? { Cookie: `form_session_id=${sessionId}` } : {}, + headers: sessionId ? { Cookie: `form_session_id_${formId}=${sessionId}` } : {}, }), }); diff --git a/packages/server/src/pages/signin/callback.ts b/packages/server/src/pages/signin/callback.ts index 2d9f3044..82afeecf 100644 --- a/packages/server/src/pages/signin/callback.ts +++ b/packages/server/src/pages/signin/callback.ts @@ -1,6 +1,6 @@ import type { APIContext } from 'astro'; -import { processProviderCallback } from '@gsa-tts/forms-auth'; +import { processProviderCallback } from '@flexion/forms-auth'; import { getServerContext } from '../../config/astro.js'; import * as routes from '../../routes.js'; diff --git a/packages/server/src/pages/signin/index.ts b/packages/server/src/pages/signin/index.ts index a2522538..264619ff 100644 --- a/packages/server/src/pages/signin/index.ts +++ b/packages/server/src/pages/signin/index.ts @@ -1,5 +1,5 @@ import type { APIContext } from 'astro'; -import { getProviderRedirect } from '@gsa-tts/forms-auth'; +import { getProviderRedirect } from '@flexion/forms-auth'; import { getServerContext } from '../../config/astro.js'; diff --git a/packages/server/src/pages/signout/confirm.ts b/packages/server/src/pages/signout/confirm.ts index 63599e4c..772ec9e8 100644 --- a/packages/server/src/pages/signout/confirm.ts +++ b/packages/server/src/pages/signout/confirm.ts @@ -1,6 +1,6 @@ import type { APIContext } from 'astro'; -import { logOut } from '@gsa-tts/forms-auth'; +import { logOut } from '@flexion/forms-auth'; import { getServerContext } from '../../config/astro.js'; import * as routes from '../../routes.js'; diff --git a/packages/server/src/styles.css b/packages/server/src/styles.css index 40f8ac4f..5809a82e 100644 --- a/packages/server/src/styles.css +++ b/packages/server/src/styles.css @@ -1 +1,2 @@ -@import '@gsa-tts/forms-design/static/uswds/styles/styles.css'; +@import '@flexion/forms-design/static/uswds/styles/styles.css'; +@import '@flexion/forms-design/dist/assets/forms-design.css'; diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index d8f40787..6e11151a 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -7,7 +7,8 @@ "moduleResolution": "NodeNext", "jsx": "react", "resolveJsonModule": true, - "types": ["@testing-library/jest-dom"] + "types": ["@testing-library/jest-dom"], + "customConditions": ["development"] }, "include": [ "globals.d.ts", diff --git a/packages/server/vitest.config.browser.ts b/packages/server/vitest.config.browser.ts index 64cda4e7..d1dc5eb7 100644 --- a/packages/server/vitest.config.browser.ts +++ b/packages/server/vitest.config.browser.ts @@ -5,7 +5,7 @@ export default defineConfig({ exclude: ['chromium-bidi', 'fsevents'], }, test: { - name: '@gsa-tts/forms-server:browser', + name: '@flexion/forms-server:browser', browser: { provider: 'playwright', enabled: true, diff --git a/packages/server/vitest.config.ts b/packages/server/vitest.config.ts index 09447d81..eb8bff8e 100644 --- a/packages/server/vitest.config.ts +++ b/packages/server/vitest.config.ts @@ -5,7 +5,7 @@ import { configDefaults } from 'vitest/config'; export default getViteConfig({ test: { ...configDefaults, - name: '@gsa-tts/forms-server:node', + name: '@flexion/forms-server:node', setupFiles: ['./vitest.setup.ts'], environment: 'node', include: ['src/**/*.test.ts'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e27193f..e649ac55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,10 +28,10 @@ importers: version: 22.14.0 '@vitest/browser': specifier: ^3.0.5 - version: 3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1))(vitest@3.1.1) + version: 3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.1.1) '@vitest/coverage-v8': specifier: ^3.0.5 - version: 3.1.1(@vitest/browser@3.1.1)(vitest@3.1.1) + version: 3.1.1(@vitest/browser@3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.1.1))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) '@vitest/ui': specifier: ^3.0.5 version: 3.1.1(vitest@3.1.1) @@ -64,7 +64,10 @@ importers: version: 10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.8.2) tsup: specifier: ^8.3.0 - version: 8.4.0(@microsoft/api-extractor@7.52.2(@types/node@22.14.0))(@swc/core@1.11.16)(jiti@2.4.2)(postcss@8.5.3)(typescript@5.8.2)(yaml@2.7.1) + version: 8.4.0(@microsoft/api-extractor@7.52.2(@types/node@22.14.0))(@swc/core@1.11.16)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.20.6)(typescript@5.8.2)(yaml@2.7.1) + tsx: + specifier: ^4.20.6 + version: 4.20.6 turbo: specifier: ^2.1.3 version: 2.5.0 @@ -73,20 +76,23 @@ importers: version: 5.8.2 vitest: specifier: ^3.0.5 - version: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + version: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) vitest-mock-extended: specifier: ^2.0.2 - version: 2.0.2(typescript@5.8.2)(vitest@3.1.1) + version: 2.0.2(typescript@5.8.2)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) apps/cli: dependencies: - '@gsa-tts/forms-auth': + '@flexion/forms-auth': specifier: workspace:^ version: link:../../packages/auth - '@gsa-tts/forms-database': + '@flexion/forms-core': + specifier: workspace:* + version: link:../../packages/forms + '@flexion/forms-database': specifier: workspace:* version: link:../../packages/database - '@gsa-tts/forms-infra-core': + '@flexion/forms-infra-core': specifier: workspace:* version: link:../../infra/core commander: @@ -95,13 +101,13 @@ importers: apps/sandbox: dependencies: - '@gsa-tts/forms-database': + '@flexion/forms-database': specifier: workspace:* version: link:../../packages/database - '@gsa-tts/forms-infra-core': + '@flexion/forms-infra-core': specifier: workspace:* version: link:../../infra/core - '@gsa-tts/forms-server': + '@flexion/forms-server': specifier: workspace:* version: link:../../packages/server devDependencies: @@ -114,13 +120,13 @@ importers: apps/server-doj: dependencies: - '@gsa-tts/forms-database': + '@flexion/forms-database': specifier: workspace:* version: link:../../packages/database - '@gsa-tts/forms-infra-core': + '@flexion/forms-infra-core': specifier: workspace:* version: link:../../infra/core - '@gsa-tts/forms-server': + '@flexion/forms-server': specifier: workspace:* version: link:../../packages/server devDependencies: @@ -136,18 +142,18 @@ importers: '@astrojs/react': specifier: ^3.6.1 version: 3.6.3(@types/node@22.14.0)(@types/react-dom@19.1.1(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass-embedded@1.83.4)(terser@5.39.0) - '@gsa-tts/forms-common': + '@flexion/forms-common': specifier: workspace:* version: link:../../packages/common - '@gsa-tts/forms-core': + '@flexion/forms-core': specifier: workspace:* version: link:../../packages/forms - '@gsa-tts/forms-design': + '@flexion/forms-design': specifier: workspace:* version: link:../../packages/design astro: specifier: ^4.16.18 - version: 4.16.18(@types/node@22.14.0)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(typescript@5.9.0-dev.20250428) + version: 4.16.18(@types/node@22.14.0)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(typescript@6.0.0-dev.20251008) qs: specifier: ^6.13.0 version: 6.14.0 @@ -172,7 +178,7 @@ importers: devDependencies: '@astrojs/check': specifier: ^0.4.1 - version: 0.4.1(prettier@3.5.3)(typescript@5.9.0-dev.20250428) + version: 0.4.1(prettier@3.5.3)(typescript@6.0.0-dev.20251008) '@size-limit/preset-app': specifier: ^11.1.6 version: 11.2.0(size-limit@11.2.0) @@ -188,7 +194,7 @@ importers: e2e: dependencies: - '@gsa-tts/forms-common': + '@flexion/forms-common': specifier: workspace:* version: link:../packages/common devDependencies: @@ -207,7 +213,7 @@ importers: '@aws-cdk/aws-apprunner-alpha': specifier: 2.184.1-alpha.0 version: 2.184.1-alpha.0(aws-cdk-lib@2.184.1(constructs@10.4.2))(constructs@10.4.2) - '@gsa-tts/forms-infra-core': + '@flexion/forms-infra-core': specifier: workspace:* version: link:../core aws-cdk: @@ -232,9 +238,12 @@ importers: '@aws-sdk/client-ssm': specifier: ^3.750.0 version: 3.782.0 - '@gsa-tts/forms-infra-aws-cdk': + '@flexion/forms-infra-aws-cdk': specifier: workspace:* version: link:../aws-cdk + '@flexion/forms-infra-core': + specifier: workspace:* + version: link:../core cdktf: specifier: ^0.20.11 version: 0.20.11(constructs@10.4.2) @@ -253,22 +262,22 @@ importers: '@aws-sdk/client-ssm': specifier: ^3.624.0 version: 3.782.0 - '@gsa-tts/forms-common': + '@flexion/forms-common': specifier: workspace:* version: link:../../packages/common - '@gsa-tts/forms-core': + '@flexion/forms-core': specifier: workspace:* version: link:../../packages/forms zod: - specifier: ^3.23.8 - version: 3.24.2 + specifier: ^4.1.11 + version: 4.1.11 packages/auth: dependencies: - '@gsa-tts/forms-common': + '@flexion/forms-common': specifier: workspace:^ version: link:../common - '@gsa-tts/forms-database': + '@flexion/forms-database': specifier: workspace:* version: link:../database '@lucia-auth/adapter-postgresql': @@ -295,13 +304,13 @@ importers: version: 7.6.12 vitest-fetch-mock: specifier: ^0.4.3 - version: 0.4.5(vitest@3.1.1) + version: 0.4.5(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) packages/common: {} packages/database: dependencies: - '@gsa-tts/forms-common': + '@flexion/forms-common': specifier: workspace:* version: link:../common '@types/pg': @@ -331,7 +340,7 @@ importers: version: 10.24.0 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.2)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)) + version: 5.1.4(typescript@5.8.2)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) packages/design: dependencies: @@ -344,10 +353,10 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@18.3.1) - '@gsa-tts/forms-common': + '@flexion/forms-common': specifier: workspace:* version: link:../common - '@gsa-tts/forms-core': + '@flexion/forms-core': specifier: workspace:* version: link:../forms '@size-limit/preset-big-lib': @@ -416,22 +425,22 @@ importers: version: 8.6.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.12(prettier@3.5.3)) '@storybook/experimental-addon-test': specifier: ^8.4.7 - version: 8.6.12(@vitest/browser@3.1.1)(@vitest/runner@3.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.12(prettier@3.5.3))(vitest@3.1.1) + version: 8.6.12(@vitest/browser@3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.1.1))(@vitest/runner@3.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.12(prettier@3.5.3))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) '@storybook/preview-api': specifier: ^8.4.7 version: 8.6.12(storybook@8.6.12(prettier@3.5.3)) '@storybook/react': specifier: ^8.4.7 - version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.12(prettier@3.5.3))(typescript@5.9.0-dev.20250428) + version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.12(prettier@3.5.3))(typescript@6.0.0-dev.20251008) '@storybook/react-vite': specifier: ^8.4.7 - version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.39.0)(storybook@8.6.12(prettier@3.5.3))(typescript@5.9.0-dev.20250428)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)) + version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.39.0)(storybook@8.6.12(prettier@3.5.3))(typescript@6.0.0-dev.20251008)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) '@storybook/test': specifier: ^8.4.7 version: 8.6.12(storybook@8.6.12(prettier@3.5.3)) '@storybook/test-runner': specifier: ^0.21.0 - version: 0.21.3(@types/node@22.14.0)(storybook@8.6.12(prettier@3.5.3))(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + version: 0.21.3(@types/node@22.14.0)(storybook@8.6.12(prettier@3.5.3))(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) '@storybook/types': specifier: ^8.4.7 version: 8.6.12(storybook@8.6.12(prettier@3.5.3)) @@ -455,16 +464,16 @@ importers: version: 19.1.1(@types/react@18.3.20) '@typescript-eslint/eslint-plugin': specifier: ^7.18.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.0-dev.20250428))(eslint@8.57.1)(typescript@5.9.0-dev.20250428) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@6.0.0-dev.20251008))(eslint@8.57.1)(typescript@6.0.0-dev.20251008) '@typescript-eslint/parser': specifier: ^7.18.0 - version: 7.18.0(eslint@8.57.1)(typescript@5.9.0-dev.20250428) + version: 7.18.0(eslint@8.57.1)(typescript@6.0.0-dev.20251008) '@uswds/compile': specifier: ^1.2.2 - version: 1.2.2(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + version: 1.2.2(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)) + version: 4.3.4(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -491,22 +500,37 @@ importers: version: 18.3.1(react@18.3.1) vite: specifier: ^6.0.9 - version: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + version: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) vite-plugin-dts: specifier: ^4.4.0 - version: 4.5.3(@types/node@22.14.0)(rollup@4.39.0)(typescript@5.9.0-dev.20250428)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)) + version: 4.5.3(@types/node@22.14.0)(rollup@4.39.0)(typescript@6.0.0-dev.20251008)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) wait-on: specifier: ^7.2.0 version: 7.2.0 packages/forms: dependencies: - '@gsa-tts/forms-common': + '@ai-sdk/amazon-bedrock': + specifier: ^3.0.30 + version: 3.0.30(zod@4.1.11) + '@aws-sdk/credential-providers': + specifier: ^3.901.0 + version: 3.901.0 + '@aws-sdk/types': + specifier: ^3.901.0 + version: 3.901.0 + '@flexion/forms-common': specifier: workspace:* version: link:../common - '@gsa-tts/forms-database': + '@flexion/forms-database': specifier: workspace:* version: link:../database + ai: + specifier: ^5.0.59 + version: 5.0.59(zod@4.1.11) + kysely: + specifier: ^0.27.4 + version: 0.27.6 pdf-lib: specifier: ^1.17.1 version: 1.17.1 @@ -517,8 +541,8 @@ importers: specifier: ^4.1.0 version: 4.1.0 zod: - specifier: ^3.23.8 - version: 3.24.2 + specifier: ^4.1.11 + version: 4.1.11 devDependencies: '@types/qs': specifier: ^6.9.15 @@ -528,31 +552,31 @@ importers: dependencies: '@astrojs/check': specifier: ^0.9.4 - version: 0.9.4(prettier@3.5.3)(typescript@5.9.0-dev.20250428) + version: 0.9.4(prettier@3.5.3)(typescript@6.0.0-dev.20251008) '@astrojs/node': specifier: ^9.0.0 - version: 9.1.3(astro@5.6.0(@types/node@22.14.0)(jiti@2.4.2)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(typescript@5.9.0-dev.20250428)(yaml@2.7.1)) + version: 9.1.3(astro@5.6.0(@types/node@22.14.0)(aws4fetch@1.0.20)(jiti@2.4.2)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(typescript@6.0.0-dev.20251008)(yaml@2.7.1)) '@astrojs/react': specifier: ^4.1.2 - version: 4.2.3(@types/node@22.14.0)(@types/react-dom@19.1.1(@types/react@18.3.20))(@types/react@18.3.20)(jiti@2.4.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) - '@gsa-tts/forms-auth': + version: 4.2.3(@types/node@22.14.0)(@types/react-dom@19.1.1(@types/react@18.3.20))(@types/react@18.3.20)(jiti@2.4.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + '@flexion/forms-auth': specifier: workspace:^ version: link:../auth - '@gsa-tts/forms-common': + '@flexion/forms-common': specifier: workspace:* version: link:../common - '@gsa-tts/forms-core': + '@flexion/forms-core': specifier: workspace:* version: link:../forms - '@gsa-tts/forms-database': + '@flexion/forms-database': specifier: workspace:* version: link:../database - '@gsa-tts/forms-design': + '@flexion/forms-design': specifier: workspace:* version: link:../design astro: specifier: ^5.1.3 - version: 5.6.0(@types/node@22.14.0)(jiti@2.4.2)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(typescript@5.9.0-dev.20250428)(yaml@2.7.1) + version: 5.6.0(@types/node@22.14.0)(aws4fetch@1.0.20)(jiti@2.4.2)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(typescript@6.0.0-dev.20251008)(yaml@2.7.1) express: specifier: ^4.21.0 version: 4.21.2 @@ -596,6 +620,34 @@ packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} + '@ai-sdk/amazon-bedrock@3.0.30': + resolution: {integrity: sha512-aF21FFpTusWAdXc70bqA8SIFnIfCokwrp4G8efMETIRXIH+N5QGd6UYEMbfMfwx4P9iN9v3oUwsHsRtr87TKPQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/anthropic@2.0.23': + resolution: {integrity: sha512-ZEBiiv1UhjGjBwUU63pFhLK5LCSlNDb1idY9K1oZHm5/Fda1cuTojf32tOp0opH0RPbPAN/F8fyyNjbU33n9Kw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/gateway@1.0.32': + resolution: {integrity: sha512-TQRIM63EI/ccJBc7RxeB8nq/CnGNnyl7eu5stWdLwL41stkV5skVeZJe0QRvFbaOrwCkgUVE0yrUqJi4tgDC1A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@3.0.10': + resolution: {integrity: sha512-T1gZ76gEIwffep6MWI0QNy9jgoybUHE7TRaHB5k54K8mF91ciGFlbtCGxDYhMH3nCRergKwYFIDeFF0hJSIQHQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@2.0.0': + resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} + engines: {node: '>=18'} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -704,6 +756,10 @@ packages: - jsonschema - semver + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + '@aws-crypto/sha256-browser@5.2.0': resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} @@ -717,6 +773,10 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + '@aws-sdk/client-cognito-identity@3.901.0': + resolution: {integrity: sha512-cDJ+npYeAiS9u/52RwR0AHgneEF+rnyxiYm4d/c4FTI6xTQId3hSD0zdK0EgZ1wfoMk0/+5Ft6mYk0V6JN+cbQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/client-secrets-manager@3.782.0': resolution: {integrity: sha512-j2n8717Q7HeQgyXjdiUwuE0Pk80nOaDacWvEmmMr2w+DYXLvdXrOThbyy3jItt5XQw8mhaC8KZ4RfK6dqUlwGA==} engines: {node: '>=18.0.0'} @@ -729,74 +789,154 @@ packages: resolution: {integrity: sha512-5GlJBejo8wqMpSSEKb45WE82YxI2k73YuebjLH/eWDNQeE6VI5Bh9lA1YQ7xNkLLH8hIsb0pSfKVuwh0VEzVrg==} engines: {node: '>=18.0.0'} + '@aws-sdk/client-sso@3.901.0': + resolution: {integrity: sha512-sGyDjjkJ7ppaE+bAKL/Q5IvVCxtoyBIzN+7+hWTS/mUxWJ9EOq9238IqmVIIK6sYNIzEf9yhobfMARasPYVTNg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/core@3.775.0': resolution: {integrity: sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA==} engines: {node: '>=18.0.0'} + '@aws-sdk/core@3.901.0': + resolution: {integrity: sha512-brKAc3y64tdhyuEf+OPIUln86bRTqkLgb9xkd6kUdIeA5+qmp/N6amItQz+RN4k4O3kqkCPYnAd3LonTKluobw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-cognito-identity@3.901.0': + resolution: {integrity: sha512-irVFwiiEC+JRFQTZwI7264LOGXRjqdp3AvmqiEmmZS0+sJsEaF65prCs+nzw6J1WqQ6IZKClKKQsH7x8FfOPrQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-env@3.775.0': resolution: {integrity: sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-env@3.901.0': + resolution: {integrity: sha512-5hAdVl3tBuARh3zX5MLJ1P/d+Kr5kXtDU3xm1pxUEF4xt2XkEEpwiX5fbkNkz2rbh3BCt2gOHsAbh6b3M7n+DA==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-http@3.775.0': resolution: {integrity: sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-http@3.901.0': + resolution: {integrity: sha512-Ggr7+0M6QZEsrqRkK7iyJLf4LkIAacAxHz9c4dm9hnDdU7vqrlJm6g73IxMJXWN1bIV7IxfpzB11DsRrB/oNjQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-ini@3.782.0': resolution: {integrity: sha512-wd4KdRy2YjLsE4Y7pz00470Iip06GlRHkG4dyLW7/hFMzEO2o7ixswCWp6J2VGZVAX64acknlv2Q0z02ebjmhw==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-ini@3.901.0': + resolution: {integrity: sha512-zxadcDS0hNJgv8n4hFYJNOXyfjaNE1vvqIiF/JzZSQpSSYXzCd+WxXef5bQh+W3giDtRUmkvP5JLbamEFjZKyw==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-node@3.782.0': resolution: {integrity: sha512-HZiAF+TCEyKjju9dgysjiPIWgt/+VerGaeEp18mvKLNfgKz1d+/82A2USEpNKTze7v3cMFASx3CvL8yYyF7mJw==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-node@3.901.0': + resolution: {integrity: sha512-dPuFzMF7L1s/lQyT3wDxqLe82PyTH+5o1jdfseTEln64LJMl0ZMWaKX/C1UFNDxaTd35Cgt1bDbjjAWHMiKSFQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-process@3.775.0': resolution: {integrity: sha512-A6k68H9rQp+2+7P7SGO90Csw6nrUEm0Qfjpn9Etc4EboZhhCLs9b66umUsTsSBHus4FDIe5JQxfCUyt1wgNogg==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-process@3.901.0': + resolution: {integrity: sha512-/IWgmgM3Cl1wTdJA5HqKMAojxLkYchh5kDuphApxKhupLu6Pu0JBOHU8A5GGeFvOycyaVwosod6zDduINZxe+A==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-sso@3.782.0': resolution: {integrity: sha512-1y1ucxTtTIGDSNSNxriQY8msinilhe9gGvQpUDYW9gboyC7WQJPDw66imy258V6osdtdi+xoHzVCbCz3WhosMQ==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-sso@3.901.0': + resolution: {integrity: sha512-SjmqZQHmqFSET7+6xcZgtH7yEyh5q53LN87GqwYlJZ6KJ5oNw11acUNEhUOL1xTSJEvaWqwTIkS2zqrzLcM9bw==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.782.0': resolution: {integrity: sha512-xCna0opVPaueEbJoclj5C6OpDNi0Gynj+4d7tnuXGgQhTHPyAz8ZyClkVqpi5qvHTgxROdUEDxWqEO5jqRHZHQ==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.901.0': + resolution: {integrity: sha512-NYjy/6NLxH9m01+pfpB4ql8QgAorJcu8tw69kzHwUd/ql6wUDTbC7HcXqtKlIwWjzjgj2BKL7j6SyFapgCuafA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-providers@3.901.0': + resolution: {integrity: sha512-jaJ+sVF9xuBwYiQznjrbDkw2W8/aQijGGdzroDL1mJfwyZA0hj3zfYUion+iWwjYhb0vS0bAyrIHtjtTfA2Qpw==} + engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-host-header@3.775.0': resolution: {integrity: sha512-tkSegM0Z6WMXpLB8oPys/d+umYIocvO298mGvcMCncpRl77L9XkvSLJIFzaHes+o7djAgIduYw8wKIMStFss2w==} engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-host-header@3.901.0': + resolution: {integrity: sha512-yWX7GvRmqBtbNnUW7qbre3GvZmyYwU0WHefpZzDTYDoNgatuYq6LgUIQ+z5C04/kCRoFkAFrHag8a3BXqFzq5A==} + engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-logger@3.775.0': resolution: {integrity: sha512-FaxO1xom4MAoUJsldmR92nT1G6uZxTdNYOFYtdHfd6N2wcNaTuxgjIvqzg5y7QIH9kn58XX/dzf1iTjgqUStZw==} engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-logger@3.901.0': + resolution: {integrity: sha512-UoHebjE7el/tfRo8/CQTj91oNUm+5Heus5/a4ECdmWaSCHCS/hXTsU3PTTHAY67oAQR8wBLFPfp3mMvXjB+L2A==} + engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-recursion-detection@3.775.0': resolution: {integrity: sha512-GLCzC8D0A0YDG5u3F5U03Vb9j5tcOEFhr8oc6PDk0k0vm5VwtZOE6LvK7hcCSoAB4HXyOUM0sQuXrbaAh9OwXA==} engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-recursion-detection@3.901.0': + resolution: {integrity: sha512-Wd2t8qa/4OL0v/oDpCHHYkgsXJr8/ttCxrvCKAt0H1zZe2LlRhY9gpDVKqdertfHrHDj786fOvEQA28G1L75Dg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-user-agent@3.782.0': resolution: {integrity: sha512-i32H2R6IItX+bQ2p4+v2gGO2jA80jQoJO2m1xjU9rYWQW3+ErWy4I5YIuQHTBfb6hSdAHbaRfqPDgbv9J2rjEg==} engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-user-agent@3.901.0': + resolution: {integrity: sha512-Zby4F03fvD9xAgXGPywyk4bC1jCbnyubMEYChLYohD+x20ULQCf+AimF/Btn7YL+hBpzh1+RmqmvZcx+RgwgNQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/nested-clients@3.782.0': resolution: {integrity: sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA==} engines: {node: '>=18.0.0'} + '@aws-sdk/nested-clients@3.901.0': + resolution: {integrity: sha512-feAAAMsVwctk2Tms40ONybvpfJPLCmSdI+G+OTrNpizkGLNl6ik2Ng2RzxY6UqOfN8abqKP/DOUj1qYDRDG8ag==} + engines: {node: '>=18.0.0'} + '@aws-sdk/region-config-resolver@3.775.0': resolution: {integrity: sha512-40iH3LJjrQS3LKUJAl7Wj0bln7RFPEvUYKFxtP8a+oKFDO0F65F52xZxIJbPn6sHkxWDAnZlGgdjZXM3p2g5wQ==} engines: {node: '>=18.0.0'} + '@aws-sdk/region-config-resolver@3.901.0': + resolution: {integrity: sha512-7F0N888qVLHo4CSQOsnkZ4QAp8uHLKJ4v3u09Ly5k4AEStrSlFpckTPyUx6elwGL+fxGjNE2aakK8vEgzzCV0A==} + engines: {node: '>=18.0.0'} + '@aws-sdk/token-providers@3.782.0': resolution: {integrity: sha512-4tPuk/3+THPrzKaXW4jE2R67UyGwHLFizZ47pcjJWbhb78IIJAy94vbeqEQ+veS84KF5TXcU7g5jGTXC0D70Wg==} engines: {node: '>=18.0.0'} + '@aws-sdk/token-providers@3.901.0': + resolution: {integrity: sha512-pJEr1Ggbc/uVTDqp9IbNu9hdr0eQf3yZix3s4Nnyvmg4xmJSGAlbPC9LrNr5u3CDZoc8Z9CuLrvbP4MwYquNpQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/types@3.775.0': resolution: {integrity: sha512-ZoGKwa4C9fC9Av6bdfqcW6Ix5ot05F/S4VxWR2nHuMv7hzfmAjTOcUiWT7UR4hM/U0whf84VhDtXN/DWAk52KA==} engines: {node: '>=18.0.0'} + '@aws-sdk/types@3.901.0': + resolution: {integrity: sha512-FfEM25hLEs4LoXsLXQ/q6X6L4JmKkKkbVFpKD4mwfVHtRVQG6QxJiCPcrkcPISquiy6esbwK2eh64TWbiD60cg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/util-endpoints@3.782.0': resolution: {integrity: sha512-/RJOAO7o7HI6lEa4ASbFFLHGU9iPK876BhsVfnl54MvApPVYWQ9sHO0anOUim2S5lQTwd/6ghuH3rFYSq/+rdw==} engines: {node: '>=18.0.0'} + '@aws-sdk/util-endpoints@3.901.0': + resolution: {integrity: sha512-5nZP3hGA8FHEtKvEQf4Aww5QZOkjLW1Z+NixSd+0XKfHvA39Ah5sZboScjLx0C9kti/K3OGW1RCx5K9Zc3bZqg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/util-locate-window@3.723.0': resolution: {integrity: sha512-Yf2CS10BqK688DRsrKI/EO6B8ff5J86NXe4C+VCysK7UOgN0l1zOTeTukZ3H8Q9tYYX3oaF1961o8vRkFm7Nmw==} engines: {node: '>=18.0.0'} @@ -804,6 +944,9 @@ packages: '@aws-sdk/util-user-agent-browser@3.775.0': resolution: {integrity: sha512-txw2wkiJmZKVdDbscK7VBK+u+TJnRtlUjRTLei+elZg2ADhpQxfVAQl436FUeIv6AhB/oRHW6/K/EAGXUSWi0A==} + '@aws-sdk/util-user-agent-browser@3.901.0': + resolution: {integrity: sha512-Ntb6V/WFI21Ed4PDgL/8NSfoZQQf9xzrwNgiwvnxgAl/KvAvRBgQtqj5gHsDX8Nj2YmJuVoHfH9BGjL9VQ4WNg==} + '@aws-sdk/util-user-agent-node@3.782.0': resolution: {integrity: sha512-dMFkUBgh2Bxuw8fYZQoH/u3H4afQ12VSkzEi//qFiDTwbKYq+u+RYjc8GLDM6JSK1BShMu5AVR7HD4ap1TYUnA==} engines: {node: '>=18.0.0'} @@ -813,6 +956,23 @@ packages: aws-crt: optional: true + '@aws-sdk/util-user-agent-node@3.901.0': + resolution: {integrity: sha512-l59KQP5TY7vPVUfEURc7P5BJKuNg1RSsAKBQW7LHLECXjLqDUbo2SMLrexLBEoArSt6E8QOrIN0C8z/0Xk0jYw==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.901.0': + resolution: {integrity: sha512-pxFCkuAP7Q94wMTNPAwi6hEtNrp/BdFf+HOrIEeFQsk4EoOmpKY3I6S+u6A9Wg295J80Kh74LqDWM22ux3z6Aw==} + engines: {node: '>=18.0.0'} + + '@aws/lambda-invoke-store@0.0.1': + resolution: {integrity: sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -1485,6 +1645,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -2061,6 +2227,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@oslojs/asn1@1.0.0': resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} @@ -2420,10 +2590,22 @@ packages: resolution: {integrity: sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==} engines: {node: '>=18.0.0'} + '@smithy/abort-controller@4.2.0': + resolution: {integrity: sha512-PLUYa+SUKOEZtXFURBu/CNxlsxfaFGxSBPcStL13KpVeVWIfdezWyDqkz7iDLmwnxojXD0s5KzuB5HGHvt4Aeg==} + engines: {node: '>=18.0.0'} + '@smithy/config-resolver@4.1.0': resolution: {integrity: sha512-8smPlwhga22pwl23fM5ew4T9vfLUCeFXlcqNOCD5M5h8VmNPNUE9j6bQSuRXpDSV11L/E/SwEBQuW8hr6+nS1A==} engines: {node: '>=18.0.0'} + '@smithy/config-resolver@4.3.0': + resolution: {integrity: sha512-9oH+n8AVNiLPK/iK/agOsoWfrKZ3FGP3502tkksd6SRsKMYiu7AFX0YXo6YBADdsAj7C+G/aLKdsafIJHxuCkQ==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.14.0': + resolution: {integrity: sha512-XJ4z5FxvY/t0Dibms/+gLJrI5niRoY0BCmE02fwmPcRYFPI4KI876xaE79YGWIKnEslMbuQPsIEsoU/DXa0DoA==} + engines: {node: '>=18.0.0'} + '@smithy/core@3.2.0': resolution: {integrity: sha512-k17bgQhVZ7YmUvA8at4af1TDpl0NDMBuBKJl8Yg0nrefwmValU+CnA5l/AriVdQNthU/33H3nK71HrLgqOPr1Q==} engines: {node: '>=18.0.0'} @@ -2432,18 +2614,38 @@ packages: resolution: {integrity: sha512-32lVig6jCaWBHnY+OEQ6e6Vnt5vDHaLiydGrwYMW9tPqO688hPGTYRamYJ1EptxEC2rAwJrHWmPoKRBl4iTa8w==} engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.2.0': + resolution: {integrity: sha512-SOhFVvFH4D5HJZytb0bLKxCrSnwcqPiNlrw+S4ZXjMnsC+o9JcUQzbZOEQcA8yv9wJFNhfsUiIUKiEnYL68Big==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.0': + resolution: {integrity: sha512-XE7CtKfyxYiNZ5vz7OvyTf1osrdbJfmUy+rbh+NLQmZumMGvY0mT0Cq1qKSfhrvLtRYzMsOBuRpi10dyI0EBPg==} + engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.0.2': resolution: {integrity: sha512-+9Dz8sakS9pe7f2cBocpJXdeVjMopUDLgZs1yWeu7h++WqSbjUYv/JAJwKwXw1HV6gq1jyWjxuyn24E2GhoEcQ==} engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.3.0': + resolution: {integrity: sha512-BG3KSmsx9A//KyIfw+sqNmWFr1YBUr+TwpxFT7yPqAk0yyDh7oSNgzfNH7pS6OC099EGx2ltOULvumCFe8bcgw==} + engines: {node: '>=18.0.0'} + '@smithy/hash-node@4.0.2': resolution: {integrity: sha512-VnTpYPnRUE7yVhWozFdlxcYknv9UN7CeOqSrMH+V877v4oqtVYuoqhIhtSjmGPvYrYnAkaM61sLMKHvxL138yg==} engines: {node: '>=18.0.0'} + '@smithy/hash-node@4.2.0': + resolution: {integrity: sha512-ugv93gOhZGysTctZh9qdgng8B+xO0cj+zN0qAZ+Sgh7qTQGPOJbMdIuyP89KNfUyfAqFSNh5tMvC+h2uCpmTtA==} + engines: {node: '>=18.0.0'} + '@smithy/invalid-dependency@4.0.2': resolution: {integrity: sha512-GatB4+2DTpgWPday+mnUkoumP54u/MDM/5u44KF9hIu8jF0uafZtQLcdfIKkIcUNuF/fBojpLEHZS/56JqPeXQ==} engines: {node: '>=18.0.0'} + '@smithy/invalid-dependency@4.2.0': + resolution: {integrity: sha512-ZmK5X5fUPAbtvRcUPtk28aqIClVhbfcmfoS4M7UQBTnDdrNxhsrxYVv0ZEl5NaPSyExsPWqL4GsPlRvtlwg+2A==} + engines: {node: '>=18.0.0'} + '@smithy/is-array-buffer@2.2.0': resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} @@ -2452,86 +2654,170 @@ packages: resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==} engines: {node: '>=18.0.0'} + '@smithy/is-array-buffer@4.2.0': + resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-content-length@4.0.2': resolution: {integrity: sha512-hAfEXm1zU+ELvucxqQ7I8SszwQ4znWMbNv6PLMndN83JJN41EPuS93AIyh2N+gJ6x8QFhzSO6b7q2e6oClDI8A==} engines: {node: '>=18.0.0'} + '@smithy/middleware-content-length@4.2.0': + resolution: {integrity: sha512-6ZAnwrXFecrA4kIDOcz6aLBhU5ih2is2NdcZtobBDSdSHtE9a+MThB5uqyK4XXesdOCvOcbCm2IGB95birTSOQ==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-endpoint@4.1.0': resolution: {integrity: sha512-xhLimgNCbCzsUppRTGXWkZywksuTThxaIB0HwbpsVLY5sceac4e1TZ/WKYqufQLaUy+gUSJGNdwD2jo3cXL0iA==} engines: {node: '>=18.0.0'} + '@smithy/middleware-endpoint@4.3.0': + resolution: {integrity: sha512-jFVjuQeV8TkxaRlcCNg0GFVgg98tscsmIrIwRFeC74TIUyLE3jmY9xgc1WXrPQYRjQNK3aRoaIk6fhFRGOIoGw==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-retry@4.1.0': resolution: {integrity: sha512-2zAagd1s6hAaI/ap6SXi5T3dDwBOczOMCSkkYzktqN1+tzbk1GAsHNAdo/1uzxz3Ky02jvZQwbi/vmDA6z4Oyg==} engines: {node: '>=18.0.0'} + '@smithy/middleware-retry@4.4.0': + resolution: {integrity: sha512-yaVBR0vQnOnzex45zZ8ZrPzUnX73eUC8kVFaAAbn04+6V7lPtxn56vZEBBAhgS/eqD6Zm86o6sJs6FuQVoX5qg==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-serde@4.0.3': resolution: {integrity: sha512-rfgDVrgLEVMmMn0BI8O+8OVr6vXzjV7HZj57l0QxslhzbvVfikZbVfBVthjLHqib4BW44QhcIgJpvebHlRaC9A==} engines: {node: '>=18.0.0'} + '@smithy/middleware-serde@4.2.0': + resolution: {integrity: sha512-rpTQ7D65/EAbC6VydXlxjvbifTf4IH+sADKg6JmAvhkflJO2NvDeyU9qsWUNBelJiQFcXKejUHWRSdmpJmEmiw==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-stack@4.0.2': resolution: {integrity: sha512-eSPVcuJJGVYrFYu2hEq8g8WWdJav3sdrI4o2c6z/rjnYDd3xH9j9E7deZQCzFn4QvGPouLngH3dQ+QVTxv5bOQ==} engines: {node: '>=18.0.0'} + '@smithy/middleware-stack@4.2.0': + resolution: {integrity: sha512-G5CJ//eqRd9OARrQu9MK1H8fNm2sMtqFh6j8/rPozhEL+Dokpvi1Og+aCixTuwDAGZUkJPk6hJT5jchbk/WCyg==} + engines: {node: '>=18.0.0'} + '@smithy/node-config-provider@4.0.2': resolution: {integrity: sha512-WgCkILRZfJwJ4Da92a6t3ozN/zcvYyJGUTmfGbgS/FkCcoCjl7G4FJaCDN1ySdvLvemnQeo25FdkyMSTSwulsw==} engines: {node: '>=18.0.0'} + '@smithy/node-config-provider@4.3.0': + resolution: {integrity: sha512-5QgHNuWdT9j9GwMPPJCKxy2KDxZ3E5l4M3/5TatSZrqYVoEiqQrDfAq8I6KWZw7RZOHtVtCzEPdYz7rHZixwcA==} + engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.0.4': resolution: {integrity: sha512-/mdqabuAT3o/ihBGjL94PUbTSPSRJ0eeVTdgADzow0wRJ0rN4A27EOrtlK56MYiO1fDvlO3jVTCxQtQmK9dZ1g==} engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.3.0': + resolution: {integrity: sha512-RHZ/uWCmSNZ8cneoWEVsVwMZBKy/8123hEpm57vgGXA3Irf/Ja4v9TVshHK2ML5/IqzAZn0WhINHOP9xl+Qy6Q==} + engines: {node: '>=18.0.0'} + '@smithy/property-provider@4.0.2': resolution: {integrity: sha512-wNRoQC1uISOuNc2s4hkOYwYllmiyrvVXWMtq+TysNRVQaHm4yoafYQyjN/goYZS+QbYlPIbb/QRjaUZMuzwQ7A==} engines: {node: '>=18.0.0'} + '@smithy/property-provider@4.2.0': + resolution: {integrity: sha512-rV6wFre0BU6n/tx2Ztn5LdvEdNZ2FasQbPQmDOPfV9QQyDmsCkOAB0osQjotRCQg+nSKFmINhyda0D3AnjSBJw==} + engines: {node: '>=18.0.0'} + '@smithy/protocol-http@5.1.0': resolution: {integrity: sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==} engines: {node: '>=18.0.0'} + '@smithy/protocol-http@5.3.0': + resolution: {integrity: sha512-6POSYlmDnsLKb7r1D3SVm7RaYW6H1vcNcTWGWrF7s9+2noNYvUsm7E4tz5ZQ9HXPmKn6Hb67pBDRIjrT4w/d7Q==} + engines: {node: '>=18.0.0'} + '@smithy/querystring-builder@4.0.2': resolution: {integrity: sha512-NTOs0FwHw1vimmQM4ebh+wFQvOwkEf/kQL6bSM1Lock+Bv4I89B3hGYoUEPkmvYPkDKyp5UdXJYu+PoTQ3T31Q==} engines: {node: '>=18.0.0'} + '@smithy/querystring-builder@4.2.0': + resolution: {integrity: sha512-Q4oFD0ZmI8yJkiPPeGUITZj++4HHYCW3pYBYfIobUCkYpI6mbkzmG1MAQQ3lJYYWj3iNqfzOenUZu+jqdPQ16A==} + engines: {node: '>=18.0.0'} + '@smithy/querystring-parser@4.0.2': resolution: {integrity: sha512-v6w8wnmZcVXjfVLjxw8qF7OwESD9wnpjp0Dqry/Pod0/5vcEA3qxCr+BhbOHlxS8O+29eLpT3aagxXGwIoEk7Q==} engines: {node: '>=18.0.0'} + '@smithy/querystring-parser@4.2.0': + resolution: {integrity: sha512-BjATSNNyvVbQxOOlKse0b0pSezTWGMvA87SvoFoFlkRsKXVsN3bEtjCxvsNXJXfnAzlWFPaT9DmhWy1vn0sNEA==} + engines: {node: '>=18.0.0'} + '@smithy/service-error-classification@4.0.2': resolution: {integrity: sha512-LA86xeFpTKn270Hbkixqs5n73S+LVM0/VZco8dqd+JT75Dyx3Lcw/MraL7ybjmz786+160K8rPOmhsq0SocoJQ==} engines: {node: '>=18.0.0'} + '@smithy/service-error-classification@4.2.0': + resolution: {integrity: sha512-Ylv1ttUeKatpR0wEOMnHf1hXMktPUMObDClSWl2TpCVT4DwtJhCeighLzSLbgH3jr5pBNM0LDXT5yYxUvZ9WpA==} + engines: {node: '>=18.0.0'} + '@smithy/shared-ini-file-loader@4.0.2': resolution: {integrity: sha512-J9/gTWBGVuFZ01oVA6vdb4DAjf1XbDhK6sLsu3OS9qmLrS6KB5ygpeHiM3miIbj1qgSJ96GYszXFWv6ErJ8QEw==} engines: {node: '>=18.0.0'} + '@smithy/shared-ini-file-loader@4.3.0': + resolution: {integrity: sha512-VCUPPtNs+rKWlqqntX0CbVvWyjhmX30JCtzO+s5dlzzxrvSfRh5SY0yxnkirvc1c80vdKQttahL71a9EsdolSQ==} + engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.0.2': resolution: {integrity: sha512-Mz+mc7okA73Lyz8zQKJNyr7lIcHLiPYp0+oiqiMNc/t7/Kf2BENs5d63pEj7oPqdjaum6g0Fc8wC78dY1TgtXw==} engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.3.0': + resolution: {integrity: sha512-MKNyhXEs99xAZaFhm88h+3/V+tCRDQ+PrDzRqL0xdDpq4gjxcMmf5rBA3YXgqZqMZ/XwemZEurCBQMfxZOWq/g==} + engines: {node: '>=18.0.0'} + '@smithy/smithy-client@4.2.0': resolution: {integrity: sha512-Qs65/w30pWV7LSFAez9DKy0Koaoh3iHhpcpCCJ4waj/iqwsuSzJna2+vYwq46yBaqO5ZbP9TjUsATUNxrKeBdw==} engines: {node: '>=18.0.0'} + '@smithy/smithy-client@4.7.0': + resolution: {integrity: sha512-3BDx/aCCPf+kkinYf5QQhdQ9UAGihgOVqI3QO5xQfSaIWvUE4KYLtiGRWsNe1SR7ijXC0QEPqofVp5Sb0zC8xQ==} + engines: {node: '>=18.0.0'} + '@smithy/types@4.2.0': resolution: {integrity: sha512-7eMk09zQKCO+E/ivsjQv+fDlOupcFUCSC/L2YUPgwhvowVGWbPQHjEFcmjt7QQ4ra5lyowS92SV53Zc6XD4+fg==} engines: {node: '>=18.0.0'} + '@smithy/types@4.6.0': + resolution: {integrity: sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA==} + engines: {node: '>=18.0.0'} + '@smithy/url-parser@4.0.2': resolution: {integrity: sha512-Bm8n3j2ScqnT+kJaClSVCMeiSenK6jVAzZCNewsYWuZtnBehEz4r2qP0riZySZVfzB+03XZHJeqfmJDkeeSLiQ==} engines: {node: '>=18.0.0'} + '@smithy/url-parser@4.2.0': + resolution: {integrity: sha512-AlBmD6Idav2ugmoAL6UtR6ItS7jU5h5RNqLMZC7QrLCoITA9NzIN3nx9GWi8g4z1pfWh2r9r96SX/jHiNwPJ9A==} + engines: {node: '>=18.0.0'} + '@smithy/util-base64@4.0.0': resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==} engines: {node: '>=18.0.0'} + '@smithy/util-base64@4.2.0': + resolution: {integrity: sha512-+erInz8WDv5KPe7xCsJCp+1WCjSbah9gWcmUXc9NqmhyPx59tf7jqFz+za1tRG1Y5KM1Cy1rWCcGypylFp4mvA==} + engines: {node: '>=18.0.0'} + '@smithy/util-body-length-browser@4.0.0': resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==} engines: {node: '>=18.0.0'} + '@smithy/util-body-length-browser@4.2.0': + resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} + engines: {node: '>=18.0.0'} + '@smithy/util-body-length-node@4.0.0': resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==} engines: {node: '>=18.0.0'} + '@smithy/util-body-length-node@4.2.0': + resolution: {integrity: sha512-U8q1WsSZFjXijlD7a4wsDQOvOwV+72iHSfq1q7VD+V75xP/pdtm0WIGuaFJ3gcADDOKj2MIBn4+zisi140HEnQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-buffer-from@2.2.0': resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} @@ -2540,42 +2826,82 @@ packages: resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==} engines: {node: '>=18.0.0'} + '@smithy/util-buffer-from@4.2.0': + resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} + engines: {node: '>=18.0.0'} + '@smithy/util-config-provider@4.0.0': resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==} engines: {node: '>=18.0.0'} + '@smithy/util-config-provider@4.2.0': + resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} + engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-browser@4.0.8': resolution: {integrity: sha512-ZTypzBra+lI/LfTYZeop9UjoJhhGRTg3pxrNpfSTQLd3AJ37r2z4AXTKpq1rFXiiUIJsYyFgNJdjWRGP/cbBaQ==} engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-browser@4.2.0': + resolution: {integrity: sha512-qzHp7ZDk1Ba4LDwQVCNp90xPGqSu7kmL7y5toBpccuhi3AH7dcVBIT/pUxYcInK4jOy6FikrcTGq5wxcka8UaQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-node@4.0.8': resolution: {integrity: sha512-Rgk0Jc/UDfRTzVthye/k2dDsz5Xxs9LZaKCNPgJTRyoyBoeiNCnHsYGOyu1PKN+sDyPnJzMOz22JbwxzBp9NNA==} engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-node@4.2.0': + resolution: {integrity: sha512-FxUHS3WXgx3bTWR6yQHNHHkQHZm/XKIi/CchTnKvBulN6obWpcbzJ6lDToXn+Wp0QlVKd7uYAz2/CTw1j7m+Kg==} + engines: {node: '>=18.0.0'} + '@smithy/util-endpoints@3.0.2': resolution: {integrity: sha512-6QSutU5ZyrpNbnd51zRTL7goojlcnuOB55+F9VBD+j8JpRY50IGamsjlycrmpn8PQkmJucFW8A0LSfXj7jjtLQ==} engines: {node: '>=18.0.0'} + '@smithy/util-endpoints@3.2.0': + resolution: {integrity: sha512-TXeCn22D56vvWr/5xPqALc9oO+LN+QpFjrSM7peG/ckqEPoI3zaKZFp+bFwfmiHhn5MGWPaLCqDOJPPIixk9Wg==} + engines: {node: '>=18.0.0'} + '@smithy/util-hex-encoding@4.0.0': resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==} engines: {node: '>=18.0.0'} + '@smithy/util-hex-encoding@4.2.0': + resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} + engines: {node: '>=18.0.0'} + '@smithy/util-middleware@4.0.2': resolution: {integrity: sha512-6GDamTGLuBQVAEuQ4yDQ+ti/YINf/MEmIegrEeg7DdB/sld8BX1lqt9RRuIcABOhAGTA50bRbPzErez7SlDtDQ==} engines: {node: '>=18.0.0'} + '@smithy/util-middleware@4.2.0': + resolution: {integrity: sha512-u9OOfDa43MjagtJZ8AapJcmimP+K2Z7szXn8xbty4aza+7P1wjFmy2ewjSbhEiYQoW1unTlOAIV165weYAaowA==} + engines: {node: '>=18.0.0'} + '@smithy/util-retry@4.0.2': resolution: {integrity: sha512-Qryc+QG+7BCpvjloFLQrmlSd0RsVRHejRXd78jNO3+oREueCjwG1CCEH1vduw/ZkM1U9TztwIKVIi3+8MJScGg==} engines: {node: '>=18.0.0'} + '@smithy/util-retry@4.2.0': + resolution: {integrity: sha512-BWSiuGbwRnEE2SFfaAZEX0TqaxtvtSYPM/J73PFVm+A29Fg1HTPiYFb8TmX1DXp4hgcdyJcNQmprfd5foeORsg==} + engines: {node: '>=18.0.0'} + '@smithy/util-stream@4.2.0': resolution: {integrity: sha512-Vj1TtwWnuWqdgQI6YTUF5hQ/0jmFiOYsc51CSMgj7QfyO+RF4EnT2HNjoviNlOOmgzgvf3f5yno+EiC4vrnaWQ==} engines: {node: '>=18.0.0'} + '@smithy/util-stream@4.4.0': + resolution: {integrity: sha512-vtO7ktbixEcrVzMRmpQDnw/Ehr9UWjBvSJ9fyAbadKkC4w5Cm/4lMO8cHz8Ysb8uflvQUNRcuux/oNHKPXkffg==} + engines: {node: '>=18.0.0'} + '@smithy/util-uri-escape@4.0.0': resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==} engines: {node: '>=18.0.0'} + '@smithy/util-uri-escape@4.2.0': + resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} + engines: {node: '>=18.0.0'} + '@smithy/util-utf8@2.3.0': resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} @@ -2584,10 +2910,21 @@ packages: resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} engines: {node: '>=18.0.0'} + '@smithy/util-utf8@4.2.0': + resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} + engines: {node: '>=18.0.0'} + '@smithy/util-waiter@4.0.3': resolution: {integrity: sha512-JtaY3FxmD+te+KSI2FJuEcfNC9T/DGGVf551babM7fAaXhjJUt7oSYurH1Devxd2+BOSUACCgt3buinx4UnmEA==} engines: {node: '>=18.0.0'} + '@smithy/uuid@1.1.0': + resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} + engines: {node: '>=18.0.0'} + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@storybook/addon-a11y@8.6.12': resolution: {integrity: sha512-H28zHiL8uuv29XsVNf9VjNWsCeht/l66GPYHT7aom1jh+f3fS9+sutrCGEBC/T7cnRpy8ZyuHCtihUqS+RI4pg==} peerDependencies: @@ -3578,6 +3915,12 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ai@5.0.59: + resolution: {integrity: sha512-SuAFxKXt2Ha9FiXB3gaOITkOg9ek/3QNVatGVExvTT4gNXc+hJpuNe1dmuwf6Z5Op4fzc8wdbsrYP27ZCXBzlw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -3884,6 +4227,9 @@ packages: engines: {node: '>= 14.15.0'} hasBin: true + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + axe-core@4.10.3: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} @@ -4040,6 +4386,9 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -4616,6 +4965,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -5086,6 +5444,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -5180,6 +5542,10 @@ packages: resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} hasBin: true + fast-xml-parser@5.2.5: + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + hasBin: true + fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} engines: {node: '>= 4.9.1'} @@ -5436,6 +5802,9 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + get-uri@6.0.4: resolution: {integrity: sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==} engines: {node: '>= 14'} @@ -6397,6 +6766,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -8051,6 +8423,9 @@ packages: resolution: {integrity: sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==} engines: {node: '>= 10.13.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve.exports@2.0.3: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} @@ -8344,6 +8719,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -8519,6 +8899,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -8733,6 +9114,9 @@ packages: strnum@1.1.2: resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + strnum@2.1.1: + resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} + sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -8745,6 +9129,7 @@ packages: supertest@7.1.0: resolution: {integrity: sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==} engines: {node: '>=14.18.0'} + deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} @@ -9031,6 +9416,11 @@ packages: typescript: optional: true + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -9142,8 +9532,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@5.9.0-dev.20250428: - resolution: {integrity: sha512-/6K3WJlc0zjdAgLJMpU40jIxBIQ4fpAfE3o35EsPBTaqvDh9X6rY+c0NBYpYb/3UG0a2wgSr0yD0sS1l6Km7Fw==} + typescript@6.0.0-dev.20251008: + resolution: {integrity: sha512-akqwl9XdobElV/XntkoCZ8w4NuY4zleLjYCO5lBCUaJ9suB7SotGJKuw5MoVZZ9drObDYXCyZL4z0RqQPtEm0A==} engines: {node: '>=14.17'} hasBin: true @@ -9988,6 +10378,9 @@ packages: zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + zod@4.1.11: + resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} + zustand-utils@1.3.2: resolution: {integrity: sha512-c+X8whiqWKgl6r3jzzlNR6vp5ZHsqfIxbZN2uyv+GlqATKh//6GIneywm7tcq+8XZXINT8N9tnDH8npPdXDLEA==} peerDependencies: @@ -10016,6 +10409,39 @@ snapshots: '@adobe/css-tools@4.4.2': {} + '@ai-sdk/amazon-bedrock@3.0.30(zod@4.1.11)': + dependencies: + '@ai-sdk/anthropic': 2.0.23(zod@4.1.11) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.10(zod@4.1.11) + '@smithy/eventstream-codec': 4.2.0 + '@smithy/util-utf8': 4.2.0 + aws4fetch: 1.0.20 + zod: 4.1.11 + + '@ai-sdk/anthropic@2.0.23(zod@4.1.11)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.10(zod@4.1.11) + zod: 4.1.11 + + '@ai-sdk/gateway@1.0.32(zod@4.1.11)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.10(zod@4.1.11) + zod: 4.1.11 + + '@ai-sdk/provider-utils@3.0.10(zod@4.1.11)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.6 + zod: 4.1.11 + + '@ai-sdk/provider@2.0.0': + dependencies: + json-schema: 0.4.0 + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -10029,24 +10455,24 @@ snapshots: '@csstools/css-tokenizer': 3.0.3 lru-cache: 10.4.3 - '@astrojs/check@0.4.1(prettier@3.5.3)(typescript@5.9.0-dev.20250428)': + '@astrojs/check@0.4.1(prettier@3.5.3)(typescript@6.0.0-dev.20251008)': dependencies: - '@astrojs/language-server': 2.15.4(prettier@3.5.3)(typescript@5.9.0-dev.20250428) + '@astrojs/language-server': 2.15.4(prettier@3.5.3)(typescript@6.0.0-dev.20251008) chokidar: 3.6.0 fast-glob: 3.3.3 kleur: 4.1.5 - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 yargs: 17.7.2 transitivePeerDependencies: - prettier - prettier-plugin-astro - '@astrojs/check@0.9.4(prettier@3.5.3)(typescript@5.9.0-dev.20250428)': + '@astrojs/check@0.9.4(prettier@3.5.3)(typescript@6.0.0-dev.20251008)': dependencies: - '@astrojs/language-server': 2.15.4(prettier@3.5.3)(typescript@5.9.0-dev.20250428) + '@astrojs/language-server': 2.15.4(prettier@3.5.3)(typescript@6.0.0-dev.20251008) chokidar: 4.0.3 kleur: 4.1.5 - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 yargs: 17.7.2 transitivePeerDependencies: - prettier @@ -10058,12 +10484,12 @@ snapshots: '@astrojs/internal-helpers@0.6.1': {} - '@astrojs/language-server@2.15.4(prettier@3.5.3)(typescript@5.9.0-dev.20250428)': + '@astrojs/language-server@2.15.4(prettier@3.5.3)(typescript@6.0.0-dev.20251008)': dependencies: '@astrojs/compiler': 2.11.0 '@astrojs/yaml2ts': 0.2.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@volar/kit': 2.4.12(typescript@5.9.0-dev.20250428) + '@volar/kit': 2.4.12(typescript@6.0.0-dev.20251008) '@volar/language-core': 2.4.12 '@volar/language-server': 2.4.12 '@volar/language-service': 2.4.12 @@ -10132,10 +10558,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/node@9.1.3(astro@5.6.0(@types/node@22.14.0)(jiti@2.4.2)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(typescript@5.9.0-dev.20250428)(yaml@2.7.1))': + '@astrojs/node@9.1.3(astro@5.6.0(@types/node@22.14.0)(aws4fetch@1.0.20)(jiti@2.4.2)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(typescript@6.0.0-dev.20251008)(yaml@2.7.1))': dependencies: '@astrojs/internal-helpers': 0.6.1 - astro: 5.6.0(@types/node@22.14.0)(jiti@2.4.2)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(typescript@5.9.0-dev.20250428)(yaml@2.7.1) + astro: 5.6.0(@types/node@22.14.0)(aws4fetch@1.0.20)(jiti@2.4.2)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(typescript@6.0.0-dev.20251008)(yaml@2.7.1) send: 1.2.0 server-destroy: 1.0.1 transitivePeerDependencies: @@ -10169,15 +10595,15 @@ snapshots: - supports-color - terser - '@astrojs/react@4.2.3(@types/node@22.14.0)(@types/react-dom@19.1.1(@types/react@18.3.20))(@types/react@18.3.20)(jiti@2.4.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)': + '@astrojs/react@4.2.3(@types/node@22.14.0)(@types/react-dom@19.1.1(@types/react@18.3.20))(@types/react@18.3.20)(jiti@2.4.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)': dependencies: '@types/react': 18.3.20 '@types/react-dom': 19.1.1(@types/react@18.3.20) - '@vitejs/plugin-react': 4.3.4(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)) + '@vitejs/plugin-react': 4.3.4(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) ultrahtml: 1.5.3 - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) transitivePeerDependencies: - '@types/node' - jiti @@ -10231,12 +10657,18 @@ snapshots: '@aws-cdk/cloud-assembly-schema@40.7.0': {} + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.901.0 + tslib: 2.8.1 + '@aws-crypto/sha256-browser@5.2.0': dependencies: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.775.0 + '@aws-sdk/types': 3.901.0 '@aws-sdk/util-locate-window': 3.723.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -10244,7 +10676,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.775.0 + '@aws-sdk/types': 3.901.0 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -10253,10 +10685,54 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.775.0 + '@aws-sdk/types': 3.901.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 + '@aws-sdk/client-cognito-identity@3.901.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.901.0 + '@aws-sdk/credential-provider-node': 3.901.0 + '@aws-sdk/middleware-host-header': 3.901.0 + '@aws-sdk/middleware-logger': 3.901.0 + '@aws-sdk/middleware-recursion-detection': 3.901.0 + '@aws-sdk/middleware-user-agent': 3.901.0 + '@aws-sdk/region-config-resolver': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-endpoints': 3.901.0 + '@aws-sdk/util-user-agent-browser': 3.901.0 + '@aws-sdk/util-user-agent-node': 3.901.0 + '@smithy/config-resolver': 4.3.0 + '@smithy/core': 3.14.0 + '@smithy/fetch-http-handler': 5.3.0 + '@smithy/hash-node': 4.2.0 + '@smithy/invalid-dependency': 4.2.0 + '@smithy/middleware-content-length': 4.2.0 + '@smithy/middleware-endpoint': 4.3.0 + '@smithy/middleware-retry': 4.4.0 + '@smithy/middleware-serde': 4.2.0 + '@smithy/middleware-stack': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/node-http-handler': 4.3.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/smithy-client': 4.7.0 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + '@smithy/util-base64': 4.2.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.0 + '@smithy/util-defaults-mode-browser': 4.2.0 + '@smithy/util-defaults-mode-node': 4.2.0 + '@smithy/util-endpoints': 3.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-retry': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/client-secrets-manager@3.782.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -10378,7 +10854,7 @@ snapshots: '@smithy/node-http-handler': 4.0.4 '@smithy/protocol-http': 5.1.0 '@smithy/smithy-client': 4.2.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/url-parser': 4.0.2 '@smithy/util-base64': 4.0.0 '@smithy/util-body-length-browser': 4.0.0 @@ -10388,7 +10864,50 @@ snapshots: '@smithy/util-endpoints': 3.0.2 '@smithy/util-middleware': 4.0.2 '@smithy/util-retry': 4.0.2 - '@smithy/util-utf8': 4.0.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.901.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.901.0 + '@aws-sdk/middleware-host-header': 3.901.0 + '@aws-sdk/middleware-logger': 3.901.0 + '@aws-sdk/middleware-recursion-detection': 3.901.0 + '@aws-sdk/middleware-user-agent': 3.901.0 + '@aws-sdk/region-config-resolver': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-endpoints': 3.901.0 + '@aws-sdk/util-user-agent-browser': 3.901.0 + '@aws-sdk/util-user-agent-node': 3.901.0 + '@smithy/config-resolver': 4.3.0 + '@smithy/core': 3.14.0 + '@smithy/fetch-http-handler': 5.3.0 + '@smithy/hash-node': 4.2.0 + '@smithy/invalid-dependency': 4.2.0 + '@smithy/middleware-content-length': 4.2.0 + '@smithy/middleware-endpoint': 4.3.0 + '@smithy/middleware-retry': 4.4.0 + '@smithy/middleware-serde': 4.2.0 + '@smithy/middleware-stack': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/node-http-handler': 4.3.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/smithy-client': 4.7.0 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + '@smithy/util-base64': 4.2.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.0 + '@smithy/util-defaults-mode-browser': 4.2.0 + '@smithy/util-defaults-mode-node': 4.2.0 + '@smithy/util-endpoints': 3.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-retry': 4.2.0 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -10402,17 +10921,51 @@ snapshots: '@smithy/protocol-http': 5.1.0 '@smithy/signature-v4': 5.0.2 '@smithy/smithy-client': 4.2.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-middleware': 4.0.2 fast-xml-parser: 4.4.1 tslib: 2.8.1 + '@aws-sdk/core@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@aws-sdk/xml-builder': 3.901.0 + '@smithy/core': 3.14.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/property-provider': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/signature-v4': 5.3.0 + '@smithy/smithy-client': 4.7.0 + '@smithy/types': 4.6.0 + '@smithy/util-base64': 4.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-cognito-identity@3.901.0': + dependencies: + '@aws-sdk/client-cognito-identity': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@smithy/property-provider': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-env@3.775.0': dependencies: '@aws-sdk/core': 3.775.0 '@aws-sdk/types': 3.775.0 '@smithy/property-provider': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.901.0': + dependencies: + '@aws-sdk/core': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@smithy/property-provider': 4.2.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@aws-sdk/credential-provider-http@3.775.0': @@ -10424,10 +10977,23 @@ snapshots: '@smithy/property-provider': 4.0.2 '@smithy/protocol-http': 5.1.0 '@smithy/smithy-client': 4.2.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-stream': 4.2.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.901.0': + dependencies: + '@aws-sdk/core': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@smithy/fetch-http-handler': 5.3.0 + '@smithy/node-http-handler': 4.3.0 + '@smithy/property-provider': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/smithy-client': 4.7.0 + '@smithy/types': 4.6.0 + '@smithy/util-stream': 4.4.0 + tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.782.0': dependencies: '@aws-sdk/core': 3.775.0 @@ -10441,7 +11007,25 @@ snapshots: '@smithy/credential-provider-imds': 4.0.2 '@smithy/property-provider': 4.0.2 '@smithy/shared-ini-file-loader': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-ini@3.901.0': + dependencies: + '@aws-sdk/core': 3.901.0 + '@aws-sdk/credential-provider-env': 3.901.0 + '@aws-sdk/credential-provider-http': 3.901.0 + '@aws-sdk/credential-provider-process': 3.901.0 + '@aws-sdk/credential-provider-sso': 3.901.0 + '@aws-sdk/credential-provider-web-identity': 3.901.0 + '@aws-sdk/nested-clients': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@smithy/credential-provider-imds': 4.2.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -10458,7 +11042,24 @@ snapshots: '@smithy/credential-provider-imds': 4.0.2 '@smithy/property-provider': 4.0.2 '@smithy/shared-ini-file-loader': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.901.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.901.0 + '@aws-sdk/credential-provider-http': 3.901.0 + '@aws-sdk/credential-provider-ini': 3.901.0 + '@aws-sdk/credential-provider-process': 3.901.0 + '@aws-sdk/credential-provider-sso': 3.901.0 + '@aws-sdk/credential-provider-web-identity': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@smithy/credential-provider-imds': 4.2.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -10469,7 +11070,16 @@ snapshots: '@aws-sdk/types': 3.775.0 '@smithy/property-provider': 4.0.2 '@smithy/shared-ini-file-loader': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.901.0': + dependencies: + '@aws-sdk/core': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@aws-sdk/credential-provider-sso@3.782.0': @@ -10480,7 +11090,20 @@ snapshots: '@aws-sdk/types': 3.775.0 '@smithy/property-provider': 4.0.2 '@smithy/shared-ini-file-loader': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-sso@3.901.0': + dependencies: + '@aws-sdk/client-sso': 3.901.0 + '@aws-sdk/core': 3.901.0 + '@aws-sdk/token-providers': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -10491,7 +11114,43 @@ snapshots: '@aws-sdk/nested-clients': 3.782.0 '@aws-sdk/types': 3.775.0 '@smithy/property-provider': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.901.0': + dependencies: + '@aws-sdk/core': 3.901.0 + '@aws-sdk/nested-clients': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-providers@3.901.0': + dependencies: + '@aws-sdk/client-cognito-identity': 3.901.0 + '@aws-sdk/core': 3.901.0 + '@aws-sdk/credential-provider-cognito-identity': 3.901.0 + '@aws-sdk/credential-provider-env': 3.901.0 + '@aws-sdk/credential-provider-http': 3.901.0 + '@aws-sdk/credential-provider-ini': 3.901.0 + '@aws-sdk/credential-provider-node': 3.901.0 + '@aws-sdk/credential-provider-process': 3.901.0 + '@aws-sdk/credential-provider-sso': 3.901.0 + '@aws-sdk/credential-provider-web-identity': 3.901.0 + '@aws-sdk/nested-clients': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@smithy/config-resolver': 4.3.0 + '@smithy/core': 3.14.0 + '@smithy/credential-provider-imds': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/property-provider': 4.2.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -10500,20 +11159,41 @@ snapshots: dependencies: '@aws-sdk/types': 3.775.0 '@smithy/protocol-http': 5.1.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@aws-sdk/middleware-logger@3.775.0': dependencies: '@aws-sdk/types': 3.775.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@aws-sdk/middleware-recursion-detection@3.775.0': dependencies: '@aws-sdk/types': 3.775.0 '@smithy/protocol-http': 5.1.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@aws/lambda-invoke-store': 0.0.1 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@aws-sdk/middleware-user-agent@3.782.0': @@ -10523,7 +11203,17 @@ snapshots: '@aws-sdk/util-endpoints': 3.782.0 '@smithy/core': 3.2.0 '@smithy/protocol-http': 5.1.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.901.0': + dependencies: + '@aws-sdk/core': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-endpoints': 3.901.0 + '@smithy/core': 3.14.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@aws-sdk/nested-clients@3.782.0': @@ -10554,7 +11244,7 @@ snapshots: '@smithy/node-http-handler': 4.0.4 '@smithy/protocol-http': 5.1.0 '@smithy/smithy-client': 4.2.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/url-parser': 4.0.2 '@smithy/util-base64': 4.0.0 '@smithy/util-body-length-browser': 4.0.0 @@ -10564,7 +11254,50 @@ snapshots: '@smithy/util-endpoints': 3.0.2 '@smithy/util-middleware': 4.0.2 '@smithy/util-retry': 4.0.2 - '@smithy/util-utf8': 4.0.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/nested-clients@3.901.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.901.0 + '@aws-sdk/middleware-host-header': 3.901.0 + '@aws-sdk/middleware-logger': 3.901.0 + '@aws-sdk/middleware-recursion-detection': 3.901.0 + '@aws-sdk/middleware-user-agent': 3.901.0 + '@aws-sdk/region-config-resolver': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-endpoints': 3.901.0 + '@aws-sdk/util-user-agent-browser': 3.901.0 + '@aws-sdk/util-user-agent-node': 3.901.0 + '@smithy/config-resolver': 4.3.0 + '@smithy/core': 3.14.0 + '@smithy/fetch-http-handler': 5.3.0 + '@smithy/hash-node': 4.2.0 + '@smithy/invalid-dependency': 4.2.0 + '@smithy/middleware-content-length': 4.2.0 + '@smithy/middleware-endpoint': 4.3.0 + '@smithy/middleware-retry': 4.4.0 + '@smithy/middleware-serde': 4.2.0 + '@smithy/middleware-stack': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/node-http-handler': 4.3.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/smithy-client': 4.7.0 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + '@smithy/util-base64': 4.2.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.0 + '@smithy/util-defaults-mode-browser': 4.2.0 + '@smithy/util-defaults-mode-node': 4.2.0 + '@smithy/util-endpoints': 3.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-retry': 4.2.0 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -10573,34 +11306,68 @@ snapshots: dependencies: '@aws-sdk/types': 3.775.0 '@smithy/node-config-provider': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-config-provider': 4.0.0 '@smithy/util-middleware': 4.0.2 tslib: 2.8.1 + '@aws-sdk/region-config-resolver@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.0 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.782.0': dependencies: '@aws-sdk/nested-clients': 3.782.0 '@aws-sdk/types': 3.775.0 '@smithy/property-provider': 4.0.2 '@smithy/shared-ini-file-loader': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/token-providers@3.901.0': + dependencies: + '@aws-sdk/core': 3.901.0 + '@aws-sdk/nested-clients': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt '@aws-sdk/types@3.775.0': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.901.0': + dependencies: + '@smithy/types': 4.6.0 tslib: 2.8.1 '@aws-sdk/util-endpoints@3.782.0': dependencies: '@aws-sdk/types': 3.775.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-endpoints': 3.0.2 tslib: 2.8.1 + '@aws-sdk/util-endpoints@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + '@smithy/util-endpoints': 3.2.0 + tslib: 2.8.1 + '@aws-sdk/util-locate-window@3.723.0': dependencies: tslib: 2.8.1 @@ -10608,7 +11375,14 @@ snapshots: '@aws-sdk/util-user-agent-browser@3.775.0': dependencies: '@aws-sdk/types': 3.775.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + bowser: 2.11.0 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/types': 4.6.0 bowser: 2.11.0 tslib: 2.8.1 @@ -10617,9 +11391,25 @@ snapshots: '@aws-sdk/middleware-user-agent': 3.782.0 '@aws-sdk/types': 3.775.0 '@smithy/node-config-provider': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.901.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.901.0': + dependencies: + '@smithy/types': 4.6.0 + fast-xml-parser: 5.2.5 tslib: 2.8.1 + '@aws/lambda-invoke-store@0.0.1': {} + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -10641,7 +11431,7 @@ snapshots: '@babel/traverse': 7.27.0 '@babel/types': 7.27.0 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -10838,7 +11628,7 @@ snapshots: '@babel/parser': 7.27.0 '@babel/template': 7.27.0 '@babel/types': 7.27.0 - debug: 4.4.0 + debug: 4.4.3 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -11381,6 +12171,11 @@ snapshots: eslint: 8.57.1 eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} '@eslint/eslintrc@2.1.4': @@ -11645,7 +12440,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428))': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -11659,7 +12454,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + jest-config: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -11802,14 +12597,14 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.9.0-dev.20250428)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@6.0.0-dev.20251008)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': dependencies: glob: 10.4.5 magic-string: 0.27.0 - react-docgen-typescript: 2.2.2(typescript@5.9.0-dev.20250428) - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + react-docgen-typescript: 2.2.2(typescript@6.0.0-dev.20251008) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) optionalDependencies: - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 '@jridgewell/gen-mapping@0.3.8': dependencies: @@ -12073,6 +12868,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@opentelemetry/api@1.9.0': {} + '@oslojs/asn1@1.0.0': dependencies: '@oslojs/binary': 1.0.0 @@ -12144,11 +12941,11 @@ snapshots: '@puppeteer/browsers@2.9.0': dependencies: - debug: 4.4.0 + debug: 4.4.3 extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 - semver: 7.7.1 + semver: 7.7.3 tar-fs: 3.0.8 yargs: 17.7.2 transitivePeerDependencies: @@ -12412,7 +13209,7 @@ snapshots: '@sitespeed.io/tracium@0.3.3': dependencies: - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -12470,22 +13267,48 @@ snapshots: '@smithy/abort-controller@4.0.2': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/abort-controller@4.2.0': + dependencies: + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/config-resolver@4.1.0': dependencies: '@smithy/node-config-provider': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-config-provider': 4.0.0 '@smithy/util-middleware': 4.0.2 tslib: 2.8.1 + '@smithy/config-resolver@4.3.0': + dependencies: + '@smithy/node-config-provider': 4.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.0 + tslib: 2.8.1 + + '@smithy/core@3.14.0': + dependencies: + '@smithy/middleware-serde': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-base64': 4.2.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-stream': 4.4.0 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + '@smithy/core@3.2.0': dependencies: '@smithy/middleware-serde': 4.0.3 '@smithy/protocol-http': 5.1.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-body-length-browser': 4.0.0 '@smithy/util-middleware': 4.0.2 '@smithy/util-stream': 4.2.0 @@ -12496,28 +13319,63 @@ snapshots: dependencies: '@smithy/node-config-provider': 4.0.2 '@smithy/property-provider': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/url-parser': 4.0.2 tslib: 2.8.1 + '@smithy/credential-provider-imds@4.2.0': + dependencies: + '@smithy/node-config-provider': 4.3.0 + '@smithy/property-provider': 4.2.0 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.6.0 + '@smithy/util-hex-encoding': 4.2.0 + tslib: 2.8.1 + '@smithy/fetch-http-handler@5.0.2': dependencies: '@smithy/protocol-http': 5.1.0 '@smithy/querystring-builder': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-base64': 4.0.0 tslib: 2.8.1 + '@smithy/fetch-http-handler@5.3.0': + dependencies: + '@smithy/protocol-http': 5.3.0 + '@smithy/querystring-builder': 4.2.0 + '@smithy/types': 4.6.0 + '@smithy/util-base64': 4.2.0 + tslib: 2.8.1 + '@smithy/hash-node@4.0.2': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-buffer-from': 4.0.0 '@smithy/util-utf8': 4.0.0 tslib: 2.8.1 + '@smithy/hash-node@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + '@smithy/invalid-dependency@4.0.2': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.0': + dependencies: + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/is-array-buffer@2.2.0': @@ -12528,10 +13386,20 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/is-array-buffer@4.2.0': + dependencies: + tslib: 2.8.1 + '@smithy/middleware-content-length@4.0.2': dependencies: '@smithy/protocol-http': 5.1.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.0': + dependencies: + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/middleware-endpoint@4.1.0': @@ -12540,38 +13408,79 @@ snapshots: '@smithy/middleware-serde': 4.0.3 '@smithy/node-config-provider': 4.0.2 '@smithy/shared-ini-file-loader': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/url-parser': 4.0.2 '@smithy/util-middleware': 4.0.2 tslib: 2.8.1 + '@smithy/middleware-endpoint@4.3.0': + dependencies: + '@smithy/core': 3.14.0 + '@smithy/middleware-serde': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + '@smithy/util-middleware': 4.2.0 + tslib: 2.8.1 + '@smithy/middleware-retry@4.1.0': dependencies: '@smithy/node-config-provider': 4.0.2 '@smithy/protocol-http': 5.1.0 '@smithy/service-error-classification': 4.0.2 '@smithy/smithy-client': 4.2.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-middleware': 4.0.2 '@smithy/util-retry': 4.0.2 tslib: 2.8.1 uuid: 9.0.1 + '@smithy/middleware-retry@4.4.0': + dependencies: + '@smithy/node-config-provider': 4.3.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/service-error-classification': 4.2.0 + '@smithy/smithy-client': 4.7.0 + '@smithy/types': 4.6.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-retry': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + '@smithy/middleware-serde@4.0.3': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.0': + dependencies: + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/middleware-stack@4.0.2': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.0': + dependencies: + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/node-config-provider@4.0.2': dependencies: '@smithy/property-provider': 4.0.2 '@smithy/shared-ini-file-loader': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.0': + dependencies: + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/node-http-handler@4.0.4': @@ -12579,48 +13488,97 @@ snapshots: '@smithy/abort-controller': 4.0.2 '@smithy/protocol-http': 5.1.0 '@smithy/querystring-builder': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.3.0': + dependencies: + '@smithy/abort-controller': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/querystring-builder': 4.2.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/property-provider@4.0.2': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.0': + dependencies: + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/protocol-http@5.1.0': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.0': + dependencies: + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/querystring-builder@4.0.2': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-uri-escape': 4.0.0 tslib: 2.8.1 + '@smithy/querystring-builder@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + '@smithy/util-uri-escape': 4.2.0 + tslib: 2.8.1 + '@smithy/querystring-parser@4.0.2': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.0': + dependencies: + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/service-error-classification@4.0.2': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + + '@smithy/service-error-classification@4.2.0': + dependencies: + '@smithy/types': 4.6.0 '@smithy/shared-ini-file-loader@4.0.2': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/shared-ini-file-loader@4.3.0': + dependencies: + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/signature-v4@5.0.2': dependencies: '@smithy/is-array-buffer': 4.0.0 '@smithy/protocol-http': 5.1.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-hex-encoding': 4.0.0 '@smithy/util-middleware': 4.0.2 '@smithy/util-uri-escape': 4.0.0 - '@smithy/util-utf8': 4.0.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 '@smithy/smithy-client@4.2.0': @@ -12629,18 +13587,38 @@ snapshots: '@smithy/middleware-endpoint': 4.1.0 '@smithy/middleware-stack': 4.0.2 '@smithy/protocol-http': 5.1.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-stream': 4.2.0 tslib: 2.8.1 + '@smithy/smithy-client@4.7.0': + dependencies: + '@smithy/core': 3.14.0 + '@smithy/middleware-endpoint': 4.3.0 + '@smithy/middleware-stack': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-stream': 4.4.0 + tslib: 2.8.1 + '@smithy/types@4.2.0': dependencies: tslib: 2.8.1 + '@smithy/types@4.6.0': + dependencies: + tslib: 2.8.1 + '@smithy/url-parser@4.0.2': dependencies: '@smithy/querystring-parser': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/url-parser@4.2.0': + dependencies: + '@smithy/querystring-parser': 4.2.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/util-base64@4.0.0': @@ -12649,14 +13627,28 @@ snapshots: '@smithy/util-utf8': 4.0.0 tslib: 2.8.1 + '@smithy/util-base64@4.2.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + '@smithy/util-body-length-browser@4.0.0': dependencies: tslib: 2.8.1 + '@smithy/util-body-length-browser@4.2.0': + dependencies: + tslib: 2.8.1 + '@smithy/util-body-length-node@4.0.0': dependencies: tslib: 2.8.1 + '@smithy/util-body-length-node@4.2.0': + dependencies: + tslib: 2.8.1 + '@smithy/util-buffer-from@2.2.0': dependencies: '@smithy/is-array-buffer': 2.2.0 @@ -12667,15 +13659,32 @@ snapshots: '@smithy/is-array-buffer': 4.0.0 tslib: 2.8.1 + '@smithy/util-buffer-from@4.2.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + tslib: 2.8.1 + '@smithy/util-config-provider@4.0.0': dependencies: tslib: 2.8.1 + '@smithy/util-config-provider@4.2.0': + dependencies: + tslib: 2.8.1 + '@smithy/util-defaults-mode-browser@4.0.8': dependencies: '@smithy/property-provider': 4.0.2 '@smithy/smithy-client': 4.2.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + bowser: 2.11.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.2.0': + dependencies: + '@smithy/property-provider': 4.2.0 + '@smithy/smithy-client': 4.7.0 + '@smithy/types': 4.6.0 bowser: 2.11.0 tslib: 2.8.1 @@ -12686,45 +13695,91 @@ snapshots: '@smithy/node-config-provider': 4.0.2 '@smithy/property-provider': 4.0.2 '@smithy/smithy-client': 4.2.0 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.0': + dependencies: + '@smithy/config-resolver': 4.3.0 + '@smithy/credential-provider-imds': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/property-provider': 4.2.0 + '@smithy/smithy-client': 4.7.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/util-endpoints@3.0.2': dependencies: '@smithy/node-config-provider': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.2.0': + dependencies: + '@smithy/node-config-provider': 4.3.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/util-hex-encoding@4.0.0': dependencies: tslib: 2.8.1 + '@smithy/util-hex-encoding@4.2.0': + dependencies: + tslib: 2.8.1 + '@smithy/util-middleware@4.0.2': dependencies: - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.0': + dependencies: + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/util-retry@4.0.2': dependencies: '@smithy/service-error-classification': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.0': + dependencies: + '@smithy/service-error-classification': 4.2.0 + '@smithy/types': 4.6.0 tslib: 2.8.1 '@smithy/util-stream@4.2.0': dependencies: '@smithy/fetch-http-handler': 5.0.2 '@smithy/node-http-handler': 4.0.4 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 '@smithy/util-base64': 4.0.0 '@smithy/util-buffer-from': 4.0.0 '@smithy/util-hex-encoding': 4.0.0 - '@smithy/util-utf8': 4.0.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.4.0': + dependencies: + '@smithy/fetch-http-handler': 5.3.0 + '@smithy/node-http-handler': 4.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-base64': 4.2.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 '@smithy/util-uri-escape@4.0.0': dependencies: tslib: 2.8.1 + '@smithy/util-uri-escape@4.2.0': + dependencies: + tslib: 2.8.1 + '@smithy/util-utf8@2.3.0': dependencies: '@smithy/util-buffer-from': 2.2.0 @@ -12735,12 +13790,23 @@ snapshots: '@smithy/util-buffer-from': 4.0.0 tslib: 2.8.1 + '@smithy/util-utf8@4.2.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + tslib: 2.8.1 + '@smithy/util-waiter@4.0.3': dependencies: '@smithy/abort-controller': 4.0.2 - '@smithy/types': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.0': + dependencies: tslib: 2.8.1 + '@standard-schema/spec@1.0.0': {} + '@storybook/addon-a11y@8.6.12(storybook@8.6.12(prettier@3.5.3))': dependencies: '@storybook/addon-highlight': 8.6.12(storybook@8.6.12(prettier@3.5.3)) @@ -12866,13 +13932,13 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.6.12(storybook@8.6.12(prettier@3.5.3))(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1))': + '@storybook/builder-vite@8.6.12(storybook@8.6.12(prettier@3.5.3))(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': dependencies: '@storybook/csf-plugin': 8.6.12(storybook@8.6.12(prettier@3.5.3)) browser-assert: 1.2.1 storybook: 8.6.12(prettier@3.5.3) ts-dedent: 2.2.0 - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) '@storybook/components@8.6.12(storybook@8.6.12(prettier@3.5.3))': dependencies: @@ -12908,7 +13974,7 @@ snapshots: dependencies: type-fest: 2.19.0 - '@storybook/experimental-addon-test@8.6.12(@vitest/browser@3.1.1)(@vitest/runner@3.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.12(prettier@3.5.3))(vitest@3.1.1)': + '@storybook/experimental-addon-test@8.6.12(@vitest/browser@3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.1.1))(@vitest/runner@3.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.12(prettier@3.5.3))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -12919,9 +13985,9 @@ snapshots: storybook: 8.6.12(prettier@3.5.3) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1))(vitest@3.1.1) + '@vitest/browser': 3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.1.1) '@vitest/runner': 3.1.1 - vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) transitivePeerDependencies: - react - react-dom @@ -12953,12 +14019,12 @@ snapshots: react-dom: 18.3.1(react@18.3.1) storybook: 8.6.12(prettier@3.5.3) - '@storybook/react-vite@8.6.12(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.39.0)(storybook@8.6.12(prettier@3.5.3))(typescript@5.9.0-dev.20250428)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1))': + '@storybook/react-vite@8.6.12(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.39.0)(storybook@8.6.12(prettier@3.5.3))(typescript@6.0.0-dev.20251008)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.9.0-dev.20250428)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@6.0.0-dev.20251008)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) '@rollup/pluginutils': 5.1.4(rollup@4.39.0) - '@storybook/builder-vite': 8.6.12(storybook@8.6.12(prettier@3.5.3))(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)) - '@storybook/react': 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.12(prettier@3.5.3))(typescript@5.9.0-dev.20250428) + '@storybook/builder-vite': 8.6.12(storybook@8.6.12(prettier@3.5.3))(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + '@storybook/react': 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.12(prettier@3.5.3))(typescript@6.0.0-dev.20251008) find-up: 5.0.0 magic-string: 0.30.17 react: 18.3.1 @@ -12967,7 +14033,7 @@ snapshots: resolve: 1.22.10 storybook: 8.6.12(prettier@3.5.3) tsconfig-paths: 4.2.0 - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) optionalDependencies: '@storybook/test': 8.6.12(storybook@8.6.12(prettier@3.5.3)) transitivePeerDependencies: @@ -12975,7 +14041,7 @@ snapshots: - supports-color - typescript - '@storybook/react@8.6.12(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.12(prettier@3.5.3))(typescript@5.9.0-dev.20250428)': + '@storybook/react@8.6.12(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.12(prettier@3.5.3))(typescript@6.0.0-dev.20251008)': dependencies: '@storybook/components': 8.6.12(storybook@8.6.12(prettier@3.5.3)) '@storybook/global': 5.0.0 @@ -12988,9 +14054,9 @@ snapshots: storybook: 8.6.12(prettier@3.5.3) optionalDependencies: '@storybook/test': 8.6.12(storybook@8.6.12(prettier@3.5.3)) - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 - '@storybook/test-runner@0.21.3(@types/node@22.14.0)(storybook@8.6.12(prettier@3.5.3))(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428))': + '@storybook/test-runner@0.21.3(@types/node@22.14.0)(storybook@8.6.12(prettier@3.5.3))(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008))': dependencies: '@babel/core': 7.26.10 '@babel/generator': 7.27.0 @@ -13001,14 +14067,14 @@ snapshots: '@swc/core': 1.11.16 '@swc/jest': 0.2.37(@swc/core@1.11.16) expect-playwright: 0.8.0 - jest: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + jest: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) jest-circus: 29.7.0 jest-environment-node: 29.7.0 jest-junit: 16.0.0 - jest-playwright-preset: 4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428))) + jest-playwright-preset: 4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008))) jest-runner: 29.7.0 jest-serializer-html: 7.1.0 - jest-watch-typeahead: 2.2.2(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428))) + jest-watch-typeahead: 2.2.2(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008))) nyc: 15.1.0 playwright: 1.51.1 storybook: 8.6.12(prettier@3.5.3) @@ -13587,34 +14653,34 @@ snapshots: '@types/yoga-layout@1.9.2': {} - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.0-dev.20250428))(eslint@8.57.1)(typescript@5.9.0-dev.20250428)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@6.0.0-dev.20251008))(eslint@8.57.1)(typescript@6.0.0-dev.20251008)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.0-dev.20250428) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@6.0.0-dev.20251008) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.9.0-dev.20250428) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.0-dev.20250428) + '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@6.0.0-dev.20251008) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@6.0.0-dev.20251008) '@typescript-eslint/visitor-keys': 7.18.0 eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.9.0-dev.20250428) + ts-api-utils: 1.4.3(typescript@6.0.0-dev.20251008) optionalDependencies: - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.0-dev.20250428)': + '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@6.0.0-dev.20251008)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.0-dev.20250428) + '@typescript-eslint/typescript-estree': 7.18.0(typescript@6.0.0-dev.20251008) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.0 + debug: 4.4.3 eslint: 8.57.1 optionalDependencies: - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 transitivePeerDependencies: - supports-color @@ -13623,41 +14689,41 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.9.0-dev.20250428)': + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@6.0.0-dev.20251008)': dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.0-dev.20250428) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.0-dev.20250428) - debug: 4.4.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@6.0.0-dev.20251008) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@6.0.0-dev.20251008) + debug: 4.4.3 eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.9.0-dev.20250428) + ts-api-utils: 1.4.3(typescript@6.0.0-dev.20251008) optionalDependencies: - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 transitivePeerDependencies: - supports-color '@typescript-eslint/types@7.18.0': {} - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.0-dev.20250428)': + '@typescript-eslint/typescript-estree@7.18.0(typescript@6.0.0-dev.20251008)': dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.0 + debug: 4.4.3 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.1 - ts-api-utils: 1.4.3(typescript@5.9.0-dev.20250428) + semver: 7.7.3 + ts-api-utils: 1.4.3(typescript@6.0.0-dev.20251008) optionalDependencies: - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.9.0-dev.20250428)': + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@6.0.0-dev.20251008)': dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@8.57.1) + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.0-dev.20250428) + '@typescript-eslint/typescript-estree': 7.18.0(typescript@6.0.0-dev.20251008) eslint: 8.57.1 transitivePeerDependencies: - supports-color @@ -13670,11 +14736,11 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@uswds/compile@1.2.2(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428))': + '@uswds/compile@1.2.2(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008))': dependencies: autoprefixer: 10.4.20(postcss@8.5.2) gulp: 5.0.0 - gulp-postcss: 9.0.1(postcss@8.5.2)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + gulp-postcss: 9.0.1(postcss@8.5.2)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) gulp-rename: 2.0.0 gulp-replace: 1.1.4 gulp-sass: 5.1.0 @@ -13700,27 +14766,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.3.4(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1))': + '@vitejs/plugin-react@4.3.4(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': dependencies: '@babel/core': 7.26.10 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) transitivePeerDependencies: - supports-color - '@vitest/browser@3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1))(vitest@3.1.1)': + '@vitest/browser@3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.1.1)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 3.1.1(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)) + '@vitest/mocker': 3.1.1(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) '@vitest/utils': 3.1.1 magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) ws: 8.18.1 optionalDependencies: playwright: 1.51.1 @@ -13730,7 +14796,7 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-v8@3.1.1(@vitest/browser@3.1.1)(vitest@3.1.1)': + '@vitest/coverage-v8@3.1.1(@vitest/browser@3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.1.1))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -13744,9 +14810,9 @@ snapshots: std-env: 3.8.1 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) optionalDependencies: - '@vitest/browser': 3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1))(vitest@3.1.1) + '@vitest/browser': 3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.1.1) transitivePeerDependencies: - supports-color @@ -13764,13 +14830,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.1(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1))': + '@vitest/mocker@3.1.1(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': dependencies: '@vitest/spy': 3.1.1 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) '@vitest/pretty-format@2.0.5': dependencies: @@ -13812,7 +14878,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.12 tinyrainbow: 2.0.0 - vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) '@vitest/utils@2.0.5': dependencies: @@ -13833,12 +14899,12 @@ snapshots: loupe: 3.1.3 tinyrainbow: 2.0.0 - '@volar/kit@2.4.12(typescript@5.9.0-dev.20250428)': + '@volar/kit@2.4.12(typescript@6.0.0-dev.20251008)': dependencies: '@volar/language-service': 2.4.12 '@volar/typescript': 2.4.12 typesafe-path: 0.2.2 - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 @@ -13901,7 +14967,7 @@ snapshots: de-indent: 1.0.2 he: 1.2.0 - '@vue/language-core@2.2.0(typescript@5.9.0-dev.20250428)': + '@vue/language-core@2.2.0(typescript@6.0.0-dev.20251008)': dependencies: '@volar/language-core': 2.4.12 '@vue/compiler-dom': 3.5.13 @@ -13912,7 +14978,7 @@ snapshots: muggle-string: 0.4.1 path-browserify: 1.0.1 optionalDependencies: - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 '@vue/shared@3.5.13': {} @@ -14021,7 +15087,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -14032,6 +15098,14 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ai@5.0.59(zod@4.1.11): + dependencies: + '@ai-sdk/gateway': 1.0.32(zod@4.1.11) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.10(zod@4.1.11) + '@opentelemetry/api': 1.9.0 + zod: 4.1.11 + ajv-draft-04@1.0.0(ajv@8.13.0): optionalDependencies: ajv: 8.13.0 @@ -14299,7 +15373,7 @@ snapshots: astral-regex@2.0.0: {} - astro@4.16.18(@types/node@22.14.0)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(typescript@5.9.0-dev.20250428): + astro@4.16.18(@types/node@22.14.0)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(typescript@6.0.0-dev.20251008): dependencies: '@astrojs/compiler': 2.11.0 '@astrojs/internal-helpers': 0.4.1 @@ -14352,7 +15426,7 @@ snapshots: semver: 7.7.1 shiki: 1.29.2 tinyexec: 0.3.2 - tsconfck: 3.1.5(typescript@5.9.0-dev.20250428) + tsconfck: 3.1.5(typescript@6.0.0-dev.20251008) unist-util-visit: 5.0.0 vfile: 6.0.3 vite: 5.4.17(@types/node@22.14.0)(sass-embedded@1.83.4)(terser@5.39.0) @@ -14362,7 +15436,7 @@ snapshots: yargs-parser: 21.1.1 zod: 3.24.2 zod-to-json-schema: 3.24.5(zod@3.24.2) - zod-to-ts: 1.2.0(typescript@5.9.0-dev.20250428)(zod@3.24.2) + zod-to-ts: 1.2.0(typescript@6.0.0-dev.20251008)(zod@3.24.2) optionalDependencies: sharp: 0.33.5 transitivePeerDependencies: @@ -14378,7 +15452,7 @@ snapshots: - terser - typescript - astro@5.6.0(@types/node@22.14.0)(jiti@2.4.2)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(typescript@5.9.0-dev.20250428)(yaml@2.7.1): + astro@5.6.0(@types/node@22.14.0)(aws4fetch@1.0.20)(jiti@2.4.2)(rollup@4.39.0)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(typescript@6.0.0-dev.20251008)(yaml@2.7.1): dependencies: '@astrojs/compiler': 2.11.0 '@astrojs/internal-helpers': 0.6.1 @@ -14424,19 +15498,19 @@ snapshots: shiki: 3.2.1 tinyexec: 0.3.2 tinyglobby: 0.2.12 - tsconfck: 3.1.5(typescript@5.9.0-dev.20250428) + tsconfck: 3.1.5(typescript@6.0.0-dev.20251008) ultrahtml: 1.5.3 unist-util-visit: 5.0.0 - unstorage: 1.15.0 + unstorage: 1.15.0(aws4fetch@1.0.20) vfile: 6.0.3 - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) - vitefu: 1.0.6(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + vitefu: 1.0.6(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) xxhash-wasm: 1.1.0 yargs-parser: 21.1.1 yocto-spinner: 0.2.1 zod: 3.24.2 zod-to-json-schema: 3.24.5(zod@3.24.2) - zod-to-ts: 1.2.0(typescript@5.9.0-dev.20250428)(zod@3.24.2) + zod-to-ts: 1.2.0(typescript@6.0.0-dev.20251008)(zod@3.24.2) optionalDependencies: sharp: 0.33.5 transitivePeerDependencies: @@ -14518,6 +15592,8 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + aws4fetch@1.0.20: {} + axe-core@4.10.3: {} axios@1.8.4: @@ -14729,6 +15805,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -15186,13 +16266,13 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 - create-jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)): + create-jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + jest-config: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -15311,6 +16391,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decamelize@1.2.0: {} decamelize@5.0.1: {} @@ -15407,7 +16491,7 @@ snapshots: detect-port@1.6.1: dependencies: address: 1.2.2 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -15521,7 +16605,7 @@ snapshots: dependencies: semver: 7.7.1 shelljs: 0.8.5 - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 dset@3.1.4: {} @@ -15712,7 +16796,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.25.2): dependencies: - debug: 4.4.0 + debug: 4.4.3 esbuild: 0.25.2 transitivePeerDependencies: - supports-color @@ -15921,6 +17005,8 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.6: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -16016,7 +17102,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.3 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -16059,6 +17145,10 @@ snapshots: dependencies: strnum: 1.1.2 + fast-xml-parser@5.2.5: + dependencies: + strnum: 2.1.1 + fastest-levenshtein@1.0.16: {} fastq@1.19.1: @@ -16329,11 +17419,15 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + get-uri@6.0.4: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -16484,12 +17578,12 @@ snapshots: v8flags: 4.0.1 yargs: 16.2.0 - gulp-postcss@9.0.1(postcss@8.5.2)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)): + gulp-postcss@9.0.1(postcss@8.5.2)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)): dependencies: fancy-log: 1.3.3 plugin-error: 1.0.1 postcss: 8.5.2 - postcss-load-config: 3.1.4(postcss@8.5.2)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + postcss-load-config: 3.1.4(postcss@8.5.2)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) vinyl-sourcemaps-apply: 0.2.1 transitivePeerDependencies: - ts-node @@ -16712,7 +17806,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -16746,14 +17840,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -17153,7 +18247,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -17228,16 +18322,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)): + jest-cli@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + create-jest: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + jest-config: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -17247,7 +18341,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)): + jest-config@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)): dependencies: '@babel/core': 7.26.10 '@jest/test-sequencer': 29.7.0 @@ -17273,7 +18367,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.14.0 - ts-node: 10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428) + ts-node: 10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -17361,10 +18455,10 @@ snapshots: '@types/node': 22.14.0 jest-util: 29.7.0 - jest-playwright-preset@4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428))): + jest-playwright-preset@4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008))): dependencies: expect-playwright: 0.8.0 - jest: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + jest: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) jest-circus: 29.7.0 jest-environment-node: 29.7.0 jest-process-manager: 0.4.0 @@ -17496,7 +18590,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.7.1 + semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -17518,11 +18612,11 @@ snapshots: leven: 3.1.0 pretty-format: 29.7.0 - jest-watch-typeahead@2.2.2(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428))): + jest-watch-typeahead@2.2.2(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008))): dependencies: ansi-escapes: 6.2.1 chalk: 5.4.1 - jest: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + jest: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) jest-regex-util: 29.6.3 jest-watcher: 29.7.0 slash: 5.1.0 @@ -17553,12 +18647,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)): + jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)) + jest-cli: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -17695,6 +18789,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -17880,7 +18976,7 @@ snapshots: log4js@6.9.1: dependencies: date-format: 4.0.14 - debug: 4.4.0 + debug: 4.4.3 flatted: 3.3.3 rfdc: 1.4.1 streamroller: 3.1.5 @@ -17940,7 +19036,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.1 + semver: 7.7.3 make-error@1.3.6: {} @@ -18361,7 +19457,7 @@ snapshots: minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -18434,7 +19530,7 @@ snapshots: node-abi@3.74.0: dependencies: - semver: 7.7.1 + semver: 7.7.3 node-fetch-native@1.6.6: {} @@ -18733,7 +19829,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.3 get-uri: 6.0.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -18982,7 +20078,7 @@ snapshots: portfinder@1.0.35: dependencies: async: 3.2.6 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -18993,20 +20089,21 @@ snapshots: csso: 5.0.5 postcss: 8.5.2 - postcss-load-config@3.1.4(postcss@8.5.2)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428)): + postcss-load-config@3.1.4(postcss@8.5.2)(ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008)): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: postcss: 8.5.2 - ts-node: 10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428) + ts-node: 10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008) - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.3)(yaml@2.7.1): + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.3)(tsx@4.20.6)(yaml@2.7.1): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.4.2 postcss: 8.5.3 + tsx: 4.20.6 yaml: 2.7.1 postcss-value-parser@4.2.0: {} @@ -19250,7 +20347,7 @@ snapshots: proxy-agent@6.4.0: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.3.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -19263,7 +20360,7 @@ snapshots: proxy-agent@6.5.0: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -19341,9 +20438,9 @@ snapshots: - bufferutil - utf-8-validate - react-docgen-typescript@2.2.2(typescript@5.9.0-dev.20250428): + react-docgen-typescript@2.2.2(typescript@6.0.0-dev.20251008): dependencies: - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 react-docgen@7.1.1: dependencies: @@ -19642,6 +20739,8 @@ snapshots: dependencies: value-or-function: 4.0.0 + resolve-pkg-maps@1.0.0: {} + resolve.exports@2.0.3: {} resolve@1.22.10: @@ -19934,6 +21033,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.3: {} + send@0.19.0: dependencies: debug: 2.6.9 @@ -19954,7 +21055,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.0 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -20163,7 +21264,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.3 socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -20311,7 +21412,7 @@ snapshots: streamroller@3.1.5: dependencies: date-format: 4.0.14 - debug: 4.4.0 + debug: 4.4.3 fs-extra: 8.1.0 transitivePeerDependencies: - supports-color @@ -20447,6 +21548,8 @@ snapshots: strnum@1.1.2: {} + strnum@2.1.1: {} + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -20461,7 +21564,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.0 + debug: 4.4.3 fast-safe-stringify: 2.1.1 form-data: 4.0.2 formidable: 3.5.2 @@ -20698,9 +21801,9 @@ snapshots: trough@2.2.0: {} - ts-api-utils@1.4.3(typescript@5.9.0-dev.20250428): + ts-api-utils@1.4.3(typescript@6.0.0-dev.20251008): dependencies: - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 ts-dedent@2.2.0: {} @@ -20730,7 +21833,7 @@ snapshots: optionalDependencies: '@swc/core': 1.11.16 - ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@5.9.0-dev.20250428): + ts-node@10.9.2(@swc/core@1.11.16)(@types/node@22.14.0)(typescript@6.0.0-dev.20251008): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -20744,7 +21847,7 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: @@ -20755,9 +21858,9 @@ snapshots: optionalDependencies: typescript: 5.8.2 - tsconfck@3.1.5(typescript@5.9.0-dev.20250428): + tsconfck@3.1.5(typescript@6.0.0-dev.20251008): optionalDependencies: - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 tsconfig-paths@4.2.0: dependencies: @@ -20769,7 +21872,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.4.0(@microsoft/api-extractor@7.52.2(@types/node@22.14.0))(@swc/core@1.11.16)(jiti@2.4.2)(postcss@8.5.3)(typescript@5.8.2)(yaml@2.7.1): + tsup@8.4.0(@microsoft/api-extractor@7.52.2(@types/node@22.14.0))(@swc/core@1.11.16)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.20.6)(typescript@5.8.2)(yaml@2.7.1): dependencies: bundle-require: 5.1.0(esbuild@0.25.2) cac: 6.7.14 @@ -20779,7 +21882,7 @@ snapshots: esbuild: 0.25.2 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.3)(yaml@2.7.1) + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.3)(tsx@4.20.6)(yaml@2.7.1) resolve-from: 5.0.0 rollup: 4.39.0 source-map: 0.8.0-beta.0 @@ -20798,6 +21901,13 @@ snapshots: - tsx - yaml + tsx@4.20.6: + dependencies: + esbuild: 0.25.2 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 @@ -20895,13 +22005,13 @@ snapshots: typescript-auto-import-cache@0.3.5: dependencies: - semver: 7.7.1 + semver: 7.7.3 typescript@5.4.5: {} typescript@5.8.2: {} - typescript@5.9.0-dev.20250428: {} + typescript@6.0.0-dev.20251008: {} uc.micro@2.1.0: {} @@ -21013,7 +22123,7 @@ snapshots: acorn: 8.14.1 webpack-virtual-modules: 0.6.2 - unstorage@1.15.0: + unstorage@1.15.0(aws4fetch@1.0.20): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -21023,6 +22133,8 @@ snapshots: node-fetch-native: 1.6.6 ofetch: 1.4.1 ufo: 1.5.4 + optionalDependencies: + aws4fetch: 1.0.20 update-browserslist-db@1.1.3(browserslist@4.24.4): dependencies: @@ -21150,13 +22262,13 @@ snapshots: replace-ext: 2.0.0 teex: 1.0.1 - vite-node@3.1.1(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1): + vite-node@3.1.1(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) transitivePeerDependencies: - '@types/node' - jiti @@ -21171,20 +22283,20 @@ snapshots: - tsx - yaml - vite-plugin-dts@4.5.3(@types/node@22.14.0)(rollup@4.39.0)(typescript@5.9.0-dev.20250428)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)): + vite-plugin-dts@4.5.3(@types/node@22.14.0)(rollup@4.39.0)(typescript@6.0.0-dev.20251008)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): dependencies: '@microsoft/api-extractor': 7.52.2(@types/node@22.14.0) '@rollup/pluginutils': 5.1.4(rollup@4.39.0) '@volar/typescript': 2.4.12 - '@vue/language-core': 2.2.0(typescript@5.9.0-dev.20250428) + '@vue/language-core': 2.2.0(typescript@6.0.0-dev.20251008) compare-versions: 6.1.1 debug: 4.4.0 kolorist: 1.8.0 local-pkg: 1.1.1 magic-string: 0.30.17 - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 optionalDependencies: - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) transitivePeerDependencies: - '@types/node' - rollup @@ -21199,13 +22311,13 @@ snapshots: transitivePeerDependencies: - supports-color - vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)): + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.8.2) optionalDependencies: - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) transitivePeerDependencies: - supports-color - typescript @@ -21221,7 +22333,7 @@ snapshots: sass-embedded: 1.83.4 terser: 5.39.0 - vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1): + vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1): dependencies: esbuild: 0.25.2 postcss: 8.5.3 @@ -21232,30 +22344,31 @@ snapshots: jiti: 2.4.2 sass-embedded: 1.83.4 terser: 5.39.0 + tsx: 4.20.6 yaml: 2.7.1 vitefu@1.0.6(vite@5.4.17(@types/node@22.14.0)(sass-embedded@1.83.4)(terser@5.39.0)): optionalDependencies: vite: 5.4.17(@types/node@22.14.0)(sass-embedded@1.83.4)(terser@5.39.0) - vitefu@1.0.6(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)): + vitefu@1.0.6(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): optionalDependencies: - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) - vitest-fetch-mock@0.4.5(vitest@3.1.1): + vitest-fetch-mock@0.4.5(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): dependencies: - vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) - vitest-mock-extended@2.0.2(typescript@5.8.2)(vitest@3.1.1): + vitest-mock-extended@2.0.2(typescript@5.8.2)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): dependencies: ts-essentials: 10.0.4(typescript@5.8.2) typescript: 5.8.2 - vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) - vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1): + vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(jiti@2.4.2)(jsdom@25.0.1)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1): dependencies: '@vitest/expect': 3.1.1 - '@vitest/mocker': 3.1.1(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1)) + '@vitest/mocker': 3.1.1(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) '@vitest/pretty-format': 3.1.1 '@vitest/runner': 3.1.1 '@vitest/snapshot': 3.1.1 @@ -21271,13 +22384,13 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) - vite-node: 3.1.1(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + vite-node: 3.1.1(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.14.0 - '@vitest/browser': 3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(yaml@2.7.1))(vitest@3.1.1) + '@vitest/browser': 3.1.1(playwright@1.51.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(sass-embedded@1.83.4)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.1.1) '@vitest/ui': 3.1.1(vitest@3.1.1) jsdom: 25.0.1 transitivePeerDependencies: @@ -21335,7 +22448,7 @@ snapshots: volar-service-typescript@0.0.62(@volar/language-service@2.4.12): dependencies: path-browserify: 1.0.1 - semver: 7.7.1 + semver: 7.7.3 typescript-auto-import-cache: 0.3.5 vscode-languageserver-textdocument: 1.0.12 vscode-nls: 5.2.0 @@ -21424,7 +22537,7 @@ snapshots: dependencies: chalk: 2.4.2 commander: 3.0.2 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -21750,9 +22863,9 @@ snapshots: dependencies: zod: 3.24.2 - zod-to-ts@1.2.0(typescript@5.9.0-dev.20250428)(zod@3.24.2): + zod-to-ts@1.2.0(typescript@6.0.0-dev.20251008)(zod@3.24.2): dependencies: - typescript: 5.9.0-dev.20250428 + typescript: 6.0.0-dev.20251008 zod: 3.24.2 zod@3.22.4: {} @@ -21761,6 +22874,8 @@ snapshots: zod@3.24.2: {} + zod@4.1.11: {} + zustand-utils@1.3.2(react@18.3.1)(zustand@4.5.6(@types/react@18.3.20)(react@18.3.1)): dependencies: '@babel/runtime': 7.27.0 diff --git a/turbo.json b/turbo.json index 1e8d9761..78a466bb 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,6 @@ { "$schema": "https://turbo.build/schema.json", + "ui": "tui", "tasks": { "build": { "dependsOn": ["^build"],