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/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..31c04fdf --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,57 @@ +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..16e669a5 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:aws-main # for main branch +# pnpm deploy:aws-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=aws-main pnpm deploy # or aws-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..08bbc3b8 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 }} @@ -54,4 +50,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..461cd593 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,141 @@ +# 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 + +### 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 +- Build is required before running `pnpm dev` +- 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/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/__fixtures__/ai-cache/8f/8fd9b9039c736540e0f3becade55e2d36a3d5afe3f448c8efdb86b10525cdfd4.json b/apps/cli/__fixtures__/ai-cache/8f/8fd9b9039c736540e0f3becade55e2d36a3d5afe3f448c8efdb86b10525cdfd4.json new file mode 100644 index 00000000..c9a61f9d --- /dev/null +++ b/apps/cli/__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:

Pardon CANNOT:

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

Eligibility Requirements

To apply, you should:

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):

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:

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:

" + }, + { + "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:

" + }, + { + "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:

" + }, + { + "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:

" + }, + { + "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:

" + }, + { + "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/apps/cli/package.json b/apps/cli/package.json index 08a60bf9..661bc6cb 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", "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..c675e66d --- /dev/null +++ b/apps/cli/src/cli-controller/forms.ts @@ -0,0 +1,59 @@ +import { promises as fs } from 'fs'; +import { Command } from 'commander'; + +import { commands } from '@flexion/forms-infra-core'; +import { type Context } from './types.js'; +import { createFormService, createFormsRepository, defaultFormConfig, createTestPdfParser, parsePdf as parsePdfCore } from '@flexion/forms-core'; +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..c3b06a1f 100644 --- a/apps/cli/src/cli-controller/secrets.ts +++ b/apps/cli/src/cli-controller/secrets.ts @@ -6,7 +6,7 @@ import { type DeployEnv, commands, getSecretsVault, -} from '@gsa-tts/forms-infra-core'; +} from '@flexion/forms-infra-core'; import { type Context } from './types.js'; export const addSecretCommands = (ctx: Context, cli: Command) => { @@ -97,4 +97,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..e8f02c8c 100644 --- a/apps/sandbox/package.json +++ b/apps/sandbox/package.json @@ -1,11 +1,14 @@ { - "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", "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/sandbox/src/index.ts b/apps/sandbox/src/index.ts index b4874e4b..75e360b3 100644 --- a/apps/sandbox/src/index.ts +++ b/apps/sandbox/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'; @@ -17,25 +17,25 @@ const getCloudGovServerSecrets = () => { }; 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) { + const dbSecretArn = process.env.DB_SECRET_ARN; + const dbHost = process.env.DB_HOST; + const dbPort = process.env.DB_PORT; + const dbName = process.env.DB_NAME; + + if (!dbSecretArn || !dbHost || !dbPort || !dbName) { return; } const vault = getAWSSecretsManagerVault(); - const dbSecret = await vault.getSecret(secrets.dbSecretArn); - if (dbSecret === undefined) { - console.error('Error getting secret:', secrets.dbSecretArn); + const dbSecretString = await vault.getSecret(dbSecretArn); + if (dbSecretString === undefined) { + console.error('Error getting secret:', dbSecretArn); return; } - const secret = JSON.parse(dbSecret); + + const dbSecret = JSON.parse(dbSecretString); return { - dbUri: `postgresql://${secret.username}:${secret.password}@${secret.dbHost}:${secret.dbPort}/${secret.dbName}` + dbUri: `postgresql://${dbSecret.username}:${dbSecret.password}@${dbHost}:${dbPort}/${dbName}` }; }; diff --git a/apps/sandbox/src/server.ts b/apps/sandbox/src/server.ts index eb2ebbc0..2bfc07a9 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({ 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/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..564cf0c4 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", "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/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/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..9f56113c 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'; + createNoopPdfParser, +} 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: createNoopPdfParser(), }); } 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..997153e2 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'; 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/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..b041e75d 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,9 +1,9 @@ { - "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", "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 +14,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..160cd385 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", @@ -24,7 +27,7 @@ }, "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..09e49a49 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 +- `aws-main`: Production deployment to AWS (App Runner + RDS) +- `aws-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:aws-demo + +# Deploy infrastructure for main/production environment +pnpm deploy:aws-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..9f3007c3 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:aws-main && pnpm build:synth:aws-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:aws-main": "DEPLOY_ENV=aws-main cdktf synth", + "build:synth:aws-demo": "DEPLOY_ENV=aws-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:aws-main": "DEPLOY_ENV=aws-main cdktf deploy", + "deploy:aws-demo": "DEPLOY_ENV=aws-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..036e573e 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 'aws-main': + import('./spaces/aws/main'); + break; + case 'aws-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..e9aac537 --- /dev/null +++ b/infra/cdktf/src/lib/aws/sandbox-stack.ts @@ -0,0 +1,373 @@ +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 { 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 { SecretsmanagerSecret } from '../../../.gen/providers/aws/secretsmanager-secret'; +import { SecretsmanagerSecretVersion } from '../../../.gen/providers/aws/secretsmanager-secret-version'; +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'; + +import { getDatabaseSecretKey } from '@flexion/forms-infra-core'; + +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 NAT) + 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, + }); + + // 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, + }, + }); + + // Generate random password for database + const dbUsername = 'postgres'; + const dbPassword = Fn.base64encode( + Fn.uuid() // Use UUID for a secure random password + ); + + // Database secret (only username and password - host/port/db passed as env vars) + const dbSecret = new SecretsmanagerSecret(this, `${id}-db-secret`, { + name: getDatabaseSecretKey(environment), + description: `Database credentials for ${environment}`, + tags: { + Environment: environment, + }, + }); + + new SecretsmanagerSecretVersion(this, `${id}-db-secret-version`, { + secretId: dbSecret.id, + secretString: Fn.jsonencode({ + username: dbUsername, + password: dbPassword, + }), + }); + + // 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 + 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, + password: dbPassword, + 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: [publicSubnet1.id, publicSubnet2.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_ARN: dbSecret.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, + }, + }); + } +} 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..0cfc0195 --- /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: 'demo-aws', + }); + } +} + +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..5914decd --- /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: 'main-aws', + }); + } +} + +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..720d22e8 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", "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/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/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/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..ca1a2b4d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@gsa-tts/forms", + "name": "@flexion/forms", "version": "1.0.0-beta.1", "description": "10x Forms Platform", "type": "module", @@ -7,7 +7,7 @@ "license": "CC0", "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..d363d0f7 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,11 +1,14 @@ { - "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", "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", @@ -13,8 +16,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..48246d6f 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,11 +1,14 @@ { - "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", "main": "dist/index.js", "types": "dist/index.d.ts", + "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/package.json b/packages/database/package.json index 5a2f0a0d..38ca8d0a 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -1,12 +1,15 @@ { - "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", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "dist/types/index.d.ts", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "exports": { ".": { "types": "./dist/types/index.d.ts", @@ -31,7 +34,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..7590feb7 100644 --- a/packages/database/src/clients/kysely/types.ts +++ b/packages/database/src/clients/kysely/types.ts @@ -16,6 +16,7 @@ export interface Database { forms: FormsTable; form_sessions: FormSessionsTable; form_documents: FormDocumentsTable; + llm_request_cache: LlmRequestCacheTable; } export type DatabaseClient = Kysely; @@ -72,3 +73,15 @@ interface FormDocumentsTable { export type FormDocumentsTableSelectable = Selectable; export type FormDocumentsTableInsertable = Insertable; export type FormDocumentsTableUpdateable = Updateable; + +interface LlmRequestCacheTable { + id: Generated; + cache_key: string; + response_data: string; + created_at: Generated; + accessed_at: Date; + access_count: number; +} +export type LlmRequestCacheTableSelectable = Selectable; +export type LlmRequestCacheTableInsertable = Insertable; +export type LlmRequestCacheTableUpdateable = Updateable; 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..c25e1efa 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,8 +1,12 @@ { - "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", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, "scripts": { "build": "run-p build:lib build:styles && pnpm build:storybook", "build:lib": "vite build", @@ -23,7 +27,9 @@ "test:watch": "pnpm onchange './**/*.{tsx,ts}' -- pnpm test:url" }, "files": [ - "dist/**/*" + "dist/**/*", + "static/**/*", + "sass/**/*" ], "size-limit": [ { @@ -71,8 +77,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/index.tsx b/packages/design/src/AvailableFormList/index.tsx index 8024ffef..5a438eea 100644 --- a/packages/design/src/AvailableFormList/index.tsx +++ b/packages/design/src/AvailableFormList/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; -import { type FormService } from '@gsa-tts/forms-core'; +import { type FormService } from '@flexion/forms-core'; import * as AppRoutes from '../FormManager/routes.js'; @@ -10,7 +10,7 @@ type FormDetails = { 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({ @@ -23,13 +23,20 @@ export default function AvailableFormList({ urlForFormManager: UrlForFormManager; }) { const [forms, setForms] = useState([]); - useEffect(() => { + const location = useLocation(); + + const loadForms = React.useCallback(() => { formService.getFormList().then(result => { if (result.success) { setForms(result.data); } }); - }, []); + }, [formService]); + + useEffect(() => { + loadForms(); + }, [location.pathname, location.hash, location.key, loadForms]); + return ( <>
@@ -103,27 +110,11 @@ const FormList = ({ {form.description} - + )) @@ -133,6 +124,40 @@ const FormList = ({ ); }; +const FormActions = ({ + form, + urlForForm, + urlForFormManager, +}: { + form: FormDetails; + urlForForm: UrlForForm; + urlForFormManager: UrlForFormManager; +}) => { + const formUrl = urlForForm(form.id); + + return ( +
+ {formUrl && ( + + Go to form + + )} + + Edit + + + Delete + +
+ ); +}; + const DebugTools = () => { return (