diff --git a/AGENTS.md b/AGENTS.md index 744b1b342..c81b92aba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,6 +105,12 @@ For source code comments, follow these rules: - Prefix the PR title with the Jira ticket (e.g. `CEXT-1234: short description`) - Always follow `.github/PULL_REQUEST_TEMPLATE.md` when writing PR descriptions +## Specs + +- Specs live in `specs/features/`, named with the Jira ticket prefix (e.g. `CEXT-6138-capability-discovery.md`) +- Use `specs/features/_template.md` as the starting point +- See `specs/conventions.md` for when a spec is required and authoring guidelines + ## Changesets - Every code change that requires a release must include a changeset (test-only changes do not need one) diff --git a/package.json b/package.json index 854537b87..243a041ec 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "code:fix": "turbo run code:fix", "docs": "turbo run docs", "format": "turbo run format", - "format:markdown": "turbo run format:markdown", + "format:markdown": "turbo run format:markdown && prettier --no-error-on-unmatched-pattern --write 'specs/**/*.md'", "format:check": "turbo run format:check", "lint": "turbo run lint", "lint:fix": "turbo run lint:fix", diff --git a/specs/conventions.md b/specs/conventions.md new file mode 100644 index 000000000..8639d128d --- /dev/null +++ b/specs/conventions.md @@ -0,0 +1,76 @@ +# Spec Conventions + +## Why specs + +The spec process provides a consistent path for substantial changes to the SDK so +that all stakeholders can be confident about its direction. + +Many changes — bug fixes, internal refactoring, documentation improvements — can +go straight to a pull request. But some changes are substantial enough to warrant +a design process and explicit alignment before implementation begins. + +A spec is not a living document. It represents the design agreed upon at the +time the spec PR was merged. If a subsequent ticket changes the design, it +produces a new spec file — the original remains unchanged as a record of what +was decided and why. This avoids spec drift: the spec always reflects the +intent at the time it was written, not whatever was eventually built. + +## When to write a spec + +Write a spec when the change is "substantial". This includes: + +- New public API surface (new exports, new config fields, new lifecycle hooks) +- Changes to existing public API that affect SDK consumers +- New SDK-level features that span multiple packages +- Removal of existing public API + +A spec is not required for: + +- Bug fixes with no API surface change +- Internal refactoring that doesn't affect SDK consumers +- Test or documentation additions +- Dependency updates + +If in doubt, write a spec. A short spec is better than a surprise. A spec +should be detailed enough to align on the design and unblock implementation, +but not so detailed that it describes code — leave implementation choices to +the implementor. + +## Files + +Specs live in `specs/features/` and are named with the Jira ticket prefix so +they are easy to find: + +``` +specs/features/CEXT-6138-capability-discovery.md +``` + +Use `specs/features/_template.md` as the starting point. + +Spec files are auto-formatted by Prettier on every commit (via lint-staged) and +by `pnpm format:markdown`. Focus on content — don't worry about line wrapping or +whitespace. + +## Workflow + +A feature goes through two pull requests: + +1. **Spec PR** — contains only the spec file. Reviewers align on the design + before any implementation begins. Once merged, the spec is considered + approved and the feature is ready to implement. +2. **Implementation PR** — contains the code. References the approved spec. + Once merged, the spec status is updated to _Implemented_. + +This separation ensures design decisions are made explicitly and are not +shaped retroactively by implementation details. + +### Status + +A spec has no explicit status until it is implemented. The repo state is the +source of truth: + +- **Spec PR open** — the spec is under review. +- **Spec PR merged** — the spec is approved and implementation can begin. +- **Implementation PR merged** — check the `Implemented` box in the spec + metadata as part of the implementation PR. This is the only status transition + that requires an explicit change. diff --git a/specs/features/CEXT-6138-capability-discovery.md b/specs/features/CEXT-6138-capability-discovery.md new file mode 100644 index 000000000..4ad3ca27f --- /dev/null +++ b/specs/features/CEXT-6138-capability-discovery.md @@ -0,0 +1,158 @@ +# Capability Discovery + +- **Ticket:** [CEXT-6138](https://jira.corp.adobe.com/browse/CEXT-6138) +- **Created:** 2026-04-23 +- [ ] **Implemented** + +## Summary + +Provide a formal discoverability mechanism for the features and endpoints exposed +by a Commerce SDK-based app, so that any authenticated client can reliably +determine what is available for a given deployment without needing SDK-specific +knowledge. + +## Motivation + +As the Commerce SDK grows in scope, clients need to know which features and +endpoints are active for a given deployment. Without a formal mechanism, clients +are forced to infer feature availability at runtime by calling endpoints and +inspecting response shapes — a fragile pattern that couples client logic to +internal API details. + +`commerce-app-management` is a concrete example: it currently calls `getAppConfig` +and inspects the response shape to decide which UI features to show or hide. Every +new SDK capability requires a corresponding change in `commerce-app-management` to +detect it. This does not scale as the number of clients and SDK features grows. + +**Goals:** + +- Give any authenticated client a stable, versioned description of which endpoints are active for a given deployment. +- Remove the need for clients to infer feature availability by parsing response shapes or probing endpoints. +- Keep the SDK as the single source of truth for the API contract. + +**Non-goals:** + +- Providing a general API documentation portal or developer-facing reference — the full spec in the SDK repository serves that purpose. +- Runtime introspection of config-dependent availability — endpoint availability is determined at build time and is fixed for a given deployment. +- Client code generation from the spec. + +## Developer experience + +An authenticated client fetches `/__metadata__/openapi.json` with a valid IMS +bearer token: + +```http +GET /__metadata__/openapi.json +Authorization: Bearer +``` + +The response is an OpenAPI 3.x document listing only the endpoints active for +that deployment. A client uses it to adapt its behaviour — no SDK-specific +knowledge required. + +**Example: `commerce-app-management`** + +On load, `commerce-app-management` already holds an IMS token from the user +session. It fetches `/__metadata__/openapi.json` and drives UI visibility based on +which endpoints are present — showing or hiding features accordingly. This +replaces the current pattern of calling `getAppConfig` and inferring availability +from the response shape. + +Other clients (CLI tools, integrations, third-party UIs) follow the same pattern: +obtain an IMS token, fetch the spec, adapt accordingly. + +## Design + +### 1. Full spec in the SDK package + +`aio-commerce-lib-app` ships an `openapi.json` (OpenAPI 3.x) documenting **all** +endpoints the SDK can expose. This file is included in the published npm package +under a well-known path (e.g. `dist/openapi.json`) and is the single source of +truth for the SDK's API contract. + +### 2. Filtered spec generated by `pre-app-build` + +The SDK's `pre-app-build` hook already reads `app.commerce.config.{ts,js}` to +determine which features are active for the app being built, and uses this to +generate `ext.config.yaml` with only the required actions. + +The same hook generates a **filtered `openapi.json`** — a subset of the full spec +containing only the endpoints that correspond to the active config domains. The +generated file includes the SDK version and the app version for traceability. + +Availability is determined by what the app developer declares in +`app.commerce.config.ts`, not by which SDK modules are imported. The +`pre-app-build` hook is the correct and only insertion point since it is the only +place where both the full SDK spec and the app's config are available +simultaneously. + +### 3. Metadata action + +The filtered `openapi.json` is served by a dedicated `metadata` runtime action +protected by the same IMS authentication as the rest of the SDK endpoints. + +Suggested path: `/__metadata__/openapi.json` + +The filtered spec is bundled into the action at build time, following the same +pattern used by other SDK actions (e.g. `app-config`, `config`): `pre-app-build` +writes `openapi.json` to a well-known path, and the generated action template +imports it statically: + +```js +import openApiSpec from "../../openapi.json" with { type: "json" }; +``` + +The bundler inlines the JSON at build time; no disk read occurs at request time. +The action factory receives the spec object and returns it in the response body. + +### 4. SDK upgrades + +When an app upgrades its `aio-commerce-lib-app` dependency, the full `openapi.json` +in the new package version may include new endpoints or updated schemas. On the +next build, `pre-app-build` derives the active config domains from +`app.commerce.config.ts`, applies the same filtering, and deploys an updated +`openapi.json` with the `metadata` action. + +No manual spec maintenance is required. New SDK features are automatically included +if their config domain is already active. If a new feature introduces a new config +domain, the app developer opts in by declaring it in `app.commerce.config.ts` and +rebuilding. + +## Drawbacks + +- Adds a new `metadata` runtime action to every SDK-based deployment. +- Clients must hold a valid IMS token before they can discover what is available, + which adds a bootstrap step for clients that don't already have one. + +## Rationale and alternatives + +The filtered spec is auth-gated to prevent **deployment fingerprinting**: it +reveals which features a specific deployment has enabled, which is +deployment-specific metadata that should not be publicly enumerable. Schema +disclosure is not a concern since the full spec is already public in the +`aio-commerce-sdk` repository. + +Alternatives considered: + +- **Runtime inspection of `getAppConfig`** — current approach; fragile and + requires SDK-specific knowledge in every client. +- **Unauthenticated static file** — simpler to consume but exposes + deployment-specific configuration without auth. +- **Capabilities list endpoint** — a simpler boolean feature map instead of a + full OpenAPI spec; less powerful and doesn't give clients the full contract. + +## Unresolved questions + +- Which config domains map to which endpoints in the filtered spec — to be + defined during implementation based on the state of the SDK at that time. +- Whether the full `openapi.json` in the SDK package is hand-authored or + generated from the `HttpActionRouter` schemas (e.g. via Standard Schema + + `@standard-community/standard-openapi`) — to be decided during implementation. + +## Future possibilities + +- The `metadata` action could be extended to expose additional deployment + information beyond the OpenAPI spec (e.g. SDK version, configured domains) + under a common `/__metadata__/` namespace. +- The full `openapi.json` in the SDK package could be used to power generated + client libraries or documentation automatically. diff --git a/specs/features/_template.md b/specs/features/_template.md new file mode 100644 index 000000000..4f9f28b30 --- /dev/null +++ b/specs/features/_template.md @@ -0,0 +1,67 @@ +# Feature Name + +- **Ticket:** [CEXT-XXXX](https://jira.corp.adobe.com/browse/CEXT-XXXX) +- **Created:** YYYY-MM-DD +- [ ] **Implemented** + +## Summary + +One paragraph explanation of the feature. + +## Motivation + +Describe the problem this feature solves for developers building Commerce apps +with the SDK. Include necessary background and specific use cases where this +feature helps. This is the most important section — be concrete about the pain +point before proposing a solution. + +Close the section with explicit goals (what success looks like) and non-goals +(what is explicitly out of scope), so that the design that follows is bounded. + +## Developer experience + +Explain the feature as if it were already shipped and you were showing it to an +app developer using the SDK. This means: + +- What new concepts or APIs does it introduce? +- Show concrete usage examples (config, code, CLI output). +- How should developers think about this feature and how does it change how they + build apps? +- If applicable, show what error messages or warnings look like. +- Does this affect how developers read or maintain their existing Commerce app? + +## Design + +The technical detail of the feature. Cover enough that: + +- Interactions with existing SDK features (lib-app, lib-auth, lib-config, etc.) + are clear. +- It is reasonably clear how the feature would be implemented. +- Edge cases are called out explicitly. + +Return to the examples from the Developer experience section and explain how the +design makes them work. + +## Drawbacks + +Why should we _not_ do this? Consider implementation cost, maintenance burden, +impact on SDK consumers, and whether it adds complexity for the common case. + +## Rationale and alternatives + +- Why is this the best design in the space of possible designs? +- What other approaches were considered and why were they rejected? +- What is the impact of not doing this? +- Could this be achieved in user-space (i.e. without SDK changes)? + +## Unresolved questions + +- What needs to be resolved through discussion before this spec is accepted? +- What needs to be resolved during implementation before the feature ships? +- What related problems are explicitly out of scope for this spec? + +## Future possibilities + +What is the natural evolution of this feature? How might it interact with other +planned SDK features? Use this section to capture related ideas that are out of +scope now but worth tracking.