diff --git a/.agents/README.md b/.agents/README.md new file mode 100644 index 00000000..3a44bd55 --- /dev/null +++ b/.agents/README.md @@ -0,0 +1,46 @@ +# .agents/ — vtex/address-form + +This directory is the **source of truth** for all AI agent configuration in this repository. + +## Directory layout + +``` +.agents/ +├── README.md # This file +├── rules/ # Always-on rules (loaded automatically by supporting agents) +│ ├── 00-vtex-address-form.md # Baseline repo rules (stack, architecture, DO NOTs) +│ ├── 10-test-discipline.md # Test strategy and what requires a test +│ ├── 20-i18n-discipline.md # i18n key governance and Crowdin workflow +│ └── 30-public-api-discipline.md # npm + VTEX IO public API change protocol +├── skills/ # Vendored skills from vtex/vtex-agent-skills +│ ├── specification/SKILL.md # /specification slash command skill +│ │ └── references/template.md # SDD Lite spec template +│ ├── implementing/SKILL.md # /implementing slash command skill +│ └── README.md # Skill table + refresh instructions +└── commands/ # Repo-specific slash commands + ├── sdd-lite-bootstrap.md # /sdd-lite-bootstrap — full SDD Lite flow + └── sdd-full-bootstrap.md # /sdd-full-bootstrap — full Spec Kit flow +``` + +## Routing table — which agent reads what + +| Agent tool | Reads from | +|---|---| +| Claude Code | `.claude/` (symlinks → `.agents/`) | +| Cursor | `.agents/rules/*.md`, `.agents/skills/*/SKILL.md` | +| Copilot | `.github/copilot-instructions.md` (not present; falls back to `.agents/`) | + +## Updating vendored skills + +The `skills/` directory is vendored verbatim from [`vtex/vtex-agent-skills`](https://github.com/vtex/vtex-agent-skills). To update: + +```bash +npx skills add vtex/vtex-agent-skills +# or +git clone https://github.com/vtex/vtex-agent-skills /tmp/vtas +cp /tmp/vtas/skills/specification/SKILL.md .agents/skills/specification/SKILL.md +cp /tmp/vtas/skills/specification/references/template.md .agents/skills/specification/references/template.md +cp /tmp/vtas/skills/implementing/SKILL.md .agents/skills/implementing/SKILL.md +``` + +Do **not** hand-edit `SKILL.md` files. Repo-specific guidance goes in `.agents/rules/`. diff --git a/.agents/commands/commands b/.agents/commands/commands new file mode 120000 index 00000000..082ad8d1 --- /dev/null +++ b/.agents/commands/commands @@ -0,0 +1 @@ +.agents/commands \ No newline at end of file diff --git a/.agents/commands/sdd-full-bootstrap.md b/.agents/commands/sdd-full-bootstrap.md new file mode 100644 index 00000000..f6ff4ed2 --- /dev/null +++ b/.agents/commands/sdd-full-bootstrap.md @@ -0,0 +1,54 @@ +# /sdd-full-bootstrap — SDD Full (Spec Kit) workflow for vtex/address-form + +Use this command to understand and run the SDD Full (Spec Kit) development cycle for this repository. + +## When to use SDD Full + +Use SDD Full (not SDD Lite) when: +- The task is > 5 days +- Cross-team or cross-repo coordination is required +- Significant architectural impact (e.g., changing the `PostalCodeRules` type shape) +- Breaking change to `react/index.ts` or `react/components.ts` (requires consumer coordination with vtex.omnishipping, vtex.shipping-preview, vtex.checkout) +- Changes to `react/types/rules.ts` or `react/types/address.ts` that affect the public type surface +- High ambiguity or unresolved product decisions + +## One-time machine setup + +```bash +# Install uv +curl -LsSf https://astral.sh/uv/install.sh | sh +# or: brew install uv + +source "$HOME/.local/bin/env" +uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@v0.6.0 +specify version +``` + +## Per-task workflow + +``` +1. Drop the relevant PRD/RFC sections into docs/scope_of_work/.md +2. /speckit.specify → specs//spec.md (commit — this is the only committed artifact) +3. /speckit.clarify → resolve ambiguity, update spec +4. /speckit.plan → specs//plan.md (DO NOT commit — gitignored) +5. /speckit.tasks → specs//tasks.md (DO NOT commit — gitignored) +6. /speckit.analyze → specs//analysis.md (DO NOT commit — gitignored) +7. /speckit.implement phase 1 only → code + branch (commit code only) +``` + +The **Constitution** (`.specify/memory/constitution.md`) is the architectural contract. Spec Kit reads it before every plan/analyze/implement step. + +## Multi-repo coordination + +For breaking changes that affect `vtex.omnishipping` and `vtex.shipping-preview`: + +1. Start with this repo's spec (`specs//spec.md`) +2. Use the [vtex/speckit-multi-repo](https://github.com/vtex/speckit-multi-repo) extension to propagate the spec context to consumer repos +3. Consumer repos have their own `specs/` directories and constitutions — respect their architecture rules + +## address-form-specific notes for SDD Full + +- The constitution at `.specify/memory/constitution.md` encodes the dual-nature (IO app + npm package) as a non-negotiable principle +- Breaking API changes must list ALL consumers in the spec's Risks & Mitigations table +- The spec must call out version bump implications (both `manifest.json` and `react/package.json`) +- `PostalCodeRules` shape changes are the highest-risk category — they require coordinated updates in both omnishipping and shipping-preview before merging diff --git a/.agents/commands/sdd-lite-bootstrap.md b/.agents/commands/sdd-lite-bootstrap.md new file mode 100644 index 00000000..6585d859 --- /dev/null +++ b/.agents/commands/sdd-lite-bootstrap.md @@ -0,0 +1,66 @@ +# /sdd-lite-bootstrap — SDD Lite workflow for vtex/address-form + +Use this command to understand and run the SDD Lite development cycle for this repository. + +## When to use SDD Lite + +Use SDD Lite (not Spec Kit) when: +- The task fits in < 5 days +- Single-repo, contained scope +- Bug fixes, small features, focused refactors +- Low ambiguity +- No cross-consumer coordination needed +- Adding a new country rule file without changing `PostalCodeRules` shape +- No breaking changes to `react/index.ts` or `react/components.ts` + +Use **SDD Full** instead when the task involves changing the `PostalCodeRules` type, adding/removing exports from `react/index.ts` or `react/components.ts`, or any cross-repo coordination. + +## The two-skill pipeline + +``` +/specification "" + ↓ + Writes specs/.md + Opens PR on branch spec/ + Status: Draft + ↓ +[Engineer reviews, asks PM if needed, edits spec, manually flips Status: Approved, merges] + ↓ +/implementing "specs/.md" + ↓ + Non-interactive sandbox run + Creates branch feat/ + Writes failing tests first, then minimal implementation + Opens PR with: Summary / Tests / Assumptions / Deviations / Follow-ups / Spec + On success: Status: Done in the spec file + On block: opens GitHub issue "implementing blocked: " and ends +``` + +## address-form-specific notes + +### Running tests +```bash +# Unit tests — run from react/ subdirectory +yarn --cwd react test + +# Watch mode during development +yarn --cwd react test:watch + +# i18n parity (runs automatically on pre-push) +yarn lint:locales +``` + +### What always needs a test +- New country rule file: assert fields array, postalCodeFrom type, required field presence +- New selector/transform function: unit test every logic branch +- New validation path: test valid/invalid/edge cases +- Any bug fix: failing regression test before the fix + +### Country rule files +New files go in `react/country/.ts`. Export a `PostalCodeRules` object. Copy an existing similar country as a template. Test with the demo app (`cd demo && yarn start`). + +### Public API caution +If the spec touches `react/index.ts` or `react/components.ts`, escalate to SDD Full — those changes require consumer coordination with `vtex.omnishipping` and `vtex.shipping-preview`. + +### i18n +New user-visible strings go in `messages/en.json`. Run `yarn lint:locales` before pushing. diff --git a/.agents/rules/00-vtex-address-form.md b/.agents/rules/00-vtex-address-form.md new file mode 100644 index 00000000..d2cc75eb --- /dev/null +++ b/.agents/rules/00-vtex-address-form.md @@ -0,0 +1,87 @@ +--- +applyTo: "**/*" +--- + +# Baseline rules — vtex/address-form + +These rules apply to **every** agent conversation in this repo. They override generic best practices and codify the address-form team's standards. + +## Repository purpose + +`vtex/address-form` is the **reusable address input UI library** for VTEX. It provides country-aware address field rendering, postal code autocomplete, geolocation-based address resolution, and field validation. + +It has a **dual nature**: +- **VTEX IO app** (`vtex.address-form`) — consumed as a peer dependency inside VTEX IO apps +- **npm package** (`@vtex/address-form`) — published from `react/lib/` via Rollup + +It is consumed by `vtex.omnishipping`, `vtex.shipping-preview`, `vtex.checkout`, and any storefront app that needs address input. A breaking change here can affect checkout for all VTEX merchants. + +## Stack (do not invent alternatives) + +- VTEX IO `react@2.x` + `messages@0.x` builders (no `node` builder, no `graphql` builder) +- React 16.x (peerDep: `15.x || 16.x`) — do not upgrade without an RFC +- **TypeScript 3.9** for new code; JavaScript (JSX/JS) for legacy files. Do not migrate past TS 3.9 without an RFC. +- **No Redux, no MobX** — state is managed entirely via React Context (`RulesContext`, `AddressContext`) +- Jest 26 + `@testing-library/react` 12 + Enzyme 3 (legacy) +- ESLint with `eslint-config-vtex` + `eslint-config-vtex-react` +- Prettier with `@vtex/prettier-config` +- `@vtex/intl-equalizer` for i18n parity; Crowdin for translations; `%two_letters_code%` in `crowdin.yml` +- `window.logSplunk` for Splunk telemetry (NOT `@vtex/evidence-client-js`) +- Package manager: `yarn` (classic v1). No npm, no pnpm. +- Node 16 in the dev container. +- **Test runner**: `yarn --cwd react test` (NOT at root — Jest config lives in `react/package.json`) + +## Code style — non-negotiable + +- 2 spaces, LF, UTF-8 (see `.editorconfig`). +- Run lint **only on files you touched**, never globally. +- Never disable ESLint rules without an inline comment explaining why and a TODO. +- Never reformat unrelated files. PRs must show a small, reviewable diff. + +## Architecture rules + +- **Two React Contexts are the backbone**: + - `RulesContext` (from `addressRulesContext.tsx`) — provides loaded `PostalCodeRules` to the subtree. Set by ``. + - `AddressContext` (from `addressContainerContext.tsx`) — provides `{ address, handleAddressChange, Input }`. Set by ``. +- **``** dynamically imports `react/country/.ts` rules via `import()`. Falls back to `react/country/default.ts` on 404. +- **``** runs `validateChangedFields` on every address change and triggers postal code autocomplete when a valid postal code is entered. +- **Country rules live in `react/country/*.ts`**. Each file exports a `PostalCodeRules` object. New country files must include a unit test. +- **Selectors live in `react/selectors/`**. They are pure functions — no side effects, no component imports. +- **Transforms live in `react/transforms/`**. They produce new values from inputs — pure, no side effects. +- **`react/index.ts`** is the npm package public API (named exports). **`react/components.ts`** is the VTEX IO public component map (default export). Both are load-bearing for consumers — see rule 30. +- **No new dependencies** without a spec justification and explicit CHANGELOG entry. +- **`react/metrics.ts`** `window.logSplunk` calls must not be removed without a follow-up telemetry task. + +## Versioning & releases + +- **`manifest.json` and `react/package.json` must move in lockstep.** Use `publish-release.sh` — never bump manually. +- Release notes go in `CHANGELOG.md` (Keep a Changelog format). +- Conventional Commits required. Breaking changes use `feat!:` / `fix!:` and a `BREAKING CHANGE:` footer. + +## Testing + +- Test command (CI): `yarn --cwd react test`. +- Pre-push hook runs: `yarn lint:locales && yarn --cwd react test`. +- New utility functions and logic paths **must** have unit tests. +- New components **should** have tests asserting key rendering behaviors. +- Bug fixes ship with a regression test before the fix. +- Prefer `@testing-library/react` for new tests; Enzyme is legacy and should not be extended. + +## Branching & PRs + +- Spec PRs: branch `spec/`, only the spec file in the diff. +- Implementation PRs: branch `feat/` — even for fixes. Use `feat!:` when breaking. +- Never push to `main` directly. +- PR title = Conventional Commit. PR body must link the spec. + +## Things to NOT do + +- Do not add a `node/` builder. +- Do not introduce Redux or any other state library. +- Do not switch from yarn to npm or pnpm. +- Do not migrate TypeScript past 3.9 without an RFC. +- Do not call VTEX private APIs from this app — it's a frontend library. +- Do not commit `.env`, `.env.local`, or anything containing real account/workspace credentials. +- Do not manually edit locale files other than `en.json` — Crowdin owns the translations. +- Do not edit `manifest.json` or `react/package.json` version fields manually. +- Do not remove `window.logSplunk` calls from `react/metrics.ts` without a follow-up task. diff --git a/.agents/rules/10-test-discipline.md b/.agents/rules/10-test-discipline.md new file mode 100644 index 00000000..0078be68 --- /dev/null +++ b/.agents/rules/10-test-discipline.md @@ -0,0 +1,68 @@ +--- +applyTo: "react/**/*.{js,jsx,ts,tsx}" +--- + +# Test discipline — vtex/address-form + +## Order of operations + +1. Read the spec. Identify User Stories and their Given/When/Then acceptance criteria. +2. Translate each acceptance criterion and each row of the Key Scenarios table into one or more tests (red first). +3. Implement the minimum to flip them green. +4. Run `yarn --cwd react test` (unit) and `yarn lint:locales` (i18n parity). + +## Unit tests (Jest 26) + +### Framework preference + +- **New tests use `@testing-library/react`**. It is the current standard. +- Enzyme is present for legacy tests only. Do not write new Enzyme tests; when modifying an Enzyme test, consider migrating it to Testing Library if the scope is reasonable. + +### What requires a unit test + +- Every new selector function under `react/selectors/`. +- Every new transform function under `react/transforms/`. +- Every new validation path in `react/validateAddress.ts`. +- Every new country rule file under `react/country/` — assert required fields are present, postal code regex is correct, and the postalCodeFrom type is valid. +- Every new or modified component that contains conditional logic. +- Every bug fix — add a failing regression test before the fix. + +### What does NOT require a unit test + +- New country rule files that are pure data changes with no logic (new field ordering only) — but a manual demo app check is required. +- Type-only changes (TypeScript interface additions with no runtime behavior change). +- Adding a key to `messages/en.json` without logic changes. +- Pure CSS/style changes. +- Pure dependency upgrades (Renovate-style). + +### Test file location + +- Tests for `react/Foo.js` or `react/Foo.tsx` live at `react/Foo.test.js` or `react/__tests__/Foo.test.tsx` (both patterns exist — match the existing co-location convention for the file's directory). +- Tests for `react/selectors/bar.ts` live at `react/selectors/bar.test.ts`. +- Tests for `react/transforms/baz.ts` live at `react/transforms/baz.test.ts`. +- Tests for `react/country/XYZ.ts` live alongside the rule file or in `react/country/__tests__/` if that pattern is established. +- Shared test utilities live in `react/test-modules/test-utils.tsx`. + +### Fixtures and mocks + +- Reusable address fixtures live in `react/__mocks__/`. Use them before inlining test data. +- Country rule mocks live in `react/country/__mocks__/`. Use `usePostalCode`, `useOneLevel`, `useTwoLevels`, `useThreeLevels`, `displayBrazil`, `displayUSA` as the canonical fixtures. +- Google Maps mocks live in `react/geolocation/__mocks__/`. +- `postalCodeService` mock lives at `react/__mocks__/postalCodeService.js`. +- When adding a new external dependency, add a corresponding mock under `react/__mocks__/.js`. + +### Snapshot tests + +- Existing snapshots cover `AddressForm`, `AddressSummary`, `CountrySelector`, `DefaultInput`, `GeolocationNumberInput`, `InputText`. Do not add new snapshots unless the rendered output is genuinely a stable contract. +- Never update snapshots with `--updateSnapshot` to clear noise — update them intentionally. + +## i18n parity + +- After any `messages/en.json` change, run `yarn lint:locales` to confirm parity (reference locale: `pt`). +- A failing `lint:locales` is a blocker — do not merge. +- This check also runs on `pre-push`. + +## Coverage + +- No enforced threshold today. Do not let coverage drop on changed files. +- `yarn test:coverage` runs through `react/` Jest config. diff --git a/.agents/rules/20-i18n-discipline.md b/.agents/rules/20-i18n-discipline.md new file mode 100644 index 00000000..f6c825b6 --- /dev/null +++ b/.agents/rules/20-i18n-discipline.md @@ -0,0 +1,49 @@ +--- +applyTo: "{messages/**,react/**/*.{js,jsx,ts,tsx}}" +--- + +# i18n discipline — vtex/address-form + +## Source of truth + +`messages/en.json` is the **only file agents and engineers edit directly**. The other locale files are owned by Crowdin and must not be manually edited. + +**Reference locale for parity checks:** `pt` (Brazilian Portuguese). The `intl-equalizer` configuration (`"intl-equalizer": { "referenceLocale": "pt" }` in root `package.json`) uses `pt` to determine key parity. + +**Crowdin locale format:** `crowdin.yml` uses `%two_letters_code%` (e.g., `pt`, `es`, `de`) — **not** the full locale code (`pt-BR`). This differs from omnishipping, which uses `%locale%`. + +## Adding a new string + +1. Add the key and its English default to `messages/en.json`. +2. Use the key in the component via `react-intl`'s `` or `intl.formatMessage(...)` — never hard-code the string. +3. Run `yarn lint:locales` to confirm parity. Missing keys in other locales are expected — Crowdin will fill them. +4. Include the new key in the CHANGELOG entry. + +## Key naming convention + +- Use `.` or `.`. +- Keep keys stable — once a key is published and Crowdin has produced translations, renaming it is a breaking change. +- Examples: `addressForm.street`, `postalCode.label`, `geolocation.searchPlaceholder`, `countrySelector.label`. + +## Removing or renaming a key + +Removing or renaming a key in `en.json`: + +1. Constitutes a **breaking change** (active translations become orphaned in Crowdin). +2. Requires a `BREAKING CHANGE:` footer in the Conventional Commit. +3. Must be called out in the Arch Decisions section of the spec (Key Decision — backward compatibility). +4. Requires a CHANGELOG entry under `### Breaking Changes`. + +## Component usage + +- Always use `react-intl`'s `` or `intl.formatMessage()` — never fall back to a string literal. +- The intl utilities in `react/intl/utils.jsx` provide `injectIntl` and `intlShape` for class component compatibility. +- Parametrized messages use ICU syntax in `en.json` (e.g., `"{count, plural, one {# item} other {# items}}"`). +- Do not put HTML markup inside message values — use `FormattedMessage`'s `values` prop. + +## Crowdin workflow + +- `crowdin.yml` maps `messages/en.json` as the source and `messages/%two_letters_code%.json` as the translation output. +- Do not modify `crowdin.yml` without coordinating with the localization team. +- Translation PRs from Crowdin are auto-merged by CI; do not interfere with them. +- `messages/context.json` provides Crowdin with string context — keep it in sync when adding keys. diff --git a/.agents/rules/30-public-api-discipline.md b/.agents/rules/30-public-api-discipline.md new file mode 100644 index 00000000..96368970 --- /dev/null +++ b/.agents/rules/30-public-api-discipline.md @@ -0,0 +1,82 @@ +--- +applyTo: "react/{index.ts,components.ts,types/**,helpers.ts,inputs.ts}" +--- + +# Public API discipline — vtex/address-form + +`vtex/address-form` has **two public surfaces** that serve different consumers. A breaking change in either requires a major version bump and coordinated consumer updates. + +## Surface 1 — npm package (`react/index.ts`) + +Consumed by external packages via `@vtex/address-form`. This is the richest API. + +### What counts as the npm public API + +- Every named export from `react/index.ts` +- All types re-exported from `react/types/address.ts` and `react/types/rules.ts` +- The `helpers` default export (and its individual function members) +- The `components` default export (same as surface 2 below) + +Current named exports (as of baseline): + +| Export | Source | +|---|---| +| `CountrySelector` | `./CountrySelector` | +| `AddressForm` | `./AddressForm` | +| `AddressSummary` | `./AddressSummary` | +| `PostalCodeGetter` | `./PostalCodeGetter` | +| `AddressContainer` | `./AddressContainer` | +| `AutoCompletedFields` | `./AutoCompletedFields` | +| `AddressRules` | `./AddressRules` | +| `AddressSubmitter` | `./AddressSubmitter` | +| `addValidation`, `removeValidation` | `./transforms/address` | +| `isValidAddress`, `validateField` | `./validateAddress` | +| `injectRules` | `./addressRulesContext` | +| `injectAddressContext` | `./addressContainerContext` | +| `helpers` | `./helpers` (default) | +| All types | `./types/address`, `./types/rules` | + +## Surface 2 — VTEX IO components map (`react/components.ts`) + +Consumed by `vtex.omnishipping`, `vtex.shipping-preview`, and any IO app that uses `vtex.address-form` as a peer dep via the VTEX IO framework. + +### What counts as the IO public API + +- Every key in the `default` export object of `react/components.ts` + +Current keys: `AddressContainer`, `AddressForm`, `AddressRules`, `AddressSubmitter`, `AddressSummary`, `AutoCompletedFields`, `CountrySelector`, `GoogleMapsContainer`, `Map`, `PostalCodeGetter`, `PostalCodeLoader`, `StyleguideInput`, `StyleguideButton`. + +## Changes that REQUIRE a major version bump + +- Removing or renaming a named export from `react/index.ts`. +- Removing or renaming a key from `react/components.ts`'s default export. +- Changing the required props of any exported component in a way that breaks existing usage. +- Changing the shape of `PostalCodeRules` in `react/types/rules.ts`. +- Changing the signature of any exported function (`addValidation`, `removeValidation`, `isValidAddress`, `validateField`, `injectRules`, `injectAddressContext`). +- Changing the country rule object shape expected by ``. + +## Changes that are safe (minor/patch) + +- Adding a new named export to `react/index.ts`. +- Adding a new key to `react/components.ts`'s default export. +- Adding optional props with defaults to an existing exported component. +- Adding a new country file under `react/country/`. +- Internal refactors that preserve the public contract. +- Bug fixes that align actual behavior with the documented contract. + +## Process for breaking changes + +1. The spec must call out the breaking change in the Arch Decisions section. +2. The spec's Risks & Mitigations table must list every consumer (`vtex.omnishipping`, `vtex.shipping-preview`, `vtex.checkout`, and known external npm consumers) and the coordination plan. +3. The PR description must list every consumer that needs to update. +4. The CHANGELOG entry uses `BREAKING CHANGE:` footer. +5. Both `manifest.json` and `react/package.json` major versions move together via `publish-release.sh`. +6. Coordinate with the checkout team before merging — do not merge during a release freeze. + +## Anti-patterns to reject in code review + +- Removing an export "because it looks unused" — verify all consumers first. +- Renaming an exported type and calling it an internal refactor. +- Changing a required prop to optional (or vice versa) without a version bump. +- Silently changing the shape of `PostalCodeRules` fields. +- Adding a "v2" function next to "v1" without a deprecation plan. diff --git a/.agents/rules/rules b/.agents/rules/rules new file mode 120000 index 00000000..143ba234 --- /dev/null +++ b/.agents/rules/rules @@ -0,0 +1 @@ +.agents/rules \ No newline at end of file diff --git a/.agents/skills/README.md b/.agents/skills/README.md new file mode 100644 index 00000000..49a6bf78 --- /dev/null +++ b/.agents/skills/README.md @@ -0,0 +1,21 @@ +# Skills — vtex/address-form + +Skills vendored verbatim from [`vtex/vtex-agent-skills`](https://github.com/vtex/vtex-agent-skills) v1.2.0. + +| Skill | Slash command | Purpose | +|---|---|---| +| `specification` | `/specification ""` | Generate a SDD spec at `specs/.md` (Draft) | +| `implementing` | `/implementing "specs/.md"` | Autonomous implementation from an Approved spec → PR (Done) | + +## Refresh + +```bash +npx skills add vtex/vtex-agent-skills +# or manually: +git clone https://github.com/vtex/vtex-agent-skills /tmp/vtas +cp /tmp/vtas/skills/specification/SKILL.md .agents/skills/specification/SKILL.md +cp /tmp/vtas/skills/specification/references/template.md .agents/skills/specification/references/template.md +cp /tmp/vtas/skills/implementing/SKILL.md .agents/skills/implementing/SKILL.md +``` + +Do **not** hand-edit SKILL.md files. All repo-specific guidance belongs in `.agents/rules/`. diff --git a/.agents/skills/implementing/SKILL.md b/.agents/skills/implementing/SKILL.md new file mode 100644 index 00000000..7c2dc297 --- /dev/null +++ b/.agents/skills/implementing/SKILL.md @@ -0,0 +1,147 @@ +--- +name: implementing +description: Non-interactive sandbox implementation from an approved spec to a PR. Use when the user mentions "implement", "implement spec", "implement specs/.md", sandbox implementation, or autonomous implementation from a specification. +license: MIT +metadata: + author: VTEX + version: "1.2.0" +--- + +# Implementing — Spec to Code + +Implement a feature **autonomously** from an approved `specs/.md`. + +## Execution model + +This skill assumes a **sandboxed, asynchronous** run: **no human interaction** during execution (no questions, confirmations, or waiting for replies). + +| | | +|---|---| +| **Input** | `specs/.md` (must be `Approved` — see Phase 1) | +| **Success output** | Pull Request on `feat/`, spec status → `Done` | +| **Blocked output** | Agent **ends** without a completed feature PR; a **GitHub issue** documents why implementation could not finish | + +**Pipeline (end-to-end):** the **specification** skill produces the spec; this skill consumes it. There is **no intermediate planning artifact or skill** — you plan, decompose work, and sequence tasks internally. The spec is the single handoff contract. + +**Operating loop:** pick a user story → implement with tests → verify against acceptance criteria → commit → repeat until every story is done. **Evidence over claims:** nothing is "done" until tests and checks prove it. + +## Constraints + +**What you CAN do:** +- Create and modify source code, tests, and configuration files required by the spec +- Install dependencies explicitly mentioned in the Technical Contract or required by the chosen approach +- Create branches, commit, and open PRs +- Open a **GitHub issue** (or equivalent) when implementation is **impossible** to complete after tie-breakers and reasonable effort — see *Non-interactive execution* + +**What you CANNOT do:** +- Implement features, endpoints, or behaviors not described in the spec +- Change the spec file itself — if the spec is wrong, contradictory, or blocking, follow *Non-interactive execution* (assumptions in PR, or issue + terminate) +- Skip writing tests for a user story that has acceptance criteria +- Merge or push to the main branch without explicit user consent (from platform policy outside this run) + +**The goal is simple: make every acceptance criterion pass.** The spec is the single source of truth. If the spec says it, implement it. If the spec doesn't say it, don't. + +## Workflow + +### Phase 1: Load and validate + +1. Read the full `specs/.md` +2. Check that the status is `Approved`. If it is **not** `Approved` (e.g. `Draft`): **end the run** immediately. Do **not** open an implementation PR. Emit a **structured report** in sandbox logs/output (reason, current status, path to the spec file) so the orchestrator can mark the job failed. +3. Extract from the spec: + - **User Stories + Acceptance Criteria** → the work units + - **Key Scenarios** → the test cases + - **Arch Decisions** → the technical approach and constraints + - **Technical Contract** → interfaces, models, and boundaries to implement exactly +4. If the spec references repositories you don't have context on, use the GitHub tool to fetch their structure, README, and dependencies + +**Internal planning:** derive order of work, file touch list, and test strategy from the spec and repo. No separate plan-approval step. + +### Phase 2: Codebase reconnaissance + +Before writing any code, understand the existing codebase: + +- Project structure, conventions, and patterns already in use +- Existing tests: framework, naming conventions, where they live +- Dependency management: what's already installed, what's available +- CI/CD: how tests are run, linting rules, build steps + +Adapt your implementation to match existing patterns. Don't introduce new conventions unless the spec explicitly calls for it. + +### Phase 3: Implementation loop + +Work through user stories one at a time. For each story: + +``` +LOOP per user story: + +1. Write failing tests derived from the acceptance criteria and key scenarios +2. Run the tests — confirm they fail for the right reason +3. Implement the minimal code to make the tests pass +4. Run the tests — confirm they pass +5. Refactor if needed (tests must still pass after) +6. Commit with a descriptive message +7. Move to the next story +``` + +**Rules:** +- Follow the architecture described in Arch Decisions — don't contradict accepted decisions +- Implement interfaces and models exactly as defined in the Technical Contract +- One commit per user story (or per logical unit if a story is large) +- Commit messages follow the repo's existing convention; if none exists, use: `feat: ` +- If a test fails after implementation and you cannot fix it after **reasonable attempts** (e.g. 3+ focused tries), treat this as **blocking** → *Non-interactive execution* (issue + terminate). Do not force a green build by gutting tests or the spec. + +**If something goes wrong:** +- Implementation breaks existing tests → fix the regression before moving on; if the fix is not achievable without violating the spec or breaks invariants, treat as **blocking** → issue + terminate +- Ambiguity in acceptance criteria → apply **tie-break order**: **Key Scenarios** → **Arch Decisions** → **Technical Contract** → **repository conventions**. If ambiguity is **resolved**, document **Assumptions** in the PR body (dedicated section). If still **unresolvable** (no coherent reading), treat as **blocking** → issue + terminate +- A dependency is missing → install it if possible and note it in the PR summary; if it cannot be installed in the sandbox, treat as **blocking** → issue + terminate +- The spec contradicts itself in a way tie-breakers cannot reconcile → **blocking** → issue + terminate; do not guess + +### Phase 4: Verification + +After all user stories are implemented: + +1. Run the full test suite — everything must pass +2. Walk through the Key Scenarios table and confirm each scenario is covered by a test +3. Check that no files were modified outside the scope of the spec +4. Review your own changes: look for leftover debug code, TODOs, or unused imports + +### Phase 5: Deliver (success path) + +1. Update the spec status from `Approved` to `Done` +2. Open a Pull Request: + - Branch: `feat/` + - Title: `feat: ` + - Body: use **sections** (not a live chat summary): + - **Summary** — what was implemented, per user story + - **Tests** — what was added or changed + - **Assumptions** — explicit assumptions from tie-breakers (omit section if none) + - **Deviations** — any deviation from the spec with justification (omit if none) + - **Follow-ups** — risks or optional next steps (omit if none) + - **Spec** — link to `specs/.md` + - Reference the spec PR if it exists + +## Non-interactive execution + +There is **no** human in the loop during the run: do not ask questions or wait for answers. + +**Success:** proof lives in the **PR description** and **passing tests** (and CI, if applicable). + +**Blocked — impossible to complete the spec:** +1. **Stop** — do not open a PR that claims the feature is done. +2. **Open a GitHub issue** with at minimum: + - Title pattern: `implementing blocked: ` + - Link to `specs/.md` + - What was attempted (stories, commits, branches if any) + - Objective reason for the block (contradiction, missing dependency, irresolvable ambiguity, tests/CI that cannot be satisfied, etc.) + - Evidence: relevant spec excerpts, error output, failing test names +3. **End the agent run.** Sandbox logs should reflect failure for the orchestrator. + +Optional: push a WIP branch only if the orchestrator requires a trace — the **issue** is the primary failure artifact. + +## Important + +- The spec is law — don't add features, don't skip requirements +- Tests are not optional — every acceptance criterion must have a corresponding test +- Match existing codebase patterns — don't impose new conventions +- Keep commits atomic — one story, one commit +- Never silently work around a broken spec — resolve with documented assumptions in the PR, or file an issue and stop diff --git a/.agents/skills/skills b/.agents/skills/skills new file mode 120000 index 00000000..50182300 --- /dev/null +++ b/.agents/skills/skills @@ -0,0 +1 @@ +.agents/skills \ No newline at end of file diff --git a/.agents/skills/specification/SKILL.md b/.agents/skills/specification/SKILL.md new file mode 100644 index 00000000..9420f56c --- /dev/null +++ b/.agents/skills/specification/SKILL.md @@ -0,0 +1,150 @@ +--- +name: specification +description: Generate a Spec Driven Development (SDD) document containing Business Context, Arch Decisions, and Technical Contract. Use when the user mentions "spec", "init spec", "create specification", references a file in the specs/ folder, or wants to create a feature specification. +license: MIT +metadata: + author: VTEX + version: "1.2.0" +--- + +# Specification — Spec Driven Development + +Generate a complete SDD for a feature and write it as a structured markdown file to `specs/.md`. Uses a **hybrid approach**: for simple, well-described features the agent generates the full spec in a single pass; for complex or ambiguous features it gathers requirements interactively before writing. + +**End-to-end flow:** this skill is the **start** of the pipeline; **implementing** is the **end**. After the spec is `Approved`, the **implementing** skill takes over. There is **no required intermediate step** (no separate planning doc or skill) — capable agents plan and break work internally from the approved spec. + +## Sections + +A specification always contains all three sections: + +| # | Section | Purpose | Depends on | +|---|---------|---------|------------| +| 1 | **Business Context** | Problem, goals, requirements, acceptance criteria, key scenarios | — | +| 2 | **Arch Decisions** | Technical approach, plan & key architecture decisions | Business Context | +| 3 | **Technical Contract** | Interfaces, models, boundaries | Business Context + Arch Decisions | + +## Workflow + +### Phase 1: Identify feature + +Determine what feature the specification is for (to derive the filename). If the user's message already names the feature, move on. Otherwise, ask. + +### Phase 2: Repository context + +If the user did **not** mention which repository (or repositories) the feature relates to, **ask before proceeding**. Use the AskQuestion tool so the user can type or select the repo(s). + +Once you know the repositories, use the **GitHub** tool to fetch context and understand what already exists: + +- Repository metadata (description, language, topics) +- Languages and tech stack breakdown +- README contents (project overview) +- Dependency files (`package.json`, `requirements.txt`, `go.mod`, etc.) +- Existing specs in the `specs/` directory (to avoid duplication) + +Use the gathered context to build a **repository profile** containing: +- Primary language and tech stack +- Project purpose and domain +- Existing patterns, frameworks, and conventions +- Known dependencies and integration points +- Existing specifications (to avoid duplication) + +This profile is used in Phase 2.5 to decide whether the agent can generate the spec directly or needs to ask more questions. + +### Phase 2.5: Assess completeness + +Evaluate whether the user's initial message **plus** the repository profile already provide enough information to generate all three sections. Check each section against its core questions: + +| Section | Can generate if you know… | +|---|---| +| **Business Context** | The problem, who it affects, expected outcome, acceptance criteria per story, and key scenarios | +| **Arch Decisions** | The proposed technical approach, its trade-offs, and key architecture decisions | +| **Technical Contract** | The interfaces, models, or boundaries involved | + +**Decision rules:** + +- **All sections covered** → skip Phase 3 entirely, go to Phase 4 (single-pass) +- **Some gaps** → ask only about the gaps (targeted discovery) +- **Mostly unknown** → run full Phase 3 (interactive) + +When in doubt, prefer asking over assuming — a wrong spec is worse than a slow one. + +### Phase 3: Discovery (may be skipped) + +> **Skip this phase** if Phase 2.5 determined all sections can be generated (single-pass). + +Gather information through conversation. Use the repository profile from Phase 2 and the gap analysis from Phase 2.5 to guide which questions to ask. + +**Rules:** +- **Only ask about gaps identified in Phase 2.5** — don't re-cover what's already known +- **Skip questions that the repo context already answers** (e.g., don't ask about tech stack if `package.json` reveals it) +- **Pre-fill what you can infer** and confirm with the user instead of asking from scratch (e.g., "Based on the repo, this is a Node.js service using Express — is that correct?") +- **Only ask about what's genuinely unknown** — focus on intent, business rules, and decisions that code alone can't reveal + +**Business Context questions — Problem & Requirements** +- What problem are we solving? +- Who is affected and how? +- What happens if we don't solve it? +- What are the expected outcomes? +- What are the functional and non-functional requirements? +- Are there constraints or dependencies? +- For each user story: what are the acceptance criteria? (use given/when/then format) +- What are the key scenarios — happy path, error cases, and edge cases? What pre-conditions, steps, and expected result define each? + +**Arch Decisions questions — Technical Approach & Decisions** +- What is the proposed solution? +- What alternatives were considered and why were they rejected? +- What are the risks and how do we mitigate them? +- What key architectural or design decisions need to be made? +- For each decision: what is the context, the options, and the chosen approach? +- What is the implementation plan? + +**Technical Contract questions — Interfaces & Boundaries** +- What interfaces, data models, or system boundaries does this feature define? +- What are the inputs and outputs? +- What are the integration points with other systems or modules? + +### Phase 4: Writing + +Create the file at `specs/.md` using the template in [references/template.md](references/template.md). + +Rules: +- Use kebab-case for the filename (e.g., `specs/user-authentication.md`) +- Create the `specs/` directory at the project root if it doesn't exist +- Fill every section — no placeholders or TODOs +- Every user story must have acceptance criteria in given/when/then format +- Key Scenarios table must include at least one happy path, one error case, and one edge case +- Keep language direct and concise +- Use diagrams (mermaid) when they clarify flow or architecture + +### Phase 5: Review & Deliver + +After writing, present a summary of what was generated and ask if any section needs refinement. + +Once the user is satisfied, open a Pull Request with **only** the spec file: + +1. Create branch `spec/` from the base branch +2. Stage only `specs/.md` — no other files +3. Verify with `git status` that nothing else is staged before committing +4. Commit with message `spec: ` +5. Push and create the PR: + - Title: `spec: ` + - Body: summary of the spec contents + +## Lifecycle + +| Status | Meaning | Trigger | +|---|---|---| +| `Draft` | Written, awaiting review | Spec generated | +| `Approved` | Reviewed and accepted for implementation | User approves | +| `Done` | Fully implemented | Implementation complete | + +Update the status in the document header as the specification progresses. + +Once a spec is `Approved`, it is implemented using the **implementing** skill only — that skill loads the spec, reconnoiters the repo, runs a test-first implementation loop, verifies, and delivers (PR + status `Done`). No extra handoff artifact is required between specification and implementation. + +## Important + +- Never generate a section without having **sufficient information** for it — whether provided in the user's initial message, inferred from the repository context, or gathered through discovery questions +- Ask clarifying questions when answers are vague +- Adapt the number of Key Decisions to what the feature actually requires — don't force unnecessary decisions +- The Technical Contract section should be technology-agnostic unless the user specifies a stack diff --git a/.agents/skills/specification/references/template.md b/.agents/skills/specification/references/template.md new file mode 100644 index 00000000..22f4ad35 --- /dev/null +++ b/.agents/skills/specification/references/template.md @@ -0,0 +1,106 @@ +# {Feature Name} + +> **Status**: {Draft | Approved | Done} +> **Created**: {date} + +## 1. Business Context + +### Problem Statement + +{Why this feature exists. What problem it solves and for whom.} + +### Goals + +{Measurable outcomes this feature should achieve.} + +### User Stories + +{Key user stories in "As a [role], I want [action], so that [benefit]" format. Each story includes acceptance criteria.} + +#### US-1: {Story Title} + +- **Story**: As a {role}, I want {action}, so that {benefit}. +- **Acceptance Criteria**: + - **Given** {pre-condition}, **when** {action}, **then** {expected result}. + - **Given** {pre-condition}, **when** {action}, **then** {expected result}. + +{Repeat for each user story.} + +### Key Scenarios + +| Scenario | Pre-conditions | Steps | Expected Result | +|---|---|---|---| +| {Happy path} | {Initial state} | {Action sequence} | {Success outcome} | +| {Error case} | {Initial state} | {Action that fails} | {Error handling behavior} | +| {Edge case} | {Boundary state} | {Action sequence} | {Expected behavior} | + +### Functional Requirements + +{What the system must do.} + +### Non-Functional Requirements + +{Performance, security, scalability, accessibility constraints.} + +### Out of Scope + +{What this feature explicitly does NOT cover.} + +--- + +## 2. Arch Decisions + +### Proposed Solution + +{High-level description of the technical approach.} + +### Architecture Overview + +{System architecture, component interactions. Use mermaid diagrams when helpful.} + +### Alternatives Considered + +| Alternative | Pros | Cons | Verdict | +|---|---|---|---| +| {Option} | {Pros} | {Cons} | {Why accepted/rejected} | + +### Risks & Mitigations + +| Risk | Impact | Likelihood | Mitigation | +|---|---|---|---| +| {Risk} | {High/Med/Low} | {High/Med/Low} | {Strategy} | + +### Key Decisions + +#### Decision 1: {Decision Title} + +- **Status**: Accepted +- **Context**: {Why this decision is needed} +- **Decision**: {What was decided} +- **Consequences**: {Trade-offs and implications} + +{Repeat for each significant decision.} + +### Implementation Plan + +{Phased rollout, milestones, or sequencing of work.} + +--- + +## 3. Technical Contract + +### Data Models + +{Core entities, their attributes, and relationships.} + +### Interfaces + +{Public APIs, function signatures, event contracts, or module boundaries this feature exposes or consumes.} + +### Integration Points + +{How this feature connects with other systems, services, or modules.} + +### Invariants & Constraints + +{Rules that must always hold true in this system.} diff --git a/.claude/commands b/.claude/commands new file mode 120000 index 00000000..1ea3574a --- /dev/null +++ b/.claude/commands @@ -0,0 +1 @@ +../.agents/commands \ No newline at end of file diff --git a/.claude/rules b/.claude/rules new file mode 120000 index 00000000..2d5c9a97 --- /dev/null +++ b/.claude/rules @@ -0,0 +1 @@ +../.agents/rules \ No newline at end of file diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 00000000..2b7a412b --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..c68d096d --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,23 @@ +FROM node:16-bullseye-slim + +# System dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install VTEX Toolbelt +RUN npm install -g vtex@3 + +# Install uv (for Spec Kit / SDD Full) +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:$PATH" + +# Install Spec Kit (specify-cli) via uv +RUN uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@v0.6.0 + +# Verify +RUN node --version && yarn --version && vtex --version && specify version + +WORKDIR /workspace diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000..84a5821c --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,59 @@ +# Dev container for vtex/address-form + +Standardized, containerized environment so AI agents and engineers run address-form the same way. + +## What's inside + +- Node 16 (matches the project's React 16 + Jest 26 stack) +- Yarn (classic v1) +- VTEX Toolbelt (`vtex@3.x`) +- `uv` + Spec Kit (`specify-cli @ v0.6.0`) for **SDD Full** +- VS Code extensions: ESLint, Prettier, EditorConfig, Claude Code, Copilot, Cursor + +## Bring your VTEX session + +The container bind-mounts `~/.vtex` from your host so `vtex login`, `vtex whoami`, and authenticated `vtex link` calls work immediately. If you don't want this, drop the `mounts` entry from `devcontainer.json`. + +Set these in your shell or in `.env`: + +``` +VTEX_ACCOUNT= +VTEX_WORKSPACE= +``` + +## Usage + +In VS Code or Cursor: **Reopen in Container**. In Codespaces: pick this devcontainer when creating the codespace. After build, `yarn install` (root) and `yarn --cwd react install` run automatically. + +## Dual install + +This repo has two `package.json` files: +- Root `package.json` — lint, format, lint:locales tooling +- `react/package.json` — Jest, Rollup, and all library dependencies + +Both are installed automatically by the `postCreateCommand`. If you need to add a dependency to the library, run `yarn --cwd react add `. + +## Running tests after container start + +```bash +# Unit tests +yarn --cwd react test + +# i18n parity +yarn lint:locales + +# Build the npm bundle +yarn --cwd react build + +# Demo app (local development) +cd demo && yarn install && yarn start +``` + +## Why Node 16 + +Matches the project's React 16 dependency chain and VTEX Toolbelt compatibility requirements. When the team migrates, bump the base image here in lockstep with `react/package.json` peerDep updates. + +## Reference templates + +- [vtex/ai-agents@development](https://github.com/vtex/ai-agents/tree/development) — robust example with AWS SSO support +- [vtex/payments-support](https://github.com/vtex/payments-support) — pre-built image example diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..31dca24b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,34 @@ +{ + "name": "vtex/address-form dev container", + "build": { + "dockerfile": "Dockerfile" + }, + "mounts": [ + "source=${localEnv:HOME}/.vtex,target=/root/.vtex,type=bind,consistency=cached" + ], + "postCreateCommand": "yarn install --frozen-lockfile && yarn --cwd react install --frozen-lockfile", + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "editorconfig.editorconfig", + "anthropic.claude-code", + "github.copilot", + "anysphere.cursorless" + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "typescript.tsdk": "react/node_modules/typescript/lib" + } + } + }, + "remoteEnv": { + "VTEX_ACCOUNT": "${localEnv:VTEX_ACCOUNT}", + "VTEX_WORKSPACE": "${localEnv:VTEX_WORKSPACE}" + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4039ff11 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 57a27b9a..3549bbf8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,14 @@ yarn-debug.log* yarn-error.log* package-lock.json + +# Spec Kit (SDD Full) — ephemeral artifacts. +# spec.md is the only committed artifact under specs//. +specs/**/plan.md +specs/**/tasks.md +specs/**/analysis.md +.specify/cache/ +.specify/.tmp/ + +# Claude Code internal worktrees (per-engineer, never committed) +.claude/worktrees/ diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md new file mode 100644 index 00000000..500e2cc3 --- /dev/null +++ b/.specify/memory/constitution.md @@ -0,0 +1,189 @@ +# Constitution — vtex/address-form + +> This document is the **architectural contract** for `vtex/address-form`. Spec Kit reads it before every plan, analyze, and implement step. Edit it manually only — `/speckit.constitution` is for regeneration suggestions, not the primary mechanism. If a proposed change violates a principle here, the spec must explicitly justify and supersede it. + +--- + +## I. Identity and Role + +`vtex/address-form` is the **reusable address input UI library for VTEX**. Its purpose is to abstract the complexity of country-specific address fields, postal code autocomplete strategies, and geolocation-based address resolution into a composable React component API. + +It has a **dual nature** that must be preserved in all decisions: + +1. **VTEX IO app** (`vtex.address-form`) — deployed on the VTEX IO platform, consumed as a React peer dependency by IO apps. +2. **npm package** (`@vtex/address-form`) — published from `react/lib/` via Rollup, consumed by non-IO JavaScript projects. + +Both surfaces are production-critical. A bug in either affects checkout for VTEX merchants. A breaking change in either requires a major version bump and coordinated consumer updates. + +--- + +## II. Stack Constraints + +These are non-negotiable. No spec may propose changing them without an accompanying RFC. + +- **Platform**: VTEX IO `react@2.x` + `messages@0.x` builders. No `node` builder. +- **Language**: TypeScript 3.9 for new code. JavaScript for existing legacy files. Do not migrate past TS 3.9 without an RFC that covers both root `tsconfig.json` and `react/tsconfig.json`. +- **React**: 16.x (`peerDep: 15.x || 16.x`). Do not upgrade without an RFC. +- **State management**: React Context only (`RulesContext`, `AddressContext`). No Redux, no MobX, no Zustand. +- **Build (npm)**: Rollup 3 → `react/lib/` (CJS). No webpack, no esbuild at the library level. +- **Tests**: Jest 26 + `@testing-library/react` 12 (standard). Enzyme 3 (legacy only — do not extend). +- **Linting**: ESLint + `eslint-config-vtex` + `eslint-config-vtex-react`. No rule disabling without inline comment + TODO. +- **i18n**: `@vtex/intl-equalizer` + Crowdin. Reference locale: `pt`. Crowdin format: `%two_letters_code%`. +- **Observability**: `window.logSplunk` (NOT `@vtex/evidence-client-js`). +- **Package manager**: `yarn` classic v1. No npm, no pnpm. +- **Node in dev container**: 16. + +--- + +## III. Architecture Invariants + +### A. Two-Context Model + +The library is built on two composable React Contexts. This model must not be collapsed or replaced: + +1. **`RulesContext`** (from `addressRulesContext.tsx`): provides the loaded `PostalCodeRules` for the current country. Set only by ``. Available to any child via `useAddressRules()` or `injectRules()`. +2. **`AddressContext`** (from `addressContainerContext.tsx`): provides `{ address, handleAddressChange, Input }`. Set only by ``. + +Components that need rules or address state must consume these contexts — not receive Redux state, not lift state up, not use global variables. + +### B. Country Rules as Static Modules + +Per-country rules live in `react/country/.ts`. Each file exports a `PostalCodeRules` object. `` loads them via dynamic `import()`. The fallback is `react/country/default.ts`. + +- Adding a new country = new file under `react/country/`, never modifying `AddressRules.tsx` logic. +- Country rule files are pure data + minimal formatting logic. No API calls, no side effects. +- The `PostalCodeRules` type shape is a contract shared with consumers — changes require a major version bump. + +### C. postalCodeFrom Strategies + +There are exactly four postal code resolution strategies encoded as constants: +- `POSTAL_CODE` — single text field with API autocomplete +- `ONE_LEVEL` — one select dropdown +- `TWO_LEVELS` — two cascading select dropdowns +- `THREE_LEVELS` — three cascading select dropdowns + +New strategies require a spec. Do not add a new `postalCodeFrom` value without also adding a corresponding component under `react/postalCodeFrom/`. + +### D. Postal Code Autocomplete + +The flow: user enters a valid postal code → `AddressContainer.handleAddressChange` calls `postalCodeAutoCompleteAddress` → which calls `postalCodeService.getAddress` → VTEX public API `/api/checkout/pub/postal-code//` → fills remaining address fields. + +This call must always go through `postalCodeService.js`. No component should call the API directly. + +### E. Geolocation + +Google Maps integration lives entirely under `react/geolocation/`. The entry point is `GoogleMapsContainer`, which loads the Maps SDK and provides `googleMaps` to children. `Map` renders the map. `GeolocationInput` provides the autocomplete search input. `getAddressByGeolocation` (in `Utils.js`) resolves a structured address from geo coordinates. + +Geolocation features are opt-in via `useGeolocation` prop on ``. + +### F. Public API Surfaces + +**Never modify without a major version bump + consumer coordination:** + +1. `react/index.ts` — npm package named exports (components, functions, types) +2. `react/components.ts` — VTEX IO component map (default export object) +3. `react/types/rules.ts` — `PostalCodeRules` interface and related types +4. `react/types/address.ts` — `Address`, `AddressWithValidation`, `ValidatedField` types + +Adding new exports to surfaces 1 or 2 is safe (minor). Removing, renaming, or changing the shape is breaking (major). + +--- + +## IV. Code Style + +- 2 spaces, LF, UTF-8. +- New files: TypeScript (`.ts` / `.tsx`). Existing JS files remain JS unless the PR's scope includes migration. +- No inline `any` without a `// eslint-disable-next-line` comment explaining why. +- No default export of anonymous functions or anonymous classes — always name them. +- PropTypes are maintained on legacy JS components for runtime type checking. TypeScript components use TS types only. + +--- + +## V. Testing + +- **Test runner**: `yarn --cwd react test` (Jest 26, config in `react/package.json`). +- **New country files**: require a unit test asserting the `PostalCodeRules` shape. +- **New selectors/transforms**: require unit tests covering all logic branches. +- **New validation paths**: require tests for valid, invalid, and edge cases. +- **Bug fixes**: ship with a failing regression test before the fix. +- **Snapshot tests**: allowed only for stable outputs. Never `--updateSnapshot` to clear noise. +- **Test co-location**: `Foo.test.js` next to `Foo.js`, or `__tests__/Foo.test.tsx` for `.tsx` files. +- **Country mock fixtures**: use `react/country/__mocks__/` before inlining test data. +- **Address mock fixtures**: use `react/__mocks__/` before inlining test data. + +--- + +## VI. Performance + +- Country rule files are loaded via dynamic `import()` and are not bundled into the main chunk. +- The demo app (`demo/`) is CRA-based and separate from the library bundle — do not couple them. +- `react/lib/` (the npm bundle) should have no unnecessary dependencies. All VTEX IO peer deps must remain `peerDependencies`, not `dependencies`, in `react/package.json`. + +--- + +## VII. Versioning + +- `manifest.json` and `react/package.json` versions **must always be in lockstep**. The release script (`publish-release.sh`) handles both atomically. +- Major bump: any breaking change to public API surfaces (I.F above). +- Minor bump: new exports, new country files, new non-breaking features. +- Patch bump: bug fixes, performance improvements. +- Conventional Commits required. `BREAKING CHANGE:` footer mandatory for majors. +- `CHANGELOG.md` updated under `## [Unreleased]` in every PR. + +--- + +## VIII. i18n Governance + +- `messages/en.json` is the only file agents and engineers edit. +- Reference locale for parity: `pt`. Run `yarn lint:locales` after any `en.json` change. +- Crowdin format: `%two_letters_code%` (e.g., `pt`, `es`, `de`). +- Key naming: `.` or `.`. +- Removing or renaming a key = breaking change (CHANGELOG + `BREAKING CHANGE:` footer). +- `messages/context.json` provides Crowdin with context — keep it in sync when adding keys. + +--- + +## IX. Telemetry + +- `react/metrics.ts` contains `window.logSplunk` calls for Splunk telemetry. +- Currently tracks: `logGeolocationAddressMismatch` — fired when a geolocation-resolved field value doesn't match the rules expectation. +- Do not remove existing telemetry calls without opening a follow-up task. +- New critical geolocation or validation flows should consider adding telemetry. + +--- + +## X. Security + +- No private VTEX API keys in the browser. +- Postal code API calls go to the public endpoint `/api/checkout/pub/postal-code/` only. +- The `cors` prop on `AddressContainer` enables cross-origin postal code calls — it uses the account's `vtexcommercestable.com.br` domain, which is safe for production accounts. +- Never expose merchant credentials through the component API. + +--- + +## XI. Dependencies + +- No new `dependencies` in `react/package.json` without a spec justification and CHANGELOG entry. +- Rollup and build tooling belong in `devDependencies`. +- React, react-intl, prop-types, vtex-tachyons remain `peerDependencies`. +- `axios` is a runtime dependency (postal code API calls) — do not replace without migration plan. +- Do not upgrade react-intl peerDep constraint without verifying all consumer versions. + +--- + +## XII. Specification-Driven Development + +All non-trivial work starts from a written spec. The hierarchy: + +- **SDD Lite** — for tasks < 5 days, single-repo, no public API changes. Uses `/specification` + `/implementing` skills. +- **SDD Full** — for tasks > 5 days, cross-repo, or any change to public API surfaces. Uses Spec Kit (specify-cli v0.6.0). + +Agents must not implement features not described in an approved spec. When in doubt, write the spec first. + +--- + +## XIII. Governance + +- This constitution is edited manually by the team. It is **not** auto-generated. +- If a proposed change violates a principle in this document, the spec must explicitly state the violation, justify the exception, and get team sign-off before proceeding. +- Agents that encounter an irreconcilable conflict between this constitution and a spec must surface the conflict rather than silently resolve it. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..006f6a19 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,271 @@ +# AGENTS.md — vtex/address-form + +> **Read this first.** This file is the canonical context for AI agents (Cursor, Copilot, Claude Code, etc.) working on this repository. It is mirrored to `CLAUDE.md` via a symlink for Claude Code compatibility. + +## What this repo is + +`vtex/address-form` is the **reusable address input UI library for VTEX**. It owns the complete address entry experience: country-aware field rendering, postal code autocomplete, geolocation-based address resolution, field validation, and a composable component API consumed by checkout and storefront apps. + +It has a **dual nature**: + +- **VTEX IO app** (`react@2.x` + `messages@0.x` builders) — consumed as `vtex.address-form` peer dep inside the VTEX IO platform +- **npm package** (`@vtex/address-form`) — published from `react/lib/` via Rollup, consumed by non-IO apps and the `demo/` standalone app + +It is consumed by: + +- `vtex.omnishipping` — full checkout shipping UI (address step) +- `vtex.shipping-preview` — cart-page shipping preview (postal code entry) +- `vtex.checkout` — main checkout app +- Any VTEX storefront app that needs address input + +Any change to the exported component API or the country rules shape is a **breaking change for all consumers**. + +## Tech stack + +| Layer | Tooling | +|---|---| +| Platform | VTEX IO (`react@2.x`, `messages@0.x` builders) | +| Language | TypeScript 3.9 + JavaScript (mixed — new code in TS, legacy in JS) | +| UI | React 16.x (peerDep: `15.x \|\| 16.x`) | +| State | React Context only (`RulesContext`, `AddressContext`) — no Redux | +| GraphQL | None | +| Bundle (npm) | Rollup 3 → `react/lib/` (CJS) + locales copy | +| Tests | Jest 26 + `@testing-library/react` 12 + Enzyme 3 | +| Lint | ESLint + `eslint-config-vtex` + `eslint-config-vtex-react` | +| Format | Prettier + `@vtex/prettier-config` | +| i18n | `@vtex/intl-equalizer` over `messages/.json`; source locale is `en.json`; reference locale for parity checks is `pt`; translations synced via Crowdin (`%two_letters_code%`) | +| Observability | `window.logSplunk` (Splunk, via `react/metrics.ts`) | +| Node (dev container) | 16 | + +Stack constraints encoded as agent rules: see `.agents/rules/00-vtex-address-form.md`. + +## Layout + +``` +. +├── .agents/ # Source of truth for agent skills/commands/rules +│ ├── skills/{specification,implementing}/SKILL.md +│ ├── commands/{sdd-full-bootstrap,sdd-lite-bootstrap}.md +│ └── rules/*.md +├── .claude/ # Claude Code-specific (symlinks back into .agents/) +│ ├── skills -> ../.agents/skills +│ ├── commands -> ../.agents/commands +│ └── rules -> ../.agents/rules +├── .devcontainer/ # Node 16 + VTEX Toolbelt + uv/spec-kit +├── .specify/memory/constitution.md # SDD Full constitution (architecture contract) +├── docs/ # Long-form docs, including scope_of_work/ +│ └── scope_of_work/ # Per-task scoped contexts for /speckit.specify +├── specs/ # SDD specs (Lite: .md ; Full: /spec.md) +├── react/ # App + npm package source +│ ├── country/ # Per-country postal code rule files (55+ countries, dynamic import) +│ │ ├── BRA.ts, ARG.ts … # Each exports a PostalCodeRules object +│ │ ├── default.ts # Fallback rules when country not found +│ │ └── __mocks__/ # Mock rule fixtures for tests +│ ├── country/data/ # JSON datasets for hierarchical postal code lookups +│ ├── geolocation/ # Google Maps integration (GoogleMapsContainer, Map, GeolocationInput, Utils) +│ ├── inputs/ # Input components: DefaultInput, StyleguideInput, StyleguideButton +│ ├── postalCodeFrom/ # Postal code entry modes: OneLevel, TwoLevels, ThreeLevels, SelectPostalCode +│ ├── propTypes/ # Legacy PropTypes shapes (AddressShape, AddressShapeWithValidation, CountryType) +│ ├── selectors/ # Pure selector functions (fields, hasOption, postalCode, cleanStr) +│ ├── transforms/ # Pure transform functions (address, addressFieldsOptions, postalCodes) +│ ├── types/ # TypeScript types (address.ts, rules.ts) +│ ├── intl/ # react-intl compatibility helpers +│ ├── __mocks__/ # Address fixture mocks +│ ├── __tests__/ # Colocated tests for tsx files (AddressRules, InputFieldContainer) +│ ├── AddressContainer.js # Context provider: validates fields, triggers postal code autocomplete +│ ├── AddressForm.js # Renders all address fields per country rules +│ ├── AddressRules.tsx # Dynamically loads country rules, provides RulesContext +│ ├── AddressSubmitter.js # Validates on submit, calls back with valid/invalid result +│ ├── AddressSummary.js # Read-only formatted address display +│ ├── AutoCompletedFields.js # Displays fields filled by postal code autocomplete +│ ├── CountrySelector.js # Country picker dropdown +│ ├── PostalCodeGetter.js # Postal code entry with inline validation +│ ├── addressContainerContext.tsx # AddressContext (address state + handleAddressChange) +│ ├── addressRulesContext.tsx # RulesContext + injectRules HOC + useAddressRules hook +│ ├── validateAddress.ts # isValidAddress, validateField, validateAddress, validateChangedFields +│ ├── postalCodeService.js # Calls /api/checkout/pub/postal-code/ via axios +│ ├── postalCodeAutoCompleteAddress.js # Orchestrates postal code API → address fill +│ ├── geolocationAutoCompleteAddress.js # Google Maps geocoder → address fill +│ ├── metrics.ts # window.logSplunk telemetry (geolocation mismatch events) +│ ├── constants.ts # POSTAL_CODE, ONE_LEVEL, TWO_LEVELS, THREE_LEVELS, error codes +│ ├── helpers.ts # Named helper exports (addValidation, removeValidation, etc.) +│ ├── index.ts # npm package public API (named exports for all components + helpers + types) +│ ├── components.ts # VTEX IO public component map (default export object) +│ ├── inputs.ts # Input registry (DefaultInput, StyleguideInput, GeolocationInput, …) +│ ├── shapes.ts # Exported TypeScript shapes +│ ├── countries.ts # Country list for CountrySelector +│ ├── rollup.config.mjs # npm bundle build (→ react/lib/) +│ ├── package.json # npm package metadata + Jest config +│ └── setupTests.js # Jest global setup (Enzyme adapter) +├── demo/ # Standalone CRA-style app for local development/testing +├── jscodeshift/ # Codemod scripts (e.g., upgrade-react-intl migration) +├── messages/ # i18n source files (en.json is the source of truth) +├── crowdin.yml # Crowdin sync config (uses %two_letters_code%) +├── manifest.json # VTEX IO app manifest (vendor/name/version) +├── package.json # Root tooling (lint/format/lint:locales) +├── CHANGELOG.md # Keep a Changelog format — required updates per PR +├── AGENTS.md # This file +└── CLAUDE.md -> AGENTS.md # Symlink for Claude Code +``` + +## Day-to-day commands + +```bash +# Install (root tooling) +yarn install --frozen-lockfile + +# Install react/ dependencies +yarn --cwd react install --frozen-lockfile + +# Lint (only touch changed files — never run globally in a feature PR) +yarn lint + +# Format +yarn format + +# Run unit tests (Jest — run from react/ subdirectory) +yarn --cwd react test + +# Run unit tests in watch mode +yarn --cwd react test:watch + +# Run unit tests with coverage +yarn test:coverage # (root script that installs + runs coverage) + +# Check i18n key parity across all locales +yarn lint:locales + +# Build the npm bundle (react/lib/) +yarn --cwd react build + +# Start the demo app +cd demo && yarn install && yarn start + +# Link the app into a VTEX workspace for live testing +vtex use $VTEX_WORKSPACE +vtex link +``` + +`pre-commit` runs `lint-staged` (eslint + prettier on changed files). +`pre-push` runs `yarn lint:locales && yarn --cwd react test`. + +## How agents should approach work here + +We follow the [**VTEX Engineering Golden Path**](https://docs.google.com/document/d/1e7waGGK-7FE4nNmO7DVjbjDCoULwsPmLlpIh3g2jaoI/edit?tab=t.cobcxrp8wpxu#heading=h.w21olzdtavkg), with Specification-Driven Development as the foundation. Every non-trivial change starts from a written spec — code is the generated artifact, not the starting point. + +### Decide: SDD Full or SDD Lite? + +| Use **SDD Lite** when… | Use **SDD Full** (Spec Kit) when… | +|---|---| +| The task fits in <5 days | The task is >5 days | +| Single-repo, contained scope | Cross-team or cross-repo | +| Bug fixes, small features, focused refactors | Significant architectural impact | +| Low ambiguity | High ambiguity, unresolved product decisions | +| No cross-consumer coordination needed | Adding/removing/changing a public export (breaking change to `index.ts` or `components.ts`) | +| New country rule file only | Changes to the `PostalCodeRules` type shape | + +When in doubt, run `/sdd-lite-bootstrap` (slash command in `.agents/commands/`) to see the full Lite flow, or `/sdd-full-bootstrap` for Spec Kit. + +### SDD Lite (default for most work) + +``` +/specification "" + → opens PR on branch spec/, containing only specs/.md + → Status: Draft +[engineer reviews the spec PR, asks PM, edits, manually flips Status: Approved, merges] +/implementing "specs/.md" + → non-interactive sandbox run + → branch feat/, failing tests, minimal code, PR with named sections + → on success: Status: Done in the same PR + → on block: opens GitHub issue "implementing blocked: " and ends +``` + +The skills live at `.agents/skills/specification/SKILL.md` (+ `references/template.md`) and `.agents/skills/implementing/SKILL.md`. They are **vendored verbatim** from [`vtex/vtex-agent-skills`](https://github.com/vtex/vtex-agent-skills). To refresh: `npx skills add vtex/vtex-agent-skills` or `git clone` and copy. Do not hand-edit the SKILL.md files — repo-specific guidance goes in `.agents/rules/`. + +### SDD Full (Spec Kit) + +Setup once on your machine: + +```bash +# Install uv (one of): +curl -LsSf https://astral.sh/uv/install.sh | sh +# brew install uv + +source "$HOME/.local/bin/env" +uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@v0.6.0 +specify version +``` + +Per-task workflow: + +``` +1. Drop the relevant PRD/RFC sections into docs/scope_of_work/.md +2. /speckit.specify → specs//spec.md (commit) +3. /speckit.clarify → resolve ambiguity, update spec +4. /speckit.plan → specs//plan.md (DO NOT commit) +5. /speckit.tasks → specs//tasks.md (DO NOT commit) +6. /speckit.analyze → specs//analysis.md (DO NOT commit) +7. /speckit.implement phase 1 only → code + branch (commit code only) +``` + +Ephemeral artifacts (`plan.md`, `tasks.md`, `analysis.md`) are gitignored. + +The **Constitution** (`.specify/memory/constitution.md`) is the architectural contract. Spec Kit reads it before every plan/analyze step. **Edit it manually** — `/speckit.constitution` is for regeneration suggestions, not the primary mechanism. + +## Hierarchy of authority for agents + +1. `.specify/memory/constitution.md` — non-negotiable principles. (SDD Full only.) +2. `AGENTS.md` (this file) + `.agents/rules/*.md` — repo-level adaptations. +3. `.agents/skills/*/SKILL.md` — step-by-step skill instructions, bounded by 1 and 2. + +If two layers conflict, the higher layer wins. If you cannot reconcile them, stop and surface the conflict. + +## Sensitive areas — extra caution required + +- **`react/index.ts`** — the npm package's public API. Every named export is consumed by external packages. Removing, renaming, or changing the signature of any export is a breaking change requiring a major version bump in both `manifest.json` and `react/package.json`. +- **`react/components.ts`** — the VTEX IO public component map. Removing or renaming a key in the default export object is a breaking change for every IO consumer. +- **`react/validateAddress.ts`** — field and address validation logic. Behavior changes here affect what shoppers see in real-time validation across all checkout flows. +- **`react/country/*.ts`** — per-country rules define postal code formats, field ordering, masking, and API integration. Incorrect rules ship incorrect checkout UX to an entire market. +- **`react/types/rules.ts`** — the `PostalCodeRules` interface. Any shape change requires coordinating all consumers (omnishipping, shipping-preview) and bumping the major version. +- **`react/postalCodeService.js`** — calls the VTEX public postal code API. Changes here affect address autocomplete for all supported countries. +- **`react/geolocation/`** — Google Maps integration. Test both happy path (address found) and fallback (Maps unavailable, address not found). +- **`react/metrics.ts`** (`window.logSplunk` calls) — Splunk telemetry for geolocation mismatch monitoring. Do not remove existing calls without a follow-up telemetry task. +- **`messages/en.json`** — the Crowdin source file. Adding a key triggers translation work across 25+ locales; removing a key is a breaking change. + +## Versioning, CHANGELOG, releases + +- SemVer. **`manifest.json` and `react/package.json` versions must move in lockstep.** The release script (`publish-release.sh`) handles the bump — do not edit manually. +- Every PR updates `CHANGELOG.md` (Keep a Changelog format) under `## [Unreleased]`. +- Conventional Commits required. Breaking changes use `feat!:` / `fix!:` plus a `BREAKING CHANGE:` footer. +- Release: `publish-release.sh` bumps both versions, then `vtex publish` + `npm publish`. + +## Branching & PRs + +- Spec PRs (from `/specification`): branch `spec/`. Diff contains only the spec file. +- Implementation PRs (from `/implementing` or by hand): branch `feat/` — upstream convention is always `feat/`, even for fixes. Use `feat!:` in the title when the change is breaking. +- Never push to `main` directly. +- PR title is a Conventional Commit. Implementation PR body uses the upstream named-section format: **Summary / Tests / Assumptions / Deviations / Follow-ups / Spec**. + +## What NOT to do + +- Don't add a `node/` builder — this app has no backend. +- Don't add Redux or any other state library — the app is Context-only by design. +- Don't switch package managers (yarn only). +- Don't migrate TypeScript past 3.9 without an RFC (both root and `react/` tsconfigs must align). +- Don't disable ESLint rules without an inline justification + TODO. +- Don't reformat unrelated files in a feature PR. +- Don't commit `.env`, real account credentials, or session cookies. +- Don't add or remove `messages/en.json` keys without a CHANGELOG entry and explicit team awareness of the Crowdin impact. +- Don't remove or rename exports from `react/index.ts` or `react/components.ts` without a major version bump and consumer coordination. +- Don't remove `window.logSplunk` telemetry calls in `react/metrics.ts` without a follow-up task. +- Don't edit `manifest.json` or `react/package.json` version manually — use `publish-release.sh`. +- Don't add new country files to `react/country/` without both a unit test and a manual QA pass in the demo app. + +## Pointers + +- VTEX IO docs: https://developers.vtex.com/docs/guides/vtex-io-documentation +- Spec Kit: https://github.com/github/spec-kit +- Multi-repo Spec Kit extension: https://github.com/vtex/speckit-multi-repo +- VTEX Agent Skills (upstream of `.agents/skills/`): https://github.com/vtex/vtex-agent-skills — install via `npx skills add vtex/vtex-agent-skills` +- Consumers: [`vtex/omnishipping`](https://github.com/vtex/omnishipping), [`vtex/shipping-preview`](https://github.com/vtex/shipping-preview) — their `AGENTS.md` files document the integration contract +- Golden Path SDLC how-to: see your team's internal handbook diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..2143e700 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,16 @@ +# docs/ — vtex/address-form + +Long-form documentation and per-task scoped contexts for Spec Kit. + +## Contents + +- `scope_of_work/` — Input documents for `/speckit.specify`. Drop PRD sections, RFC excerpts, and feature briefs here before running Spec Kit. + +## What goes here vs. specs/ + +| `docs/` | `specs/` | +|---|---| +| PRDs, RFCs, raw briefs | Final SDD specs | +| Scope-of-work inputs | Approved and Done specs | +| Architecture notes | Technical contracts | +| Never committed to PRs | Always committed | diff --git a/docs/scope_of_work/README.md b/docs/scope_of_work/README.md new file mode 100644 index 00000000..bddcef78 --- /dev/null +++ b/docs/scope_of_work/README.md @@ -0,0 +1,41 @@ +# scope_of_work/ — vtex/address-form + +Input documents for `/speckit.specify`. Place PRD sections, RFC excerpts, or feature briefs here before running Spec Kit. + +## Template + +Create a file named `docs/scope_of_work/.md` with: + +```markdown +# Scope of Work: + +## Problem + + + +## Goals + + + +## Requirements + + + +## Constraints + + + +## Out of scope + + + +## References + + +``` + +## Rules + +- Files here are **never committed** to PRs — they are working documents only. +- Once a spec is generated in `specs/`, the scope-of-work file can be archived or deleted. +- Keep files focused on one feature. Split large features into multiple files. diff --git a/react/package.json b/react/package.json index 4968e5d8..6c4f2acd 100644 --- a/react/package.json +++ b/react/package.json @@ -92,10 +92,10 @@ "/umd/" ], "transformIgnorePatterns": [ - "[/\\\\]lib[/\\\\].+\\.(js|jsx)$", - "[/\\\\]umd[/\\\\].+\\.(js|jsx)$", - "[/\\\\]es[/\\\\].+\\.(js|jsx)$", - "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$" + "/lib/.+\\.(js|jsx)$", + "/umd/.+\\.(js|jsx)$", + "/es/.+\\.(js|jsx)$", + "[/\\\\]node_modules[/\\\\](?!axios[/\\\\]).+\\.(js|jsx|mjs)$" ], "moduleNameMapper": { "\\.(css|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)(\\?.*)?$": "identity-obj-proxy" diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 00000000..542857cd --- /dev/null +++ b/specs/README.md @@ -0,0 +1,42 @@ +# specs/ — vtex/address-form + +This directory contains SDD (Specification-Driven Development) specifications for features in this repository. + +## Layout + +``` +specs/ +├── README.md # This file +├── app-baseline.md # Reverse-spec of the existing app (baseline reference) +└── .md # SDD Lite spec (one file per feature) + or +└── / + └── spec.md # SDD Full spec (directory per feature) +``` + +## Spec status lifecycle + +| Status | Meaning | Who sets it | +|---|---|---| +| `Draft` | Written, awaiting review | `/specification` skill | +| `Approved` | Reviewed and accepted for implementation | Engineer (manual) | +| `Done` | Fully implemented and merged | `/implementing` skill | + +## PR conventions + +- **Spec PR**: branch `spec/`, contains only `specs/.md`. Title: `spec: `. +- **Implementation PR**: branch `feat/`, contains code + test changes. Title: `feat: `. Links to the spec PR. + +## What gets committed + +Only `spec.md` (SDD Full) or `.md` (SDD Lite) are committed. The following are gitignored: + +``` +specs/**/plan.md +specs/**/tasks.md +specs/**/analysis.md +``` + +## Multi-repo note + +When a spec covers changes that affect `vtex.omnishipping` or `vtex.shipping-preview` (e.g., breaking API changes), a corresponding spec must exist in those repos. Use [vtex/speckit-multi-repo](https://github.com/vtex/speckit-multi-repo) to propagate context across repos. diff --git a/specs/app-baseline.md b/specs/app-baseline.md new file mode 100644 index 00000000..97c52395 --- /dev/null +++ b/specs/app-baseline.md @@ -0,0 +1,604 @@ +# address-form — App Baseline + +> **Status**: Draft +> **Created**: 2026-05-12 +> **Purpose**: Reverse-spec of the existing `vtex/address-form` application. Captures what the app currently does as a canonical reference for future specs, agent grounding, and onboarding. This spec is never "Approved" or "Done" — it is a living document updated when the app's architecture materially changes. + +--- + +## 1. Business Context + +### Problem Statement + +VTEX merchants operate across 55+ countries, each with distinct address formats: different fields (CEP vs ZIP vs postcode), different validation rules, different postal code autocomplete APIs, and different administrative hierarchy depths (city-only vs city+state vs city+district+state). Storefront apps that need address input (checkout, shipping preview, returns) should not each re-implement this complexity. + +`vtex/address-form` centralizes all country-specific address UI logic in a single composable library. Consumer apps mount `` + `` + `` and get a fully country-aware, validated address entry experience. + +### Goals + +1. Provide a correct, production-grade address input experience for every VTEX-supported country. +2. Enable consumer apps to customize input rendering (via the `Input` prop) without forking business logic. +3. Expose a stable, versioned public API so consumers don't break on library updates. +4. Support both postal code autocomplete (server-side lookup) and geolocation (Google Maps) as address resolution strategies. + +### User Stories + +#### US-1: Enter address via postal code (shopper) + +- **Story**: As a shopper, I want to type my postal code and have the remaining address fields filled automatically, so that I don't have to type my full address. +- **Acceptance Criteria**: + - **Given** ``, **when** I type a valid Brazilian CEP (e.g. `22231-000`) in the postal code field, **then** `street`, `city`, `state`, and `neighborhood` are filled from the VTEX postal code API. + - **Given** an invalid/incomplete postal code, **when** I blur the field, **then** no API call is made and the field shows a validation error. + - **Given** the postal code API returns no result, **when** the call completes, **then** the field shows an error and other fields remain empty. + - **Given** `autoCompletePostalCode={false}` on ``, **when** I type any postal code, **then** no API call is made. + +#### US-2: Enter address via cascading selects (shopper — hierarchical countries) + +- **Story**: As a shopper in a country without a postal code API (e.g. Ecuador), I want to select my state, city, and neighborhood from cascading dropdowns, so that I can specify my address without knowing a postal code. +- **Acceptance Criteria**: + - **Given** `` (`postalCodeFrom: THREE_LEVELS`), **when** I select a state, **then** the city dropdown is populated with cities for that state. + - **Given** a state and city selected, **when** I select a city, **then** the district/neighborhood dropdown is populated. + - **Given** all levels selected, **when** I confirm, **then** `postalCode` is derived from the selection and the address is valid. + +#### US-3: Enter address via geolocation (shopper) + +- **Story**: As a shopper, I want to type a street address in a Google Maps autocomplete field, so that my address is resolved without me knowing my postal code. +- **Acceptance Criteria**: + - **Given** `useGeolocation={true}` on `` and a `` with a valid API key, **when** I type a partial address and select a suggestion, **then** `street`, `number`, `city`, `state`, and `postalCode` are filled from the geocoder response. + - **Given** Google Maps is unavailable, **when** `` renders, **then** geolocation falls back gracefully (standard fields are shown). + - **Given** a geocoded result has a field value that doesn't match the country rules, **when** the mismatch is detected, **then** `logGeolocationAddressMismatch` is called with the mismatch details. + +#### US-4: Validate address on submit (app developer) + +- **Story**: As an app developer, I want to validate the full address before sending it to the order form, so that I only submit addresses that satisfy country-specific field requirements. +- **Acceptance Criteria**: + - **Given** a partially filled address, **when** `` fires submit, **then** `isValidAddress` is called, invalid fields get `valid: false` and `focus: true`, and the `onSubmit` callback is not called. + - **Given** a fully valid address, **when** `` fires submit, **then** `onSubmit` is called with the validated address. + - **Given** a required field is empty, **when** `validateField` is called, **then** the field returns `{ valid: false, reason: 'ERROR_EMPTY_FIELD' }`. + +#### US-5: Render address summary (read-only) + +- **Story**: As a shopper reviewing my order, I want to see my address formatted for my country, so that I can verify it before confirming. +- **Acceptance Criteria**: + - **Given** a complete Brazilian address, **when** `` renders, **then** the address is formatted according to `rules.summary` (street + number + complement / neighborhood / city-state / CEP). + - **Given** a field with `notApplicable: true`, **when** `` renders, **then** that field is omitted from the summary. + +#### US-6: Select country (app developer) + +- **Story**: As an app developer, I want to offer a country picker that resets the address form when the country changes, so that shoppers can switch markets cleanly. +- **Acceptance Criteria**: + - **Given** `` is rendered, **when** the shopper selects a new country, **then** `onChangeAddress` is called with `{ country: { value: 'XYZ' } }` and `` short-circuits validation (no field validation on country change). + - **Given** the new country is set, **when** `` re-renders, **then** the new country's rules are loaded via dynamic `import()`. + +#### US-7: Custom input rendering (app developer) + +- **Story**: As an app developer, I want to pass a custom `Input` component to the form, so that the address fields render with my design system's style. +- **Acceptance Criteria**: + - **Given** `Input={MyCustomInput}` on ``, **when** `` renders, **then** every field uses `MyCustomInput` instead of `DefaultInput`. + - **Given** no `Input` prop is passed, **when** `` renders, **then** `DefaultInput` is used as the fallback. + +#### US-8: Auto-complete fields display (shopper) + +- **Story**: As a shopper who used postal code autocomplete, I want to see which fields were filled automatically and be able to clear them, so that I can correct an incorrect autocomplete result. +- **Acceptance Criteria**: + - **Given** fields were filled by `postalCodeAutoCompleteAddress`, **when** `` renders, **then** a summary of the auto-filled fields is shown. + - **Given** the shopper clicks to edit, **when** the address changes, **then** the `postalCodeAutoCompleted` flag is cleared from the changed field. + +### Key Scenarios + +| Scenario | Pre-conditions | Steps | Expected Result | +|---|---|---|---| +| Brazilian CEP happy path | `country=BRA`, `postalCodeFrom=POSTAL_CODE`, API returns data | Type valid CEP → blur | `street`, `city`, `state`, `neighborhood` filled; form valid | +| CEP not found | `country=BRA`, API returns 404 | Type valid-format CEP → blur | Field shows `ERROR_POSTAL_CODE`; other fields unchanged | +| Country change clears validation | Valid address in BRA | Change country to ARG | `onChangeAddress` called with new country; no validation errors on old fields | +| Three-level cascade (ECU) | `country=ECU`, `postalCodeFrom=THREE_LEVELS` | Select state → city → district | `postalCode` derived; form valid | +| Geolocation autocomplete | `useGeolocation=true`, Maps loaded | Type address → select suggestion | All available fields filled from geocoder; mismatch logged if applicable | +| Geolocation Maps unavailable | `useGeolocation=true`, Maps fails to load | Render `` | Falls back to standard postal code form | +| Submit with missing required field | Form with empty `street` | Call `` submit | `street` gets `{ valid: false, focus: true }`; `onSubmit` not called | +| Custom Input component | `Input={StyleguideInput}` on container | Render `` | All fields use `StyleguideInput`; `DefaultInput` never rendered | +| Unknown country fallback | `country=XYZ` (no rule file) | Render `` | Falls back to `default.ts` rules; warning logged in non-production | +| Postal code with geolocation flag | Valid postal code with `geolocationAutoCompleted: true` | Change postal code field | `postalCodeAutoCompleteAddress` NOT triggered (guard flag present) | + +### Functional Requirements + +1. **Country rules loading** — `` dynamically imports `react/country/.ts` on mount and on `country` prop change. Falls back to `default.ts` on import error. +2. **Postal code autocomplete** — `AddressContainer` calls `postalCodeAutoCompleteAddress` when a valid postal code is entered and `autoCompletePostalCode !== false` and `postalCodeField.postalCodeAPI === true`. +3. **Geolocation integration** — `` loads the Maps SDK and provides `googleMaps` via React context. `` provides autocomplete. `getAddressByGeolocation` resolves coordinates to address fields. +4. **Real-time validation** — `validateChangedFields` runs on every `handleAddressChange` call. Field-level errors are reflected immediately. +5. **Country change short-circuit** — When `country.value` changes, `AddressContainer` passes the new address directly to `onChangeAddress` without running `validateChangedFields`. +6. **Custom input injection** — The `Input` prop on `` (or directly on ``) is passed down through `AddressContext` to every ``. +7. **postalCodeFrom strategies** — `POSTAL_CODE`: text field with API; `ONE_LEVEL`: one select; `TWO_LEVELS`: two cascading selects; `THREE_LEVELS`: three cascading selects. `` dispatches to the correct sub-component. +8. **Address summary formatting** — `` renders fields according to `rules.summary[][]` (a 2D array of field groups, each group on one line). +9. **Address validation** — `isValidAddress`, `validateField`, `validateAddress`, `validateChangedFields` are the exported validation API. Validation respects field `required`, `regex`, `options`, and `optionsMap` from the country rules. +10. **Telemetry** — `logGeolocationAddressMismatch` fires via `window.logSplunk` when a geocoded field value doesn't match the expected country rules. +11. **Transforms** — `addValidation` wraps a flat `Address` in `ValidatedField` wrappers. `removeValidation` strips them. These are the canonical serialization utilities for consumers. + +### Non-Functional Requirements + +- React 16.x compatible (peerDep `15.x || 16.x`). +- Zero Redux dependency — all state via React Context. +- Country rule files are dynamically imported (code-split per country). +- The npm bundle (`react/lib/`) is CJS, targeting environments that already provide React, react-intl, prop-types, and vtex-tachyons as peers. +- `window.logSplunk` calls are fire-and-forget with try/catch — telemetry failures must not crash the form. +- No SSR support required — `document` and `window` are used directly. + +### Out of Scope + +- Checkout order form submission (that is `vtex.omnishipping`'s responsibility). +- Shipping SLA/price calculation. +- Pickup point selection. +- Address book management (listing, deleting saved addresses). +- Payment address (handled by separate payment flow). + +--- + +## 2. Arch Decisions + +### Proposed Solution + +A React component library organized around two composable Context providers (`RulesContext` and `AddressContext`) and a dynamic country rules system. Consumers compose the library's components in a tree; the providers wire them together without prop-drilling. + +### Architecture Overview + +```mermaid +graph TD + AF["<AddressRules country='BRA'>
(loads + provides RulesContext)"] + AC["<AddressContainer address={} onChangeAddress={}>
(validates + provides AddressContext)"] + GMC["<GoogleMapsContainer apiKey={}gt;
(optional — loads Maps SDK)"] + FORM["<AddressForm />
(iterates rules.fields)"] + IFC["<InputFieldContainer field={} />
(renders one field)"] + SPC["<SelectPostalCode />
(ONE_LEVEL / TWO_LEVELS / THREE_LEVELS)"] + GI["<GeolocationInput />
(Maps autocomplete — optional)"] + PCG["<PostalCodeGetter />
(POSTAL_CODE text field)"] + AS["<AddressSubmitter onSubmit={}>
(validates on submit)"] + SUM["<AddressSummary />
(read-only display)"] + + AF --> AC + AC --> GMC + GMC --> FORM + FORM --> IFC + FORM --> SPC + GMC --> GI + AC --> PCG + AC --> AS + AC --> SUM + + subgraph "RulesContext (PostalCodeRules)" + AF + end + + subgraph "AddressContext (address + handleAddressChange + Input)" + AC + end +``` + +**Country rule loading sequence:** + +```mermaid +sequenceDiagram + participant C as Consumer + participant AR as AddressRules + participant R as react/country/BRA.ts + participant D as react/country/default.ts + + C->>AR: render country="BRA" + AR->>R: import('./country/BRA') + alt Import succeeds + R-->>AR: PostalCodeRules + AR->>AR: setState({ rules }) + AR-->>C: {children} + else Import fails (country not found) + AR->>D: import('./country/default') + D-->>AR: default PostalCodeRules + AR->>AR: console.warn + setState({ rules: default }) + AR-->>C: {children} + end +``` + +**Postal code autocomplete sequence:** + +```mermaid +sequenceDiagram + participant S as Shopper + participant AC as AddressContainer + participant PCA as postalCodeAutoCompleteAddress + participant PS as postalCodeService + participant API as /api/checkout/pub/postal-code/ + + S->>AC: type "22231000" in postalCode + AC->>AC: validateChangedFields → postalCode.valid=true + AC->>PCA: { cors, accountName, address, rules, callback } + PCA->>PS: getAddress({ country, postalCode }) + PS->>API: GET /api/checkout/pub/postal-code/BRA/22231000 + API-->>PS: { street, city, state, neighborhood, ... } + PS-->>PCA: addressData + PCA->>AC: callback(filledAddress) + AC->>AC: handleAddressChange(filledAddress) + AC-->>S: form fields filled +``` + +### Alternatives Considered + +| Alternative | Pros | Cons | Verdict | +|---|---|---|---| +| Redux for address state | Familiar to omnishipping team | Requires consumers to set up a store; adds weight to standalone npm use | Rejected — Context is sufficient for a form library | +| Single monolithic `` | Simpler API | No composability; consumers can't inject postal code getter, summary, or submitter independently | Rejected — composable components allow flexible layout | +| Country rules as server-fetched JSON | Rules can be updated without releasing | Adds network dependency; slower TTI; complicates error handling | Rejected — static imports allow code-splitting and offline use | +| Inline all country rules in one bundle | Simpler loading | 55+ country files × average 3KB = ~165KB extra in the main bundle | Rejected — dynamic `import()` keeps initial bundle lean | +| Replace axios with fetch | Removes one runtime dependency | Fetch requires polyfill in older VTEX storefront environments; axios interceptors useful for test mocking | Deferred — not worth the migration risk today | + +### Risks & Mitigations + +| Risk | Impact | Likelihood | Mitigation | +|---|---|---|---| +| Breaking `PostalCodeRules` type shape | High — all consumers break | Med | Public API discipline rule 30; major version bump required | +| Incorrect country rule (wrong regex/mask) | High — wrong UX for an entire market | Med | Mandatory unit test for every country file; demo app manual QA | +| Google Maps SDK load failure | Med — geolocation unavailable | Med | `GoogleMapsContainer` catches load errors and renders children without Maps context | +| Postal code API timeout | Med — form hangs | Low | `axios` timeout configuration; `postalCodeAutoCompleteAddress` handles promise rejection | +| npm publish with wrong lib/ build | High — consumers get stale code | Low | `prepublishOnly` script runs `yarn build`; `publish-release.sh` orchestrates the release | +| Version skew between manifest.json and react/package.json | Med — confusing releases | Low | `publish-release.sh` bumps both atomically; constitution enforces lockstep | + +### Key Decisions + +#### Decision 1: Dual public API surfaces (IO app + npm package) + +- **Status**: Accepted +- **Context**: `vtex.address-form` must work both as a VTEX IO peer dep (where the IO framework resolves components via `react/components.ts`) and as an independent npm package (where consumers import from `@vtex/address-form` via `react/index.ts`). +- **Decision**: Maintain two distinct entry points — `react/components.ts` (VTEX IO component map, default export object) and `react/index.ts` (npm named exports). Both are public API surfaces governed by rule 30. +- **Consequences**: Two surfaces to maintain and version in lockstep. Any new component added to one should be evaluated for the other. Rollup builds from `react/index.ts` → `react/lib/`. + +#### Decision 2: React Context over Redux + +- **Status**: Accepted +- **Context**: `vtex.shipping-manager` owns the Redux store for the checkout shipping step. `address-form` is a standalone library that must also work outside Redux-managed apps (e.g., returns, npm consumers). +- **Decision**: All state is managed via React Context (`RulesContext`, `AddressContext`). No Redux dependency. +- **Consequences**: Consumers that use Redux must bridge the state themselves (e.g., by passing `address` from Redux state as a prop to ``). The library remains dependency-light for npm consumers. + +#### Decision 3: Dynamic country rule imports + +- **Status**: Accepted +- **Context**: 55+ country rule files, each ~2–10KB. Bundling all statically would add significant weight to every consumer's main chunk. +- **Decision**: `AddressRules.tsx` uses `import('./country/')` (dynamic import) so each country file is a separate code-split chunk loaded on demand. +- **Consequences**: Async loading; `AddressRules` renders nothing until the rules promise resolves. Consumers must handle the brief null render (typically unnoticeable on fast connections). Bundlers (Rollup, webpack) must support dynamic imports. + +#### Decision 4: `injectRules` and `injectAddressContext` HOCs alongside hooks + +- **Status**: Accepted +- **Context**: Legacy class components in consumers cannot use hooks (`useAddressRules`, `useAddressContext`). Both patterns must coexist. +- **Decision**: Export both `injectRules` / `injectAddressContext` HOCs (for class components) and `useAddressRules` / `useAddressContext` hooks (for function components). Both read from the same contexts. +- **Consequences**: Two usage patterns to document and test. New internal components should use hooks. Existing components using HOCs are not migrated unless the PR scope includes migration. + +#### Decision 5: `postalCodeFrom` as a strategy enum in country rules + +- **Status**: Accepted +- **Context**: Countries resolve postal codes in four fundamentally different ways. The rendering approach (text input vs. cascading selects) must be driven by data, not hardcoded per country. +- **Decision**: Each country rule file declares `postalCodeFrom: POSTAL_CODE | ONE_LEVEL | TWO_LEVELS | THREE_LEVELS`. `SelectPostalCode` dispatches to `OneLevel`, `TwoLevels`, or `ThreeLevels` sub-components based on this value. +- **Consequences**: New postal code strategies require a new constant, a new sub-component, and updates to `SelectPostalCode`. The `PostalCodeRules` type must include the new strategy string. + +### Implementation Plan + +This is an existing, stable app. The baseline is already implemented. Future work follows the SDD Lite pipeline for most tasks and SDD Full for public API changes. Priorities: + +1. New country support → SDD Lite (new country rule file + unit test). +2. Bug fixes in validation → SDD Lite (regression test + fix). +3. Breaking API changes → SDD Full (cross-repo coordination required). + +--- + +## 3. Technical Contract + +### Data Models + +#### Address (raw, from order form) + +```typescript +interface Address { + addressId: string + addressType: 'residential' | 'search' | 'pickup' | 'giftRegistry' | 'instore' | 'commercial' + postalCode?: string | null + country?: string | null + street?: string | null + number?: string | null + complement?: string | null + city?: string | null + state?: string | null + neighborhood?: string | null + reference?: string | null + isDisposable?: boolean | null + geoCoordinates?: number[] | null + receiverName?: string | null + addressQuery?: string | null +} +``` + +#### AddressWithValidation (form state) + +```typescript +type AddressWithValidation = { + [field in keyof Address]: ValidatedField +} + +interface ValidatedField { + value?: Value | null + valueOptions?: string[] + valid?: boolean + reason?: string // error code: EEMPTY, ENOTOPTION, ECOUNTRY, etc. + visited?: boolean + focus?: boolean + disabled?: boolean + postalCodeAutoCompleted?: boolean + geolocationAutoCompleted?: boolean + notApplicable?: boolean +} +``` + +#### PostalCodeRules (country rule shape) + +```typescript +interface PostalCodeRules { + country: string | null + abbr: string | null // 2-letter ISO code (for Google Maps) + postalCodeFrom?: PostalCodeSource // POSTAL_CODE | ONE_LEVEL | TWO_LEVELS | THREE_LEVELS + postalCodeLevels?: FillableFields[] + postalCodeProtectedFields?: string[] // fields overwritten by postal code API + firstLevelPostalCodes?: ... + secondLevelPostalCodes?: ... + thirdLevelPostalCodes?: ... + fields: PostalCodeFieldRule[] // ordered list of address fields + geolocation?: GeolocationRules // overrides for fields when useGeolocation=true + summary?: PostalCodeSummaryLine[][] // 2D layout for AddressSummary +} +``` + +#### PostalCodeFieldRule (per-field config) + +```typescript +type PostalCodeFieldRule = { + name: FillableFields // field key (street, city, state, etc.) + label?: string // i18n key + fixedLabel?: string // literal label (not i18n'd) + size?: 'mini' | 'small' | 'medium' | 'large' | 'xlarge' + required?: boolean + hidden?: boolean + mask?: string // e.g. '99999-999' for BRA CEP + regex?: string | RegExp + maxLength?: number + postalCodeAPI?: boolean // triggers postal code autocomplete when true + autoComplete?: boolean | string + autoUpperCase?: boolean + options?: string[] // for select fields + optionsPairs?: Array<{ label: string; value: string }> + optionsMap?: ... // for cascading selects + forgottenURL?: string // "forgot my postal code" link + basedOn?: Fields // cascading dependency + level?: number // cascade level (1, 2, 3) + notApplicable?: boolean + elementName?: string + defaultValue?: unknown +} +``` + +#### Error Codes + +```typescript +const EEMPTY = 'ERROR_EMPTY_FIELD' // required field is empty +const EADDRESSTYPE = 'ERROR_ADDRESS_TYPE' // invalid addressType value +const ENOTOPTION = 'ERROR_VALUE_IS_NOT_AN_OPTION' // value not in options list +const ECOUNTRY = 'ERROR_COUNTRY_CODE' // invalid country code +const EGEOCOORDS = 'ERROR_GEO_COORDS' // invalid geoCoordinates +const EPOSTALCODE = 'ERROR_POSTAL_CODE' // postal code API error +const EGOOGLEADDRESS = 'ERROR_GOOGLE_ADDRESS' // Google Maps geocoder error +``` + +### Interfaces + +#### npm package public API (`react/index.ts`) + +```typescript +// Components +export { AddressContainer } // Context provider: validates, triggers autocomplete +export { AddressForm } // Renders all fields per country rules +export { AddressRules } // Loads country rules, provides RulesContext +export { AddressSubmitter } // Validates on submit +export { AddressSummary } // Read-only address display +export { AutoCompletedFields } // Shows postal-code-filled fields +export { CountrySelector } // Country picker +export { PostalCodeGetter } // Postal code text field + +// Component registry +export { default as components } // Same as react/components.ts + +// Helper functions +export { addValidation } // (address: Address, rules) => AddressWithValidation +export { removeValidation } // (address: AddressWithValidation) => Address +export { isValidAddress } // (address, rules) => { valid: boolean, address: AddressWithValidation } +export { validateField } // (value, name, address, rules) => { valid, reason } +export { injectRules } // HOC: wraps component with RulesContext consumer +export { injectAddressContext } // HOC: wraps component with AddressContext consumer +export { default as helpers } // { addValidation, removeValidation, isValidAddress, validateField, injectAddressContext, injectRules, getAddressByGeolocation } + +// Types (re-exported) +export type { Address, AddressWithValidation, ValidatedField, AddressType, Fields, FillableFields } +export type { PostalCodeRules, PostalCodeFieldRule, PostalCodeSource, GeolocationRules } +``` + +#### VTEX IO component map (`react/components.ts`) + +```typescript +export default { + AddressContainer, + AddressForm, + AddressRules, + AddressSubmitter, + AddressSummary, + AutoCompletedFields, + CountrySelector, + GoogleMapsContainer, + Map, + PostalCodeGetter, + PostalCodeLoader, + StyleguideInput, + StyleguideButton, +} +``` + +#### Input registry (`react/inputs.ts`) + +```typescript +export default { + DefaultInput, // Unstyled default + GeolocationInput, // Google Maps autocomplete input + InputError, + InputLabel, + InputSelect, + InputText, + StyleguideInput, // VTEX Styleguide-styled input +} +``` + +#### Key component props + +```typescript +// AddressRules +interface AddressRulesProps { + country: string // ISO3 code (e.g. 'BRA', 'USA') + fetch?: (country: string) => Promise // custom rule fetcher + shouldUseIOFetching?: boolean // use VTEX IO built-in module resolution + useGeolocation?: boolean // merge geolocation overrides into rules.fields + children: ReactNode +} + +// AddressContainer +interface AddressContainerProps { + address: AddressWithValidation + rules: PostalCodeRules // usually injected by injectRules HOC + onChangeAddress: (address: AddressWithValidation, ...args: any[]) => void + Input?: React.ComponentType // custom input renderer + cors?: boolean // use cross-origin postal code API URL + accountName?: string // required when cors=true + autoCompletePostalCode?: boolean // default: true + shouldHandleAddressChangeOnMount?: boolean // default: false + shouldAddFocusToNextInvalidField?: boolean // default: true + children: ReactNode +} + +// AddressForm +interface AddressFormProps { + address: AddressWithValidation // from AddressContext + rules: PostalCodeRules // from RulesContext + onChangeAddress: (changed: Partial) => void + Input?: React.ComponentType + omitPostalCodeFields?: boolean + omitAutoCompletedFields?: boolean + className?: string + notApplicableLabel?: string +} +``` + +#### Validation functions + +```typescript +// Validate a single field +function validateField( + value: AddressValues, + name: Fields, + address: AddressWithValidation, + rules: Rules +): { valid: boolean; reason?: string } + +// Validate all changed fields (called by AddressContainer on every change) +function validateChangedFields( + changedAddressFields: Partial, + address: AddressWithValidation, + rules: Rules +): AddressWithValidation + +// Validate the entire address (called by AddressSubmitter on submit) +function validateAddress( + address: AddressWithValidation, + rules: Rules +): AddressWithValidation + +// Check if all required fields are valid; focus first invalid field +function isValidAddress( + address: AddressWithValidation, + rules: Rules +): { valid: boolean; address: AddressWithValidation } +``` + +#### Postal code service + +```typescript +// Called by postalCodeAutoCompleteAddress (not exported directly) +function getAddress(params: { + cors?: boolean + accountName?: string + country: string + postalCode: string +}): Promise> +// → GET /api/checkout/pub/postal-code// +// → or GET https://.vtexcommercestable.com.br/api/checkout/pub/postal-code/... +``` + +#### Geolocation utilities + +```typescript +// Entry point for geolocation-to-address resolution +function getAddressByGeolocation(props: { + address: AddressWithValidation + onChangeAddress: (address: AddressWithValidation) => void + rules: PostalCodeRules + googleMaps: typeof google.maps +}): void // async, fires onChangeAddress when done + +// Maps Google geocoder result → AddressWithValidation +function geolocationAutoCompleteAddress( + address: AddressWithValidation, + googleAddress: google.maps.GeocoderResult, + rules: PostalCodeRules, + useGeolocation?: boolean +): AddressWithValidation +``` + +#### Telemetry + +```typescript +// Logs a geolocation field mismatch to Splunk +function logGeolocationAddressMismatch(data: { + fieldValue: AddressValues | null + fieldName: Fields + countryFromRules: string | null + address: Record +}): void +// → window.logSplunk({ level: 'Debug', type: 'Warning', workflowType: 'address-form', ... }) +``` + +### Integration Points + +| System | Direction | How | Details | +|---|---|---|---| +| VTEX Checkout Postal Code API | Outbound | `axios` GET | `/api/checkout/pub/postal-code//` — returns street, city, state, neighborhood | +| Google Maps JavaScript SDK | Outbound | `load-google-maps-api` npm package | Loaded by `GoogleMapsContainer`; used for autocomplete input and geocoding | +| `vtex.omnishipping` | Consumer | VTEX IO peer dep + `injectRules` / context | Mounts `` + `` + `` in the checkout shipping step | +| `vtex.shipping-preview` | Consumer | VTEX IO peer dep | Mounts `` + `` for postal code entry on the cart page | +| `vtex.checkout` | Consumer | VTEX IO peer dep | Mounts address components during the checkout address step | +| `@vtex/address-form` (npm) | Consumer | `import { AddressRules, ... } from '@vtex/address-form'` | Non-IO consumers import from the CJS bundle in `react/lib/` | +| `window.logSplunk` | Outbound | Direct window call | Splunk telemetry for geolocation mismatch events; guarded by try/catch | +| `vtex.country-codes` | Peer dep | VTEX IO | Used by `CountrySelector` to resolve country names and codes | +| Crowdin | i18n sync | `crowdin.yml` | Translates `messages/en.json` to 25+ locale files (`%two_letters_code%.json`) | +| `@vtex/intl-equalizer` | Dev tooling | `yarn lint:locales` | Enforces key parity across all locale files; reference locale `pt` | + +### Invariants & Constraints + +1. **Two-Context model is immutable** — `RulesContext` and `AddressContext` are the only state-sharing mechanism. No global variables, no Redux, no module-level state. +2. **Country rules are always loaded before rendering** — `` renders `null` until the async import resolves. No child should assume `rules` is available synchronously. +3. **`react/index.ts` exports are the npm contract** — removing, renaming, or changing any export requires a major version bump (both `manifest.json` and `react/package.json`) and consumer coordination. +4. **`react/components.ts` keys are the IO contract** — same policy as invariant 3. +5. **`PostalCodeRules` shape is frozen** — changing any field in `PostalCodeFieldRule` or `PostalCodeRules` is a breaking change for all consumers. +6. **Postal code API calls flow through `postalCodeService.js`** — no component may call the API directly. +7. **`en.json` is the only manually edited locale** — all other locale files are Crowdin-managed. +8. **Yarn only** — no npm, no pnpm. `react/` has its own `package.json` with a separate `yarn.lock`. +9. **Test runner is `yarn --cwd react test`** — not at repo root. The Jest config lives in `react/package.json`. +10. **Version lockstep** — `manifest.json` version and `react/package.json` version must always be identical after a release. Divergence is a bug. +11. **`window.logSplunk` calls must not be removed** without a follow-up telemetry task being filed. +12. **No SSR** — `document` and `window` are used directly in `postalCodeService.js` and `metrics.ts`. Do not add SSR support without a spec.