From b3eb87ab83cf035733ad740ad094e1042c9437bf Mon Sep 17 00:00:00 2001 From: jrmartinez Date: Sat, 30 May 2026 01:26:40 -0400 Subject: [PATCH 1/6] fix(archetype): sync archetype-catalog version + add @implSpec guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Archetype catalog was pinned to 0.3.32-SNAPSHOT while the project is at 0.4.28-SNAPSHOT, so headless `archetype:generate` from the catalog resolved an unavailable version. Sync all 19 entries to 0.4.28-SNAPSHOT. Add @implSpec Javadoc to BaseCreateController/BaseCreateService demonstrating the self-describing pattern (extend, override executeCreate, don't add verb mappings) — comment-only, no behavioral change. Co-Authored-By: Claude Opus 4.8 (1M context) --- archetype/archetype-catalog.xml | 38 +++++++++---------- .../rest/controller/BaseCreateController.java | 6 +++ .../rest/service/BaseCreateService.java | 13 +++++++ 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/archetype/archetype-catalog.xml b/archetype/archetype-catalog.xml index fdc8c87c0..cb2f90585 100644 --- a/archetype/archetype-catalog.xml +++ b/archetype/archetype-catalog.xml @@ -20,97 +20,97 @@ io.americanexpress.synapse synapse-archetype-client-graphql - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-client-rest - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-client-rest-delete - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-client-rest-get - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-client-rest-post - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-client-rest-put - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-client-rest-reactive - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-client-rest-reactive-delete - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-client-rest-reactive-get - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-client-rest-reactive-post - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-client-rest-reactive-put - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-data-postgres - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-service-rest-delete - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-service-rest-post - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-service-rest-get - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-service-rest-put - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-service-rest-reactive-get - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-service-rest-reactive-post - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT io.americanexpress.synapse synapse-archetype-service-rest-reactive-put - 0.3.32-SNAPSHOT + 0.4.28-SNAPSHOT diff --git a/service/synapse-service-rest/src/main/java/io/americanexpress/synapse/service/rest/controller/BaseCreateController.java b/service/synapse-service-rest/src/main/java/io/americanexpress/synapse/service/rest/controller/BaseCreateController.java index b43523512..b895d7756 100644 --- a/service/synapse-service-rest/src/main/java/io/americanexpress/synapse/service/rest/controller/BaseCreateController.java +++ b/service/synapse-service-rest/src/main/java/io/americanexpress/synapse/service/rest/controller/BaseCreateController.java @@ -31,6 +31,12 @@ * {@code BaseCreateController} class specifies the abstraction for listening for requests from the consumer * to Create (POST) a resource. This controller expects only one entry as request. * + * @implSpec To expose a create (HTTP POST) endpoint, extend this class and supply the type arguments; + * the {@code @PostMapping}, {@code @Valid} validation, Swagger documentation and {@code ResponseEntity} + * wrapping are inherited. Do not add verb mappings ({@code @PostMapping} etc.) on the + * subclass — declare only {@code @RestController} and {@code @RequestMapping("")}. Put the + * business logic in the paired service's {@code executeCreate} method, not here. + * * @param input request type * @param output response type * @param service type diff --git a/service/synapse-service-rest/src/main/java/io/americanexpress/synapse/service/rest/service/BaseCreateService.java b/service/synapse-service-rest/src/main/java/io/americanexpress/synapse/service/rest/service/BaseCreateService.java index 95053f3d0..f94445e03 100644 --- a/service/synapse-service-rest/src/main/java/io/americanexpress/synapse/service/rest/service/BaseCreateService.java +++ b/service/synapse-service-rest/src/main/java/io/americanexpress/synapse/service/rest/service/BaseCreateService.java @@ -46,6 +46,19 @@ public O create(HttpHeaders headers, I request) { * @param request body received from the controller * @return response body to the controller */ + /** + * Create the resource. This is the single extension point for create operations. + * + * @implSpec Implement this method (not the public {@code create}) with the create business logic. + * Return the populated response; on invalid input or business-rule failure throw + * {@code io.americanexpress.synapse.framework.exception.ApplicationClientException} with an + * {@code ErrorCode} (4XX), or {@code ApplicationServerException} (5XX) for unexpected failures — + * the framework's {@code ControllerExceptionHandler} renders the standard error response. + * + * @param headers the HTTP headers + * @param request the input request to create + * @return the created resource response + */ protected abstract O executeCreate(HttpHeaders headers, I request); } From ff53468a2e18f6fa6f48e9f6ca551cfaf1abea5c Mon Sep 17 00:00:00 2001 From: jrmartinez Date: Sat, 30 May 2026 01:27:28 -0400 Subject: [PATCH 2/6] feat(agent-tooling): add Synapse AI-agent adaptation layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make Synapse reliably consumable by AI coding agents so they generate idiomatic code instead of re-deriving base-class signatures from source. - .synapse/catalog.json (+schema): authoritative machine-readable catalog of every base class — generics, the execute* method to override, HTTP verb/path, config triple, ErrorCodes, archetype coordinates. - .synapse/mcp/ + .mcp.json: zero-dependency MCP server exposing the catalog as list_base_classes / scaffold_operation / validate_module to any client. - .claude/agents/synapse-engineer.md + .claude/skills/synapse-engineer/: Claude Code agent + scaffolding skill. - docs/agent/RECOMMENDATIONS.md: framework-level proposals for the team (resolve dual imperative API family, normalize per-DB property keys, generate the catalog from source, etc.). - docs/agent/archunit/: convention tests giving agents a deterministic verify loop (template; not yet wired into the reactor). - CLAUDE.md: repo orientation + AI agent tooling section. - .gitignore: ignore the Claude Code harness lock artifact. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/agents/synapse-engineer.md | 140 +++++++ .claude/skills/synapse-engineer/SKILL.md | 231 +++++++++++ .gitignore | 3 + .mcp.json | 8 + .synapse/catalog.json | 202 ++++++++++ .synapse/catalog.schema.json | 62 +++ .synapse/mcp/README.md | 65 ++++ .synapse/mcp/synapse_mcp.py | 366 ++++++++++++++++++ CLAUDE.md | 121 ++++++ docs/agent/RECOMMENDATIONS.md | 114 ++++++ docs/agent/archunit/README.md | 39 ++ .../agent/archunit/SynapseConventionTest.java | 118 ++++++ 12 files changed, 1469 insertions(+) create mode 100644 .claude/agents/synapse-engineer.md create mode 100644 .claude/skills/synapse-engineer/SKILL.md create mode 100644 .mcp.json create mode 100644 .synapse/catalog.json create mode 100644 .synapse/catalog.schema.json create mode 100644 .synapse/mcp/README.md create mode 100644 .synapse/mcp/synapse_mcp.py create mode 100644 CLAUDE.md create mode 100644 docs/agent/RECOMMENDATIONS.md create mode 100644 docs/agent/archunit/README.md create mode 100644 docs/agent/archunit/SynapseConventionTest.java diff --git a/.claude/agents/synapse-engineer.md b/.claude/agents/synapse-engineer.md new file mode 100644 index 000000000..9527a5457 --- /dev/null +++ b/.claude/agents/synapse-engineer.md @@ -0,0 +1,140 @@ +--- +name: synapse-engineer +description: Expert implementer of the American Express Synapse framework (io.americanexpress.synapse) on Spring Boot. Delegate to this agent whenever code must be written USING Synapse — building a REST/reactive/GraphQL service, a REST/SOAP client, or a JPA/Mongo data-access module, or scaffolding a new Synapse app from an archetype. It knows the exact base classes to extend, the generics to pass, the protected methods to override, the config/package wiring, and the archetypes — so other agents emit idiomatic Synapse code instead of plain Spring. +color: blue +model: opus +--- + +You are **Synapse Engineer**, the in-house authority on the **American Express Synapse framework** +(`io.americanexpress.synapse`) — a convention-over-configuration layer on top of Spring Boot. You +implement application code *the Synapse way*: extend a small, opinionated **Base class per layer** and +override its single `protected abstract execute*` method, instead of hand-wiring controllers/services/DAOs. + +Every signature below is **verified against this repo**. Do not invent classes or methods. When unsure, +read the closest module under a `*-samples/` directory (canonical: `service/service-samples/sample-service-rest-mysql-book`). + +**Authoritative source: read `.synapse/catalog.json` first.** It is the machine-readable catalog of every +base class, generics, the exact `execute*` method to override, HTTP verb/path, config triple, ErrorCodes, +and archetype coordinates — prefer it over re-deriving from source. The same data is available to any MCP +client via the server in `.synapse/mcp/` (`list_base_classes` / `scaffold_operation` / `validate_module`). +The step-by-step recipe, test scaffolding, and pom/properties templates live in the **`synapse-engineer` skill**. + +## Stack facts + +- `io.americanexpress.synapse`, multi-module Maven, **Java 21**, **Spring Boot 3.5.x**, Spring Cloud + 2025.x, **`jakarta.*`** (not `javax.*`), JUnit 5 / Mockito 5. Artifacts publish to Maven Central. +- Logging uses **`org.slf4j.ext.XLogger`** (`XLoggerFactory.getXLogger(...)`) with `logger.entry()/exit()`. +- Imperative = Spring Web MVC; reactive = WebFlux (Mono/Flux). Parallel mirror hierarchies — pick one. +- **Two controller/service families exist.** Center on the one the samples use: + `io.americanexpress.synapse.service.rest.*` (granular, `executeCreate/executeRead/...`). A newer unified + family lives under `io.americanexpress.synapse.api.rest.imperative.*` (single `BaseService.execute`); + mention it only if asked. + +## The core mental model + +``` +@RestController + @RequestMapping ─ Controller extends Base*Controller (you write ~3 lines) +@Service ─ Service extends Base*Service (override one protected execute* method) +@Repository ─ Repository extends JpaRepository (Spring Data directly — Synapse adds no base repo) +@Entity @Table ─ Entity extends BaseEntity (id + audit columns free) +Request ─ implements BaseServiceRequest (INTERFACE) (jakarta.validation constraints) +Response ─ extends BaseServiceResponse (abstract, has String id) +@Configuration ─ AppConfig @Import({YourDataConfig.class, ServiceRestConfig.class}) + @ComponentScan(app pkg) + @PropertySource +``` + +## Imperative REST catalog — `io.americanexpress.synapse.service.rest` + +Controllers (`.controller`) — you only declare the class + `@RestController` + `@RequestMapping`. The base owns the verb mapping, `@Valid`, Swagger, logging, `ResponseEntity`: + +| Controller (extend) | HTTP | Path | Service generic bound | Service overrides (`protected abstract`) | +|---|---|---|---|---| +| `BaseCreateController>` | POST | `` root → 201 | `BaseCreateService` | `O executeCreate(HttpHeaders, I)` | +| `BaseReadMonoController>` | POST | `/inquiry_results` | `BaseReadMonoService` | `O executeRead(HttpHeaders, I)` | +| `BaseReadPolyController>` | POST | `/multiple_results` → `List` | `BaseReadPolyService` | `Page executeRead(HttpHeaders, I)` | +| `BaseGetMonoController>` | GET | `/{id}` | `BaseGetMonoService` (no `I`) | `O executeRead(HttpHeaders, String identifier)` | +| `BaseUpdateController>` | PUT | `` root → 204 (void) | `BaseUpdateService` (no `O`) | `void executeUpdate(HttpHeaders, I)` | +| `BaseDeleteController` | DELETE | `/{identifier}` → 204 | `BaseDeleteService` (no `I`/`O`) | `void executeDelete(HttpHeaders, String id)` | + +Key gotchas: the public method is `create/read/update/delete`; you override the **`execute*`** variant. Reads +are **POST** with a request body (`/inquiry_results`, `/multiple_results`); only get-by-id is a real GET. +`BaseReadPolyService.executeRead` returns Spring Data **`Page`**. `BaseGetMono*` carries only `` and reads by `String`. + +## Models, config, errors + +- **`BaseServiceRequest` is an interface → `implements`.** `BaseServiceResponse` is an abstract class with a + `String id` (getId/setId) → `extends`. Put `jakarta.validation` constraints (`@NotBlank`, …) on the request; + the base controller's `@Valid` enforces them. +- **Config class is `ServiceRestConfig`** (`io.americanexpress.synapse.service.rest.config`): + `@Configuration @ComponentScan("io.americanexpress.synapse.service.rest") @Import({ExceptionConfig.class, UtilitiesCommonConfig.class})`. + App config: `@Configuration @PropertySource("classpath:.properties") @ComponentScan(basePackages="") @Import({.class, ServiceRestConfig.class})`. + This auto-registers the `ControllerExceptionHandler` (`@RestControllerAdvice`) — never write your own. +- **Errors** (`io.americanexpress.synapse.framework.exception`): throw + `new ApplicationClientException(String developerMessage, ErrorCode errorCode, String... messageArguments)` for 4XX, + and `new ApplicationServerException(Throwable cause)` for 5XX. `ErrorCode` + (`...exception.model.ErrorCode`) carries `HttpStatus` + message: `GENERIC_4XX_ERROR`(400), + `MISSING_HTTP_HEADER_ERROR`(400), `UNAUTHORIZED`(401), `FORBIDDEN`(403), `NOT_FOUND`(404), + `GENERIC_5XX_ERROR`(500), `AUTHENTICATION_ERROR`(403), `TOO_MANY_REQUESTS`(429), `LOCKED`(423), + `RESOURCE_OUT_OF_SYNC`(409). Idiom: `throw new ApplicationClientException(ErrorCode.NOT_FOUND.getMessage(), ErrorCode.NOT_FOUND, (String[]) null);` + Never hand-build error bodies; the handler emits an `ErrorResponse{code,message,moreInfo,developerMessage}`. + +## Data layer — `io.americanexpress.synapse.data.*` + +- **`BaseEntity` (JPA)**: `@MappedSuperclass @EntityListeners(AuditingEntityListener.class)`; `Long id` + (`@Id @GeneratedValue(IDENTITY)`); audit cols `created_date_time`/`last_modified_date_time` typed **`Instant`** + (`@CreatedDate`/`@LastModifiedDate`), `created_by`/`last_modified_by` String, `Long version` (`@Version`). + Entity: `@Entity @Table(name="...") extends BaseEntity`. +- **Repository**: `@Repository interface XRepository extends JpaRepository` — Spring Data directly + (Synapse does **not** supply a base repo to extend; just add derived finders). +- **Auditing**: enable with `InstantJpaAuditingDataConfig` (`@EnableJpaAuditing(dateTimeProviderRef="instantDateTimeProvider")`). +- **DB config**: extend the per-DB abstract config (`BasePostgresDataConfig`, `BaseMySqlDataConfig`, …) — ctor takes + `Environment`, override `void setPackagesToScan(LocalContainerEntityManagerFactoryBean)`, annotate with + `@Configuration @PropertySource(...) @EnableJpaRepositories(basePackages="...dao")`. Postgres reads + `spring.datasource.{url,username,password,driver-class-name}` (+ Hikari pool, `spring.jpa.properties.hibernate.default_schema`). +- Variants: `synapse-data-jpa` (JPA core + BaseEntity/auditing), `-postgres`/`-mysql`/`-mssql`/`-oracle`/`-db2` + (per-DB Hikari config), `-jdbc` (Spring Data JDBC, no ORM), `-mongodb` (document; `String` id, `LocalDateTime` + audits, `BaseMongoDBDataConfig`), `-cassandra`/`-couchbase`/`-redis`. + +## Reactive REST — `io.americanexpress.synapse.service.reactive.rest` + +Mirror of imperative, but: controllers return `Mono>` / `Flux`; services override +`Mono executeCreate(...)`, `Mono/Flux executeRead(...)`, `Mono executeUpdate/executeDelete(...)`. +Controllers: `BaseCreateReactiveController`, `BaseGetMonoReactiveController` (GET `/{id}`), `BaseGetFluxReactiveController` +(GET → `Flux`), `BaseReadMonoReactiveController` (POST `/inquiry_results`), `BaseReadFluxReactiveController` +(POST `/multiple_results`), `BaseUpdateReactiveController`, `BaseDeleteReactiveController`. Reactive **models live in +`...reactive.rest.model`** (own `BaseServiceRequest` interface / `BaseServiceResponse` abstract). App config +**extends** `BaseServiceReactiveRestConfig` (`@EnableWebFlux`, constructor takes `ObjectMapper`). Pair with +`synapse-data-mongodb` (reactive), never blocking JPA. + +## Client layer — `io.americanexpress.synapse.client.rest` + +Consume external APIs by extending +`BaseRestClient>` +(`super(httpHeadersFactory, httpMethod)`); call via `O callMonoService(HttpHeaders, I, String... pathVars)` / +`List callPolyService(...)`. Reactive: `BaseReactiveRestClient` → `Mono/Flux`. Provide a +`BaseClientHttpHeadersFactory` subclass (`abstract HttpHeaders create(HttpHeaders, I, String url)`); errors via +`BaseRestResponseErrorHandler`, logging via `RestClientLoggingInterceptor`. `BaseClientRequest`/`BaseClientResponse` +are marker interfaces. Config extends `BaseRestClientConfig` and calls `initializeClient(url, client, errorHandler, ...)`. +SOAP → `synapse-client-soap` (`BaseSoapClient`, `${client.url}`); GraphQL → `synapse-client-graphql` (`BaseGraphQLClient`, always POST). + +## Module families (choose deps by need) + +service: `synapse-service-rest`, `-reactive-rest`, `-graphql`, `-test` · client: `synapse-client-rest`, `-soap`, +`-graphql`, `-test` · data: `synapse-data-jpa`/`-postgres`/`-mysql`/`-mssql`/`-oracle`/`-db2`/`-jdbc`/`-mongodb`/`-cassandra`/`-couchbase`/`-redis` +· framework: `synapse-framework-exception`, `-logging` (Log4j2+XLogger), `-api-docs` (springdoc/Swagger `ApiDocsConfig`), `-test` +· messaging: `synapse-publisher-kafka`, `synapse-subscriber-kafka` · serverless: `synapse-function` +· utility: `synapse-utilities-common` (+ `UtilitiesCommonConfig`), `-cryptography` (`CryptoUtil`), `-date`, `-number`, `-telephone`. + +## How you work + +1. Clarify the shape: REST vs reactive vs GraphQL? which operation rows? persisted by which data module? +2. **New standalone app → prefer the archetype** (`mvn archetype:generate`, see the skill). Existing module → hand-write the slice. +3. Generate the full slice per operation: Controller + Service + Request + Response (+ Entity + Repository if persisted), + matching the package layout `config/ controller/ service/ model/ entity/ dao(repository)/`. +4. Wire it: app `@Configuration` (`@Import` the layer + data config, `@ComponentScan`, `@PropertySource`); pom deps + for the chosen `synapse-*` modules + DB driver; properties for datasource/JPA/server/springdoc. +5. Mirror, don't mix imperative/reactive. Validate inputs with jakarta constraints; raise errors via the two exceptions. +6. Verify: `./mvnw -pl -am clean test` (root reactor: `./mvnw clean package`). +7. When fuzzy, read the nearest `*-samples` module and copy its structure. + +For the step-by-step recipe with copy-ready skeletons, the archetype command, and pom/properties templates, +invoke the **`synapse-engineer` skill**. diff --git a/.claude/skills/synapse-engineer/SKILL.md b/.claude/skills/synapse-engineer/SKILL.md new file mode 100644 index 000000000..1624e5374 --- /dev/null +++ b/.claude/skills/synapse-engineer/SKILL.md @@ -0,0 +1,231 @@ +--- +name: synapse-engineer +description: > + Scaffold and implement Spring Boot code with the American Express Synapse framework (io.americanexpress.synapse) — + REST/reactive services, REST/SOAP/GraphQL clients, and JPA/Mongo data access. Use when creating a new Synapse app + or adding a Synapse controller/service/repository to an existing one. Provides the base-class decision table, + copy-ready file skeletons, the Maven archetype command, and pom/properties templates. +--- + +# Synapse Engineer — scaffolding procedure + +Implement code the Synapse way: extend one Base class per layer and override its single `protected abstract +execute*` method. All signatures are verified against this repo (`io.americanexpress.synapse`, Java 21, Spring +Boot 3.5.x, `jakarta.*`, `XLogger`). Canonical worked example: +`service/service-samples/sample-service-rest-mysql-book` (service) + `data/data-samples/sample-data-mysql-book` (data). + +## Ground truth: `.synapse/catalog.json` + +Before scaffolding, read **`.synapse/catalog.json`** — the machine-readable catalog of every base class, +its generics, the exact `execute*` method to override, HTTP verb/path, the config triple, ErrorCodes, and +archetype coordinates. It is authoritative; prefer it over re-deriving signatures from source. The tables +below mirror it for quick reference. (An MCP server at `.synapse/mcp/` exposes the same data as +`list_base_classes` / `scaffold_operation` / `validate_module` for non–Claude-Code agents.) + +## Step 1 — Choose the path + +- **New standalone app** → use the Maven archetype (Step 2A). +- **Add to an existing module** → hand-write the slice (Step 2B onward). + +## Step 2A — Generate from an archetype + +Archetypes (under `archetype/`, coords `io.americanexpress.synapse:synapse-archetype-`): service-rest-`get`/`post`/`put`/`delete`, +their `service-rest-reactive-*` variants, `client-rest`(+`-get/-post/-put/-delete` and reactive), `client-graphql`, +`data-postgres`. Full list in `archetype/archetype-catalog.xml`. + +```bash +mvn archetype:generate \ + -DarchetypeGroupId=io.americanexpress.synapse \ + -DarchetypeArtifactId=synapse-archetype-service-rest-post \ + -DarchetypeVersion= \ + -DgroupId=com.example.myapp -DartifactId=my-service -Dversion=0.0.1-SNAPSHOT \ + -DjavaVersion=21 \ + -Dauthor="Your Name" -DapiName="MyApi" -DbaseUrl="/v1/books" -DclassName="Book" \ + -DinteractiveMode=false +``` + +Required/typical properties: `groupId`, `artifactId`, `version`, `javaVersion` (default 21), plus archetype-specific +`author`, `apiName`, `className`, and `baseUrl` (service) / `url` (client). Then fill in domain fields + business logic (Steps 3–5). + +> Editing archetype *templates*? CI enforces tokenization in `archetype-resources/`: keep +> `@project.version@`, `maven-compiler-plugin` with +> `@maven.compiler.plugin.version@`, and `requiredProperty key="javaVersion"` defaulting to +> `@maven.compiler.source@`. Never hardcode versions there. + +## Step 2B — Pick the base class (imperative REST) + +| Operation | Controller extends | Service extends | Override (`protected abstract`) | +|---|---|---|---| +| Create (POST → 201) | `BaseCreateController` | `BaseCreateService` | `Resp executeCreate(HttpHeaders, Req)` | +| Read one by body (POST `/inquiry_results`) | `BaseReadMonoController` | `BaseReadMonoService` | `Resp executeRead(HttpHeaders, Req)` | +| Read many (POST `/multiple_results`) | `BaseReadPolyController` | `BaseReadPolyService` | `Page executeRead(HttpHeaders, Req)` | +| Get one by id (GET `/{id}`) | `BaseGetMonoController` | `BaseGetMonoService` | `Resp executeRead(HttpHeaders, String identifier)` | +| Update (PUT → 204) | `BaseUpdateController` | `BaseUpdateService` | `void executeUpdate(HttpHeaders, Req)` | +| Delete (DELETE `/{identifier}` → 204) | `BaseDeleteController` | `BaseDeleteService` | `void executeDelete(HttpHeaders, String id)` | + +Packages: controllers `io.americanexpress.synapse.service.rest.controller`, services `…service.rest.service`, +models `…service.rest.model`. Note: reads are POST-with-body except get-by-id; you override `execute*`, not the public method. + +**Reactive** (`io.americanexpress.synapse.service.reactive.rest.*`): same shapes but controllers return +`Mono>`/`Flux` and services override `Mono`/`Flux`/`Mono`; app config **extends** +`BaseServiceReactiveRestConfig`. Choose one paradigm; never mix. + +## Step 3 — Write the slice (verified patterns) + +App package root e.g. `com.example.myapp` with subpackages `config/ controller/ service/ model/ entity/ dao/`. + +**Request** — `implements BaseServiceRequest` (it is an interface) + jakarta validation: +```java +public class CreateBookRequest implements BaseServiceRequest { // io.americanexpress.synapse.service.rest.model.BaseServiceRequest + @NotBlank private String title; // jakarta.validation.constraints.NotBlank + @NotBlank private String author; + private String createdBy; + // getters / setters +} +``` + +**Response** — `extends BaseServiceResponse` (abstract; inherits `String id` + getId/setId): +```java +public class CreateBookResponse extends BaseServiceResponse { // io.americanexpress.synapse.service.rest.model.BaseServiceResponse + private String title; private String author; // getters/setters; id via inherited setId(...) +} +``` + +**Controller** — declaration only (base maps the verb): +```java +@RestController +@RequestMapping("/v1/books") +public class CreateBookController + extends BaseCreateController { } +``` + +**Service** — override the `execute*` method; constructor-inject the repository: +```java +@Service +public class CreateBookService extends BaseCreateService { + private final BookRepository bookRepository; + public CreateBookService(BookRepository bookRepository) { this.bookRepository = bookRepository; } + + @Override + protected CreateBookResponse executeCreate(HttpHeaders headers, CreateBookRequest request) { + if (bookRepository.findByTitle(request.getTitle()) != null) { + throw new ApplicationClientException(ErrorCode.RESOURCE_OUT_OF_SYNC.getMessage(), + ErrorCode.RESOURCE_OUT_OF_SYNC, (String[]) null); + } + BookEntity entity = new BookEntity(); + entity.setTitle(request.getTitle()); + entity.setAuthor(request.getAuthor()); + BookEntity saved = bookRepository.save(entity); + CreateBookResponse response = new CreateBookResponse(); + response.setId(String.valueOf(saved.getId())); + response.setTitle(saved.getTitle()); + response.setAuthor(saved.getAuthor()); + return response; + } +} +``` +Imports: `io.americanexpress.synapse.service.rest.service.BaseCreateService`, +`io.americanexpress.synapse.framework.exception.ApplicationClientException`, +`io.americanexpress.synapse.framework.exception.model.ErrorCode`, `org.springframework.http.HttpHeaders`. + +**Entity + Repository** (only if persisted): +```java +@Entity @Table(name = "book") +public class BookEntity extends BaseEntity { // io.americanexpress.synapse.data.jpa.entity.BaseEntity (id + audit cols free) + @Column(name = "title") private String title; + @Column(name = "author") private String author; // getters/setters +} + +@Repository +public interface BookRepository extends JpaRepository { // Spring Data directly + BookEntity findByTitle(String title); + BookEntity findByTitleAndAuthor(String title, String author); +} +``` + +## Step 4 — Wire it + +**App / service config** — import the layer config + your data config: +```java +@Configuration +@PropertySource("classpath:service-book-application.properties") +@ComponentScan(basePackages = "com.example.myapp") +@Import({ BookDataConfig.class, ServiceRestConfig.class }) // io.americanexpress.synapse.service.rest.config.ServiceRestConfig +public class BookConfig { } +``` +`ServiceRestConfig` auto-registers the `ControllerExceptionHandler` — don't write one. + +**Data config** — extend the per-DB base, point it at your entity + dao packages: +```java +@Configuration +@PropertySource("classpath:data-book-application.properties") +@EnableJpaRepositories(basePackages = "com.example.myapp.dao") +public class BookDataConfig extends BasePostgresDataConfig { // or BaseMySqlDataConfig, etc. + public BookDataConfig(Environment environment) { super(environment); } + @Override + protected void setPackagesToScan(LocalContainerEntityManagerFactoryBean emf) { + emf.setPackagesToScan("com.example.myapp.entity"); + } +} +// Enable auditing once: @Import(InstantJpaAuditingDataConfig.class) or add @EnableJpaAuditing(dateTimeProviderRef="instantDateTimeProvider"). +``` + +**Main class**: a normal `@SpringBootApplication` (samples log readiness via `XLoggerFactory.getXLogger(...)`). + +**pom.xml** — Spring Boot parent + the synapse modules + driver: +```xml + + io.americanexpress.synapse + synapse-service-rest${synapse.version} + io.americanexpress.synapse + synapse-data-postgres${synapse.version} + io.americanexpress.synapse + synapse-framework-exception${synapse.version} + org.postgresqlpostgresqlruntime + + +``` +Swap data module + driver per DB (mysql → `synapse-data-mysql` + `com.mysql:mysql-connector-j`, etc.). + +**application.properties** (Postgres example): +```properties +spring.application.name=Book Rest Service +server.port=8080 +spring.datasource.url=jdbc:postgresql://localhost:5432/book +spring.datasource.username=postgres +spring.datasource.password=password +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.properties.hibernate.default_schema=synapse +spring.jpa.hibernate.ddl-auto=update +``` +(The MySQL sample uses `spring.mysql.datasource.*` keys read by `BaseMySqlDataConfig`; match whichever base config you extend.) + +## Step 4.5 — Scaffold a test (don't skip) + +`synapse-service-test` provides base test classes — always emit at least one so the module ships green. +Add it as a test-scope dependency and extend the matching base: + +- `io.americanexpress.synapse.service.test.controller.BaseControllerTest` — controller slice test base. +- `io.americanexpress.synapse.service.test.controller.BaseControllerIT` — integration test base. +- Per-operation unit-test bases: `BaseCreateMonoControllerUnitTest`, `BaseReadMonoControllerUnitTest`, + `BaseReadPolyControllerUnitTest`, `BaseGetMonoControllerUnitTest`, `BaseUpdateControllerUnitTest`, + `BaseDeleteControllerUnitTest`. + +For convention enforcement (controllers extend a base, services override `execute*`, requests +`implements`/responses `extends`, no custom advice), copy `docs/agent/archunit/SynapseConventionTest.java` +into `src/test/java`, set `APP_PACKAGE`, add `com.tngtech.archunit:archunit-junit5` (test scope), and run +`mvn test` as the verify loop. + +## Step 5 — Conventions & verification + +- **Override `execute*`, not the public method.** `BaseServiceRequest` is an interface (`implements`); + `BaseServiceResponse` is abstract (`extends`, gives `id`). +- **Errors**: `throw new ApplicationClientException(devMsg, ErrorCode, msgArgs...)` (4XX) or + `new ApplicationServerException(cause)` (5XX). Real `ErrorCode`s: `GENERIC_4XX_ERROR`, `MISSING_HTTP_HEADER_ERROR`, + `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `GENERIC_5XX_ERROR`, `AUTHENTICATION_ERROR`, `TOO_MANY_REQUESTS`, + `LOCKED`, `RESOURCE_OUT_OF_SYNC`. Don't build error bodies by hand. +- **Validation**: jakarta constraints on the Request; the base controller's `@Valid` enforces them. +- **Don't** add `@PostMapping`/`ResponseEntity` plumbing to controllers — the base owns it. One controller class + per operation, grouped under a shared `@RequestMapping` base path. +- **Build/test**: `./mvnw -pl -am clean test` (root reactor: `./mvnw clean package`). +- When in doubt, read the nearest `*-samples` module and mirror it. diff --git a/.gitignore b/.gitignore index 419f8927a..2baf15dcf 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ buildNumber.properties # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) !/.mvn/wrapper/maven-wrapper.jar + +# Claude Code harness artifact +.claude/scheduled_tasks.lock diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..df4693e70 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "synapse": { + "command": "python3", + "args": [".synapse/mcp/synapse_mcp.py"] + } + } +} diff --git a/.synapse/catalog.json b/.synapse/catalog.json new file mode 100644 index 000000000..a55c0d0c1 --- /dev/null +++ b/.synapse/catalog.json @@ -0,0 +1,202 @@ +{ + "$schema": "./catalog.schema.json", + "catalogVersion": "1", + "framework": { + "groupId": "io.americanexpress.synapse", + "frameworkVersion": "0.4.28-SNAPSHOT", + "java": "21", + "springBoot": "3.5.x", + "jakarta": true, + "loggingFacade": "org.slf4j.ext.XLogger (XLoggerFactory.getXLogger), use logger.entry()/exit()" + }, + "notes": [ + "Build each operation by extending a Base*Controller + Base*Service pair; override the protected abstract execute* method, not the public method.", + "Two parallel imperative families exist. PREFER 'service.rest' (granular execute* methods) used by all samples. The 'api.rest.imperative' family (unified BaseService.execute) is newer/experimental; do not mix the two. See docs/agent/RECOMMENDATIONS.md #2.", + "BaseServiceRequest is an INTERFACE (implements). BaseServiceResponse is an abstract class with a String id (extends).", + "Reads are POST-with-body at /inquiry_results (one) and /multiple_results (many); only get-by-id is a real GET /{id}.", + "Synapse provides NO base repository: repositories extend Spring Data JpaRepository directly.", + "Never write a @RestControllerAdvice: importing the layer config registers ControllerExceptionHandler automatically." + ], + "families": { + "imperativeRest": { + "basePackage": "io.americanexpress.synapse.service.rest", + "artifact": "synapse-service-rest", + "appConfig": { + "import": "io.americanexpress.synapse.service.rest.config.ServiceRestConfig", + "pattern": "@Configuration @PropertySource(\"classpath:.properties\") @ComponentScan(basePackages=\"\") @Import({.class, ServiceRestConfig.class})" + }, + "models": { + "request": { "type": "io.americanexpress.synapse.service.rest.model.BaseServiceRequest", "kind": "interface", "use": "implements", "validation": "jakarta.validation.constraints on fields; @Valid enforced by base controller" }, + "response": { "type": "io.americanexpress.synapse.service.rest.model.BaseServiceResponse", "kind": "abstract class", "use": "extends", "inherits": "String id (getId/setId)" }, + "paginatedRequest": { "type": "io.americanexpress.synapse.service.rest.model.BasePaginatedServiceRequest", "use": "implements/extends for poly reads", "carries": "PageInformation{int page,int size}" } + }, + "operations": [ + { + "id": "create", + "httpMethod": "POST", + "path": "", + "successStatus": 201, + "controller": { "class": "BaseCreateController", "generics": ">" }, + "service": { "class": "BaseCreateService", "generics": "", "override": "protected O executeCreate(HttpHeaders headers, I request)" } + }, + { + "id": "readMono", + "httpMethod": "POST", + "path": "/inquiry_results", + "successStatus": 200, + "controller": { "class": "BaseReadMonoController", "generics": ">" }, + "service": { "class": "BaseReadMonoService", "generics": "", "override": "protected O executeRead(HttpHeaders headers, I request)" } + }, + { + "id": "readPoly", + "httpMethod": "POST", + "path": "/multiple_results", + "successStatus": 200, + "returns": "List (service returns Page)", + "controller": { "class": "BaseReadPolyController", "generics": ">" }, + "service": { "class": "BaseReadPolyService", "generics": "", "override": "protected org.springframework.data.domain.Page executeRead(HttpHeaders headers, I request)" } + }, + { + "id": "getMono", + "httpMethod": "GET", + "path": "/{id}", + "successStatus": 200, + "note": "Get-by-id; service generic carries only , reads by String identifier.", + "controller": { "class": "BaseGetMonoController", "generics": ">" }, + "service": { "class": "BaseGetMonoService", "generics": "", "override": "protected O executeRead(HttpHeaders headers, String identifier)" } + }, + { + "id": "update", + "httpMethod": "PUT", + "path": "", + "successStatus": 204, + "note": "Controller returns void; service generic carries only .", + "controller": { "class": "BaseUpdateController", "generics": ">" }, + "service": { "class": "BaseUpdateService", "generics": "", "override": "protected void executeUpdate(HttpHeaders headers, I request)" } + }, + { + "id": "delete", + "httpMethod": "DELETE", + "path": "/{identifier}", + "successStatus": 204, + "controller": { "class": "BaseDeleteController", "generics": "" }, + "service": { "class": "BaseDeleteService", "generics": "(none)", "override": "protected void executeDelete(HttpHeaders headers, String identifier)" } + } + ] + }, + "reactiveRest": { + "basePackage": "io.americanexpress.synapse.service.reactive.rest", + "artifact": "synapse-service-reactive-rest", + "appConfig": { + "extends": "io.americanexpress.synapse.service.reactive.rest.config.BaseServiceReactiveRestConfig", + "pattern": "@Configuration @EnableWebFlux(via base) class AppConfig extends BaseServiceReactiveRestConfig { AppConfig(ObjectMapper m){ super(m);} }", + "note": "Reactive models live in io.americanexpress.synapse.service.reactive.rest.model (own BaseServiceRequest interface + BaseServiceResponse abstract)." + }, + "operations": [ + { "id": "create", "httpMethod": "POST", "path": "", "successStatus": 201, "controller": { "class": "BaseCreateReactiveController", "generics": ">" }, "service": { "class": "BaseCreateReactiveService", "override": "protected Mono executeCreate(HttpHeaders headers, I request)" } }, + { "id": "getMono", "httpMethod": "GET", "path": "/{id}", "controller": { "class": "BaseGetMonoReactiveController", "generics": ">" }, "service": { "class": "BaseGetMonoReactiveService", "override": "protected Mono executeRead(HttpHeaders headers, String request)" } }, + { "id": "getFlux", "httpMethod": "GET", "path": "", "returns": "Flux", "controller": { "class": "BaseGetFluxReactiveController", "generics": ">" }, "service": { "class": "BaseGetFluxReactiveService", "override": "protected Flux executeRead(HttpHeaders headers)" } }, + { "id": "readMono", "httpMethod": "POST", "path": "/inquiry_results", "controller": { "class": "BaseReadMonoReactiveController", "generics": ">" }, "service": { "class": "BaseReadMonoReactiveService", "override": "protected Mono executeRead(HttpHeaders headers, I request)" } }, + { "id": "readFlux", "httpMethod": "POST", "path": "/multiple_results", "returns": "ResponseEntity>", "controller": { "class": "BaseReadFluxReactiveController", "generics": ">" }, "service": { "class": "BaseReadFluxReactiveService", "override": "protected Flux executeRead(HttpHeaders headers, I request)" } }, + { "id": "update", "httpMethod": "PUT", "path": "", "successStatus": 204, "controller": { "class": "BaseUpdateReactiveController", "generics": ">" }, "service": { "class": "BaseUpdateReactiveService", "override": "protected Mono executeUpdate(HttpHeaders headers, I request)" } }, + { "id": "delete", "httpMethod": "DELETE", "path": "/{identifier}", "successStatus": 204, "controller": { "class": "BaseDeleteReactiveController", "generics": "" }, "service": { "class": "BaseDeleteReactiveService", "override": "protected Mono executeDelete(HttpHeaders headers, String identifier)" } } + ] + }, + "data": { + "baseEntity": { + "jpa": { + "class": "io.americanexpress.synapse.data.jpa.entity.BaseEntity", + "artifact": "synapse-data-jpa", + "annotations": ["@MappedSuperclass", "@EntityListeners(AuditingEntityListener.class)"], + "id": "Long id @Id @GeneratedValue(strategy=GenerationType.IDENTITY)", + "auditFields": "Instant createdDateTime(created_date_time), Instant lastModifiedDateTime(last_modified_date_time), String createdBy(created_by), String lastModifiedBy(last_modified_by), Long version(@Version)", + "use": "@Entity @Table(name=\"...\") public class XEntity extends BaseEntity" + }, + "mongodb": { "class": "io.americanexpress.synapse.data.mongodb.entity.BaseEntity", "artifact": "synapse-data-mongodb", "id": "String (subclass adds @Id)", "auditFields": "LocalDateTime created/lastModified, String createdBy/lastModifiedBy, Long version", "note": "Different id + timestamp types than JPA — do not cross imports." }, + "jdbc": { "class": "io.americanexpress.synapse.data.jdbc.entity.BaseEntity", "artifact": "synapse-data-jdbc", "id": "Long (Spring Data JDBC @Id)", "note": "Spring Data Relational @Column, no ORM." } + }, + "repository": { + "pattern": "@Repository public interface XRepository extends org.springframework.data.jpa.repository.JpaRepository { ... derived finders ... }", + "note": "No Synapse base repository exists. Use Spring Data interfaces directly." + }, + "auditingConfig": "io.americanexpress.synapse.data.jpa.config.InstantJpaAuditingDataConfig (@EnableJpaAuditing(dateTimeProviderRef=\"instantDateTimeProvider\"))", + "dbConfigs": { + "pattern": "@Configuration @PropertySource(...) @EnableJpaRepositories(basePackages=\"...dao\") class XDataConfig extends BaseDataConfig { XDataConfig(Environment e){super(e);} @Override protected void setPackagesToScan(LocalContainerEntityManagerFactoryBean emf){ emf.setPackagesToScan(\"...entity\"); } }", + "modules": { + "synapse-data-postgres": { "config": "BasePostgresDataConfig", "driver": "org.postgresql:postgresql", "propertyKeys": "spring.datasource.{url,username,password,driver-class-name}, Hikari pool, spring.jpa.properties.hibernate.default_schema" }, + "synapse-data-mysql": { "config": "BaseMySqlDataConfig", "driver": "com.mysql:mysql-connector-j", "propertyKeys": "spring.mysql.datasource.{url,username,password,port} (NOTE: non-standard prefix — see RECOMMENDATIONS.md #6)" }, + "synapse-data-mssql": { "config": "BaseMssqlDataConfig" }, + "synapse-data-oracle": { "config": "BaseOracleDataConfig" }, + "synapse-data-db2": { "config": "BaseDb2DataConfig" }, + "synapse-data-jdbc": { "config": "SynapseJdbcDataConfig", "propertyKeys": "spring.datasource.*" }, + "synapse-data-mongodb": { "config": "BaseMongoDBDataConfig", "propertyKeys": "spring.data.mongodb.{database,uri}" } + }, + "otherModules": ["synapse-data-cassandra", "synapse-data-couchbase", "synapse-data-redis"] + } + }, + "client": { + "rest": { + "artifact": "synapse-client-rest", + "baseClass": "io.americanexpress.synapse.client.rest.client.BaseRestClient", + "generics": ">", + "ctor": "super(httpHeadersFactory, HttpMethod.)", + "callMethods": ["O callMonoService(HttpHeaders, I, String... pathVariables)", "List callPolyService(HttpHeaders, I, ParameterizedTypeReference>, String... pathVariables)"], + "headersFactory": "extend BaseClientHttpHeadersFactory; override: public HttpHeaders create(HttpHeaders clientHeaders, I request, String url)", + "errorHandler": "BaseRestResponseErrorHandler (throws ApplicationClientException on 4xx/5xx)", + "loggingInterceptor": "RestClientLoggingInterceptor", + "config": "extend BaseRestClientConfig; call initializeClient(url, client, errorHandler, connectTimeout, readTimeout, maxConnections)", + "models": "BaseClientRequest / BaseClientResponse are marker interfaces" + }, + "reactive": { "baseClass": "io.americanexpress.synapse.client.rest.client.BaseReactiveRestClient", "returns": "Mono/Flux", "note": "Uses WebClient; ctor takes BaseReactiveRestResponseErrorHandler" }, + "soap": { "artifact": "synapse-client-soap", "baseClass": "io.americanexpress.synapse.client.soap.client.BaseSoapClient", "url": "${client.url}", "deps": "WebServiceOperations template, Jaxb2Marshaller" }, + "graphql": { "artifact": "synapse-client-graphql", "baseClass": "io.americanexpress.synapse.client.graphql.client.BaseGraphQLClient", "note": "always POST; callPolyService unsupported" } + } + }, + "exceptions": { + "package": "io.americanexpress.synapse.framework.exception", + "artifact": "synapse-framework-exception", + "client": { "class": "ApplicationClientException", "ctor": "ApplicationClientException(String developerMessage, ErrorCode errorCode, String... messageArguments)", "use": "4XX / business validation failures" }, + "server": { "class": "ApplicationServerException", "ctor": "ApplicationServerException(Throwable cause)", "use": "wrap checked/unexpected exceptions → 5XX" }, + "idiom": "throw new ApplicationClientException(ErrorCode.NOT_FOUND.getMessage(), ErrorCode.NOT_FOUND, (String[]) null);", + "handler": "ControllerExceptionHandler (@RestControllerAdvice, auto-registered by layer config) emits ErrorResponse{code,message,moreInfo,developerMessage}", + "errorCodes": [ + { "name": "GENERIC_4XX_ERROR", "status": 400 }, + { "name": "MISSING_HTTP_HEADER_ERROR", "status": 400 }, + { "name": "UNAUTHORIZED", "status": 401 }, + { "name": "FORBIDDEN", "status": 403 }, + { "name": "NOT_FOUND", "status": 404 }, + { "name": "RESOURCE_OUT_OF_SYNC", "status": 409 }, + { "name": "LOCKED", "status": 423 }, + { "name": "TOO_MANY_REQUESTS", "status": 429 }, + { "name": "AUTHENTICATION_ERROR", "status": 403 }, + { "name": "GENERIC_5XX_ERROR", "status": 500 } + ] + }, + "archetypes": { + "catalogFile": "archetype/archetype-catalog.xml", + "groupId": "io.americanexpress.synapse", + "version": "0.4.28-SNAPSHOT", + "command": "mvn archetype:generate -DarchetypeGroupId=io.americanexpress.synapse -DarchetypeArtifactId= -DarchetypeVersion= -DgroupId= -DartifactId= -Dversion=0.0.1-SNAPSHOT -DjavaVersion=21 -Dauthor= -DapiName= -DclassName= -DbaseUrl=/v1/ -DinteractiveMode=false", + "requiredProperties": ["groupId", "artifactId", "version", "javaVersion", "author", "apiName", "className", "baseUrl (service) / url (client)"], + "ids": [ + "synapse-archetype-service-rest-get", "synapse-archetype-service-rest-post", "synapse-archetype-service-rest-put", "synapse-archetype-service-rest-delete", + "synapse-archetype-service-rest-reactive-get", "synapse-archetype-service-rest-reactive-post", "synapse-archetype-service-rest-reactive-put", + "synapse-archetype-client-rest", "synapse-archetype-client-rest-get", "synapse-archetype-client-rest-post", "synapse-archetype-client-rest-put", "synapse-archetype-client-rest-delete", + "synapse-archetype-client-rest-reactive", "synapse-archetype-client-rest-reactive-get", "synapse-archetype-client-rest-reactive-post", "synapse-archetype-client-rest-reactive-put", "synapse-archetype-client-rest-reactive-delete", + "synapse-archetype-client-graphql", "synapse-archetype-data-postgres" + ] + }, + "testing": { + "artifacts": ["synapse-service-test", "synapse-framework-test"], + "baseClasses": { + "controllerSliceTest": "io.americanexpress.synapse.service.test.controller.BaseControllerTest", + "controllerIT": "io.americanexpress.synapse.service.test.controller.BaseControllerIT", + "perOperationUnitTests": ["BaseCreateMonoControllerUnitTest", "BaseReadMonoControllerUnitTest", "BaseReadPolyControllerUnitTest", "BaseGetMonoControllerUnitTest", "BaseUpdateControllerUnitTest", "BaseDeleteControllerUnitTest"] + } + }, + "samples": { + "imperativeRestWithDb": "service/service-samples/sample-service-rest-mysql-book (+ data/data-samples/sample-data-mysql-book)", + "reactiveRest": "service/service-samples/sample-service-reactive-rest-book", + "graphql": "service/service-samples/sample-service-graphql-book" + } +} diff --git a/.synapse/catalog.schema.json b/.synapse/catalog.schema.json new file mode 100644 index 000000000..b2e21ee43 --- /dev/null +++ b/.synapse/catalog.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://americanexpress.github.io/synapse/agent/catalog.schema.json", + "title": "Synapse Agent Catalog", + "description": "Machine-readable catalog of Synapse base classes, operations, and conventions, consumed by AI coding agents (the synapse-engineer skill and MCP server) so they generate correct code without re-deriving signatures from source. SHOULD be generated from the framework source (annotation processor / Maven plugin) so it cannot drift. See docs/agent/RECOMMENDATIONS.md #1.", + "type": "object", + "required": ["catalogVersion", "framework", "families", "exceptions", "archetypes"], + "properties": { + "catalogVersion": { "type": "string", "description": "Schema/contract version of this catalog." }, + "framework": { + "type": "object", + "required": ["groupId", "frameworkVersion", "java"], + "properties": { + "groupId": { "type": "string" }, + "frameworkVersion": { "type": "string" }, + "java": { "type": "string" }, + "springBoot": { "type": "string" }, + "jakarta": { "type": "boolean" }, + "loggingFacade": { "type": "string" } + } + }, + "notes": { "type": "array", "items": { "type": "string" } }, + "families": { + "type": "object", + "description": "Layer families. Each operation pairs a controller with a service and names the protected abstract method to override.", + "additionalProperties": { + "type": "object", + "properties": { + "basePackage": { "type": "string" }, + "artifact": { "type": "string" }, + "operations": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "controller", "service"], + "properties": { + "id": { "type": "string" }, + "httpMethod": { "type": "string", "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"] }, + "path": { "type": "string" }, + "successStatus": { "type": "integer" }, + "controller": { + "type": "object", + "required": ["class"], + "properties": { "class": { "type": "string" }, "generics": { "type": "string" } } + }, + "service": { + "type": "object", + "required": ["class", "override"], + "properties": { "class": { "type": "string" }, "generics": { "type": "string" }, "override": { "type": "string", "description": "The exact protected abstract method signature the subclass must implement." } } + } + } + } + } + } + } + }, + "exceptions": { "type": "object" }, + "archetypes": { "type": "object" }, + "testing": { "type": "object" }, + "samples": { "type": "object" } + } +} diff --git a/.synapse/mcp/README.md b/.synapse/mcp/README.md new file mode 100644 index 000000000..cc07b1282 --- /dev/null +++ b/.synapse/mcp/README.md @@ -0,0 +1,65 @@ +# Synapse MCP server + +A zero-dependency [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the +Synapse base-class catalog (`.synapse/catalog.json`) to AI coding agents, so any MCP-capable runtime +generates correct, idiomatic Synapse code instead of re-deriving signatures from source. + +It is the runtime-agnostic counterpart to the `synapse-engineer` Claude Code agent/skill: the skill is +for Claude Code; this server works with **any** MCP client (Claude Code, Cursor, custom agents, …). + +## Requirements + +Python 3.8+. No pip installs — it uses only the standard library and speaks JSON-RPC 2.0 over stdio. + +## Tools + +| Tool | Purpose | Key args | +|---|---|---| +| `list_base_classes` | List operations/base classes for a layer family, with the `execute*` method to override. | `family` (optional: `imperativeRest`, `reactiveRest`) | +| `scaffold_operation` | Return copy-ready Controller + Service (+ Request/Response) skeletons for one operation. | `operation`, `entity`, `packageName`, `baseUrl`, `family` | +| `validate_module` | Statically check a generated module dir against Synapse conventions. | `modulePath` | + +`validate_module` flags: `@RestController` not extending a `Base*Controller`; a Synapse `@Service` that +never overrides `execute*`; `implements BaseServiceResponse` (it's abstract → `extends`); +`extends BaseServiceRequest` (it's an interface → `implements`); custom `@RestControllerAdvice` +(redundant — the layer config auto-registers `ControllerExceptionHandler`); verb `@*Mapping` on a +controller (the base already maps the verb). + +## Smoke test + +```bash +python3 .synapse/mcp/synapse_mcp.py --selftest # prints "selftest OK" +``` + +Manual JSON-RPC round trip: + +```bash +printf '%s\n' \ + '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"scaffold_operation","arguments":{"operation":"create","entity":"Book","packageName":"com.acme.book","baseUrl":"/v1/books"}}}' \ + | python3 .synapse/mcp/synapse_mcp.py +``` + +## Registering with an MCP client + +A ready-made `.mcp.json` lives at the repo root (Claude Code auto-discovers it). For other clients, +point them at the command: + +```json +{ + "mcpServers": { + "synapse": { + "command": "python3", + "args": [".synapse/mcp/synapse_mcp.py"] + } + } +} +``` + +Use an absolute path to `synapse_mcp.py` if the client's working directory isn't the repo root. + +## Keeping the catalog honest + +The server reads `.synapse/catalog.json`. That file is currently hand-maintained from verified source. +To guarantee it never drifts from the framework, generate it during the build — see +`docs/agent/RECOMMENDATIONS.md` (#1, catalog-generation Maven plugin). diff --git a/.synapse/mcp/synapse_mcp.py b/.synapse/mcp/synapse_mcp.py new file mode 100644 index 000000000..af0a49987 --- /dev/null +++ b/.synapse/mcp/synapse_mcp.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 +"""Synapse MCP server — exposes the Synapse base-class catalog to any MCP-capable +AI agent (Claude Code, etc.) so it generates correct, idiomatic Synapse code. + +Zero third-party dependencies: speaks the Model Context Protocol over stdio using +newline-delimited JSON-RPC 2.0, reading ground truth from ../catalog.json. + +Tools: + - list_base_classes : list operations/base classes for a layer family (filterable) + - scaffold_operation: return copy-ready Controller+Service(+Request/Response) skeletons for an operation + - validate_module : statically check a generated module dir against Synapse conventions + +Run directly for a smoke test: python3 synapse_mcp.py --selftest +""" +import json +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +CATALOG_PATH = os.path.join(HERE, "..", "catalog.json") +PROTOCOL_VERSION = "2024-11-05" +SERVER_INFO = {"name": "synapse-mcp", "version": "0.1.0"} + + +def load_catalog(): + with open(CATALOG_PATH, "r", encoding="utf-8") as fh: + return json.load(fh) + + +# --------------------------------------------------------------------------- # +# Tool implementations +# --------------------------------------------------------------------------- # +def _find_operation(catalog, family, operation_id): + fam = catalog["families"].get(family) + if not fam: + raise ValueError( + "unknown family '%s'; known: %s" % (family, ", ".join(catalog["families"])) + ) + for op in fam.get("operations", []): + if op["id"] == operation_id: + return fam, op + known = ", ".join(o["id"] for o in fam.get("operations", [])) + raise ValueError( + "unknown operation '%s' in family '%s'; known: %s" % (operation_id, family, known) + ) + + +def tool_list_base_classes(catalog, args): + family = args.get("family") + families = [family] if family else list(catalog["families"]) + out = {} + for fam_name in families: + fam = catalog["families"].get(fam_name) + if not fam or "operations" not in fam: + continue + out[fam_name] = { + "basePackage": fam.get("basePackage"), + "artifact": fam.get("artifact"), + "operations": [ + { + "id": op["id"], + "http": "%s %s" % (op.get("httpMethod", "?"), op.get("path", "")), + "controller": op["controller"]["class"], + "service": op["service"]["class"], + "override": op["service"]["override"], + } + for op in fam["operations"] + ], + } + return out + + +def _camel(name): + return re.sub(r"[^0-9a-zA-Z]", "", name[:1].upper() + name[1:]) + + +def tool_scaffold_operation(catalog, args): + family = args.get("family", "imperativeRest") + operation_id = args["operation"] + entity = _camel(args.get("entity", "Resource")) + pkg = args.get("packageName", "com.example.app") + base_url = args.get("baseUrl", "/v1/" + entity.lower() + "s") + + fam, op = _find_operation(catalog, family, operation_id) + ctrl = op["controller"]["class"] + svc = op["service"]["class"] + base_pkg = fam["basePackage"] + op_cap = _camel(operation_id) + reactive = family == "reactiveRest" + + req_cls = "%s%sRequest" % (entity, op_cap) + resp_cls = "%s%sResponse" % (entity, op_cap) + ctrl_cls = "%s%sController" % (entity, op_cap) + svc_cls = "%s%sService" % (entity, op_cap) + + # Determine generic argument list the concrete class passes to the base. + needs_req = "I extends" in (op["controller"].get("generics") or "") or "I extends" in ( + op["service"].get("generics") or "" + ) + needs_resp = "O extends" in (op["controller"].get("generics") or "") + + ctrl_generics = [] + if needs_req: + ctrl_generics.append(req_cls) + if needs_resp: + ctrl_generics.append(resp_cls) + ctrl_generics.append(svc_cls) + ctrl_decl = "%s<%s>" % (ctrl, ", ".join(ctrl_generics)) if ctrl_generics else ctrl + + svc_generics = [] + if needs_req: + svc_generics.append(req_cls) + if needs_resp: + svc_generics.append(resp_cls) + svc_decl = "%s<%s>" % (svc, ", ".join(svc_generics)) if svc_generics else svc + + files = {} + + files["controller/%s.java" % ctrl_cls] = ( + "package %s.controller;\n\n" + "import %s.controller.%s;\n" + "import org.springframework.web.bind.annotation.RequestMapping;\n" + "import org.springframework.web.bind.annotation.RestController;\n\n" + "@RestController\n" + "@RequestMapping(\"%s\")\n" + "public class %s extends %s {\n}\n" + % (pkg, base_pkg, ctrl, base_url, ctrl_cls, ctrl_decl) + ) + + override = op["service"]["override"] + if reactive: + body = " return reactor.core.publisher.Mono.empty(); // TODO business logic" + if "Flux<" in override: + body = " return reactor.core.publisher.Flux.empty(); // TODO business logic" + elif override.startswith("protected void"): + body = " // TODO business logic" + elif "Page<" in override: + body = " return org.springframework.data.domain.Page.empty(); // TODO business logic" + else: + body = " return new %s(); // TODO business logic" % resp_cls + + files["service/%s.java" % svc_cls] = ( + "package %s.service;\n\n" + "import %s.service.%s;\n" + "import org.springframework.http.HttpHeaders;\n" + "import org.springframework.stereotype.Service;\n\n" + "@Service\n" + "public class %s extends %s {\n\n" + " @Override\n" + " %s {\n" + "%s\n" + " }\n}\n" + % (pkg, base_pkg, svc, svc_cls, svc_decl, override, body) + ) + + if needs_req: + files["model/%s.java" % req_cls] = ( + "package %s.model;\n\n" + "import %s.model.BaseServiceRequest;\n" + "import jakarta.validation.constraints.NotBlank;\n\n" + "public class %s implements BaseServiceRequest {\n" + " @NotBlank private String name; // TODO real fields\n" + " public String getName() { return name; }\n" + " public void setName(String name) { this.name = name; }\n}\n" + % (pkg, base_pkg, req_cls) + ) + if needs_resp: + files["model/%s.java" % resp_cls] = ( + "package %s.model;\n\n" + "import %s.model.BaseServiceResponse;\n\n" + "public class %s extends BaseServiceResponse {\n" + " private String name; // TODO real fields; id inherited via setId()\n" + " public String getName() { return name; }\n" + " public void setName(String name) { this.name = name; }\n}\n" + % (pkg, base_pkg, resp_cls) + ) + + return { + "family": family, + "operation": operation_id, + "http": "%s %s%s" % (op.get("httpMethod"), base_url, op.get("path", "")), + "appConfigHint": fam.get("appConfig"), + "files": files, + } + + +def tool_validate_module(catalog, args): + root = args["modulePath"] + findings = [] + java_files = [] + for dirpath, _dirs, names in os.walk(root): + if os.sep + "test" + os.sep in dirpath: + continue + for n in names: + if n.endswith(".java"): + java_files.append(os.path.join(dirpath, n)) + + if not java_files: + return {"ok": False, "findings": [{"level": "error", "msg": "no .java files under " + root}]} + + ctrl_bases = set() + svc_bases = set() + for fam in catalog["families"].values(): + for op in fam.get("operations", []): + ctrl_bases.add(op["controller"]["class"]) + svc_bases.add(op["service"]["class"]) + + for path in java_files: + with open(path, "r", encoding="utf-8") as fh: + src = fh.read() + rel = os.path.relpath(path, root) + is_ctrl = "@RestController" in src + is_svc = re.search(r"@Service\b", src) is not None + + if is_ctrl: + if not any(("extends " + b) in src for b in ctrl_bases): + findings.append({"level": "error", "file": rel, + "msg": "@RestController does not extend a Synapse Base*Controller"}) + if "controller" not in rel.replace("\\", "/").split("/"): + findings.append({"level": "warn", "file": rel, + "msg": "controller not in a 'controller' package"}) + if re.search(r"@(Post|Get|Put|Delete|Request)Mapping", src) and "class " in src: + # mappings on the class beyond @RequestMapping base path are a smell + if re.search(r"@(Post|Get|Put|Delete)Mapping", src): + findings.append({"level": "warn", "file": rel, + "msg": "verb @*Mapping found; base controller already maps the verb"}) + if is_svc and any(("extends " + b) in src for b in svc_bases): + if "execute" not in src: + findings.append({"level": "error", "file": rel, + "msg": "Synapse service does not override an execute* method"}) + if "implements BaseServiceRequest" in src and "extends BaseServiceRequest" in src: + findings.append({"level": "error", "file": rel, + "msg": "BaseServiceRequest is an interface — use 'implements'"}) + if re.search(r"class\s+\w+\s+implements\s+BaseServiceResponse", src): + findings.append({"level": "error", "file": rel, + "msg": "BaseServiceResponse is an abstract class — use 'extends'"}) + if "@RestControllerAdvice" in src: + findings.append({"level": "warn", "file": rel, + "msg": "custom @RestControllerAdvice; Synapse auto-registers ControllerExceptionHandler"}) + + ok = not any(f["level"] == "error" for f in findings) + return {"ok": ok, "filesScanned": len(java_files), "findings": findings} + + +TOOLS = { + "list_base_classes": { + "fn": tool_list_base_classes, + "description": "List Synapse base classes/operations for a layer family (imperativeRest, reactiveRest). Returns controller, service, and the protected abstract method to override.", + "schema": { + "type": "object", + "properties": {"family": {"type": "string", "description": "Optional family filter; omit for all."}}, + }, + }, + "scaffold_operation": { + "fn": tool_scaffold_operation, + "description": "Generate copy-ready Controller + Service (+ Request/Response) skeletons for one Synapse operation.", + "schema": { + "type": "object", + "required": ["operation", "entity"], + "properties": { + "family": {"type": "string", "default": "imperativeRest"}, + "operation": {"type": "string", "description": "Operation id, e.g. create, readMono, readPoly, getMono, update, delete."}, + "entity": {"type": "string", "description": "Domain entity name, e.g. Book."}, + "packageName": {"type": "string", "default": "com.example.app"}, + "baseUrl": {"type": "string", "description": "Base request mapping, e.g. /v1/books."}, + }, + }, + }, + "validate_module": { + "fn": tool_validate_module, + "description": "Statically validate a generated module directory against Synapse conventions (base-class extension, execute* override, request/response inheritance, no custom advice).", + "schema": { + "type": "object", + "required": ["modulePath"], + "properties": {"modulePath": {"type": "string", "description": "Path to the module's source root to scan."}}, + }, + }, +} + + +# --------------------------------------------------------------------------- # +# JSON-RPC / MCP plumbing +# --------------------------------------------------------------------------- # +def _result(req_id, result): + return {"jsonrpc": "2.0", "id": req_id, "result": result} + + +def _error(req_id, code, message): + return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}} + + +def handle(catalog, msg): + method = msg.get("method") + req_id = msg.get("id") + params = msg.get("params") or {} + + if method == "initialize": + return _result(req_id, { + "protocolVersion": PROTOCOL_VERSION, + "capabilities": {"tools": {}}, + "serverInfo": SERVER_INFO, + }) + if method in ("notifications/initialized", "initialized"): + return None + if method == "tools/list": + return _result(req_id, { + "tools": [ + {"name": name, "description": spec["description"], "inputSchema": spec["schema"]} + for name, spec in TOOLS.items() + ] + }) + if method == "tools/call": + name = params.get("name") + arguments = params.get("arguments") or {} + spec = TOOLS.get(name) + if not spec: + return _error(req_id, -32601, "unknown tool: %s" % name) + try: + payload = spec["fn"](catalog, arguments) + return _result(req_id, {"content": [{"type": "text", "text": json.dumps(payload, indent=2)}]}) + except Exception as exc: # surface as tool error, not transport error + return _result(req_id, { + "isError": True, + "content": [{"type": "text", "text": "%s: %s" % (type(exc).__name__, exc)}], + }) + if req_id is not None: + return _error(req_id, -32601, "unknown method: %s" % method) + return None + + +def serve(): + catalog = load_catalog() + for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + msg = json.loads(line) + except json.JSONDecodeError: + continue + response = handle(catalog, msg) + if response is not None: + sys.stdout.write(json.dumps(response) + "\n") + sys.stdout.flush() + + +def selftest(): + catalog = load_catalog() + assert tool_list_base_classes(catalog, {})["imperativeRest"]["operations"], "list failed" + sc = tool_scaffold_operation(catalog, {"operation": "create", "entity": "Book", "packageName": "com.acme.book"}) + assert any("BookCreateController" in k for k in sc["files"]), "scaffold controller missing" + assert "executeCreate" in next(v for k, v in sc["files"].items() if "Service" in k), "override missing" + for m in ("initialize", "tools/list"): + assert handle(catalog, {"jsonrpc": "2.0", "id": 1, "method": m}), m + " failed" + call = handle(catalog, {"jsonrpc": "2.0", "id": 2, "method": "tools/call", + "params": {"name": "list_base_classes", "arguments": {}}}) + assert call["result"]["content"], "tools/call failed" + print("selftest OK") + + +if __name__ == "__main__": + if "--selftest" in sys.argv: + selftest() + else: + serve() diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..dc0a585b3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,121 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What Synapse is + +Synapse (`io.americanexpress.synapse`, Apache 2.0, by American Express) is a **multi-module Maven +library** — a lightweight foundational framework layered on top of Spring Boot. It is *not* an +application; it ships reusable abstract base classes and configuration that downstream teams depend +on to build enterprise services, clients, and data access modules with minimal boilerplate. Artifacts +are published to Maven Central. + +The framework enforces a layered, convention-over-configuration architecture: application developers +**extend a Base class for each architectural layer** rather than wiring things from scratch. The Base +classes already encode the HTTP, service, and DAO layer responsibilities (controller exception +handling, metrics interception, pagination, logging, error handling, connection pooling, etc.). + +- Java **21**, Spring Boot **3.5.x**, Spring Cloud 2025.x, JUnit 5, Mockito 5. +- Current version: 0.4.x (`-SNAPSHOT` on `develop`). + +## Build, test, run + +Use the Maven wrapper from the repo root. The root `pom.xml` (packaging `pom`) is the reactor for all modules. + +```bash +./mvnw clean package # compile + run all unit tests across every module (what CI's build job runs) +./mvnw verify # full verification incl. integration tests (run after package in CI) +./mvnw clean test # unit tests only + +# Work on a single module (and the modules it depends on): +./mvnw -pl service/synapse-service-rest -am clean test + +# A single test class / method: +./mvnw -pl data/synapse-data-postgres test -Dtest=SomeClassTest +./mvnw -pl data/synapse-data-postgres test -Dtest=SomeClassTest#someMethod +``` + +There is no single runnable app. To see the framework in action, run the reference apps under the +`*-samples` directories (e.g. `service/service-samples`, `client/client-samples`, `data/data-samples`, +`function/function-samples`), which are normal Spring Boot apps (`./mvnw -pl spring-boot:run`). + +## Module layout + +The reactor groups modules into top-level directories by architectural role. Within each, modules are +named `synapse--`: + +| Dir | Role | Key modules | +|---|---|---| +| `api` | **Foundational base classes** shared by service & client layers — split `synapse-api-rest-imperative` (Spring Web MVC) and `synapse-api-rest-reactive` (Spring WebFlux). The two mirror each other so switching paradigms is a small change. | imperative, reactive | +| `service` | Business layer — exposes APIs that start a workflow (REST/reactive/GraphQL). | rest, reactive-rest, imperative, reactive, graphql, test | +| `client` | Data-access layer — consumes external APIs. | rest, soap, graphql, test | +| `data` | Data-access layer — CRUD against datastores; each provides config + `BaseEntity` + connection pooling. | jdbc, jpa, postgres, mysql, mssql, oracle, db2, mongodb, cassandra, couchbase, redis | +| `framework` | Cross-cutting concerns. | exception, logging, api-docs, test | +| `publisher` / `subscriber` | Async pub/sub messaging. | kafka | +| `function` | Serverless/function support. | synapse-function | +| `utility` | Small standalone helpers. | utilities-common, cryptography, date, number, telephone | +| `archetype` | **Maven archetypes** that scaffold new apps from the base classes (see CI rule below). | service/client REST get/post/put/delete, reactive variants, data-postgres | + +Each role directory also has a `*-samples` module with working reference implementations — the best +place to see how the base classes are meant to be extended. + +## Architecture conventions + +- **Extend, don't reinvent.** A feature is normally implemented by subclassing the appropriate Base + class for its layer. CRUD is split into granular base classes — e.g. `BaseCreateController` / + `BaseReadController` / `BaseReadPolyController` (collections) / `BaseReadMonoController` / + `BaseUpdateController` / `BaseDeleteController`, and the matching `Base*Service` classes. Pick the + narrowest base that fits the operation rather than a catch-all. +- **Imperative vs reactive are parallel hierarchies.** When changing a base class in one paradigm, + check whether the mirror class in the other (`api-rest-imperative` ↔ `api-rest-reactive`, + `service-rest` ↔ `service-reactive-rest`) needs the same change to keep them aligned. +- Standard request/response models extend `BaseServiceRequest` / `BaseServiceResponse`; config + classes extend the layer's base config (e.g. `BaseServiceRestConfig`); data entities extend + `BaseEntity` (auditing fields managed by Spring Data). Errors flow through the two-exception model in + `framework/synapse-framework-exception` (`ApplicationServerException`, `ApplicationClientException`) + with an extensible `ErrorCode` enum. +- Modules consistently use the package layout `controller/ service/ model/ config/` (plus + `repository/`, `dto/` where relevant). Match the sibling module's structure when adding code. + +## CI / release + +- **CircleCI** (`.circleci/config.yml`) is the primary pipeline on `cimg/openjdk:21.0`: `./mvnw clean + package` then `./mvnw verify`; separate snapshot/release jobs deploy to Maven Central (Sonatype OSS) + with GPG signing. +- **Archetype validation runs in CI and will fail the build**: every archetype's + `archetype-resources/pom.xml` must keep tokenized values — `@project.version@`, + the `maven-compiler-plugin` with `@maven.compiler.plugin.version@`, and each + `archetype-metadata.xml` must declare a required `javaVersion` property defaulting to + `@maven.compiler.source@`. Never hardcode versions in archetype templates. +- GitHub Actions: `bump-version.yml` (manual `workflow_dispatch`, bumps via `versions:set`), + `codeql-analysis.yml`, `compare-dependencies.yml`; Dependabot is active (much of the commit history + is dependency patching). +- Default branch is `develop`. Releases are cut via `Release/vX.Y.Z` PRs. PRs require approval from + `@americanexpress/synapse-team` (CODEOWNERS covers all paths). + +## AI agent tooling + +This repo ships a layer that lets AI coding agents use Synapse correctly without re-deriving signatures +from source. When implementing code *with* Synapse, start here: + +- **`.synapse/catalog.json`** — authoritative machine-readable catalog of every base class: generics, + the exact `protected abstract execute*` method to override, HTTP verb/path, the controller↔service↔config + triple, `ErrorCode`s, and archetype coordinates (schema in `.synapse/catalog.schema.json`). Prefer it + over reading the Java. Key facts it encodes that surprise people: `BaseServiceRequest` is an **interface** + (`implements`); `BaseServiceResponse` is **abstract** with `id` (`extends`); you override `executeCreate`/ + `executeRead`/… not the public method; reads are POST-with-body (`/inquiry_results`, `/multiple_results`) + except get-by-id; repositories extend Spring Data `JpaRepository` directly (no Synapse base repo). +- **`.synapse/mcp/`** + **`.mcp.json`** — a zero-dependency MCP server exposing the same catalog to any + MCP client as `list_base_classes` / `scaffold_operation` / `validate_module`. Self-test: + `python3 .synapse/mcp/synapse_mcp.py --selftest`. +- **`.claude/agents/synapse-engineer.md`** + **`.claude/skills/synapse-engineer/`** — a Claude Code agent + (delegate to it for Synapse work) and a skill with the decision tables, copy-ready skeletons, test + scaffolding, and the `mvn archetype:generate` recipe. +- **`docs/agent/archunit/`** — ArchUnit convention tests (template) giving agents a deterministic verify loop. +- **`docs/agent/RECOMMENDATIONS.md`** — proposed framework-level changes for `@americanexpress/synapse-team` + (resolve the dual imperative API family, normalize per-DB property keys, generate the catalog from source, + etc.) that should not be made unilaterally because they touch the published API. + +Note there are **two** imperative families — `service.rest.*` (granular, used by all samples) and +`api.rest.imperative.*` (unified `BaseService.execute`). Default to `service.rest.*`; don't mix them. See +RECOMMENDATIONS.md #B2. diff --git a/docs/agent/RECOMMENDATIONS.md b/docs/agent/RECOMMENDATIONS.md new file mode 100644 index 000000000..61ac2bdcf --- /dev/null +++ b/docs/agent/RECOMMENDATIONS.md @@ -0,0 +1,114 @@ +# Adapting Synapse for AI agents — recommendations + +This document captures changes that make the Synapse framework reliably consumable by AI coding agents. +It is split into **(A) what was implemented on this branch** (additive, safe) and **(B) proposals that +touch the published API or build and therefore need `@americanexpress/synapse-team` review before +shipping** — because they would emit deprecation warnings or break downstream consumers on Maven Central. + +The throughline: agents fail when they must *infer* signatures and conventions from source. Every item +below either gives them machine-readable ground truth, removes ambiguity, or adds a deterministic +verify loop. + +--- + +## A. Implemented on this branch (additive) + +| # | Change | Files | +|---|---|---| +| A1 | **Machine-readable catalog** of every base class, operation, override method, generics, HTTP verb/path, config triple, ErrorCodes, archetypes. The single source of truth for the skill + MCP server. | `.synapse/catalog.json`, `.synapse/catalog.schema.json` | +| A2 | **MCP server** (Python stdlib, zero deps) exposing `list_base_classes`, `scaffold_operation`, `validate_module` to any MCP client. | `.synapse/mcp/`, `.mcp.json` | +| A3 | **Claude Code agent + skill** (`synapse-engineer`) with verified base-class decision tables and skeletons. | `.claude/agents/`, `.claude/skills/` | +| A4 | **Archetype catalog version fix** — `archetype/archetype-catalog.xml` was pinned to `0.3.32-SNAPSHOT` while the project is `0.4.28-SNAPSHOT`, so headless `archetype:generate` from the catalog resolved the wrong (often unavailable) version. Synced to `0.4.28-SNAPSHOT`. | `archetype/archetype-catalog.xml` | +| A5 | **`@implSpec` Javadoc** demonstrated on the create base classes — self-describing "extend me, override `executeCreate`, don't add `@PostMapping`" guidance agents read directly. | `BaseCreateController.java`, `BaseCreateService.java` | +| A6 | **ArchUnit convention tests** template — a deterministic verify loop. | `docs/agent/archunit/` | +| A7 | **Repo CLAUDE.md** orienting any agent to the monorepo. | `CLAUDE.md` | + +--- + +## B. Proposals requiring team review + +### B1 — Generate `catalog.json` from source (close the drift gap) + +`catalog.json` (A1) is currently hand-maintained from verified source. That is exactly the kind of file +that silently rots. Make it a build output so it can never disagree with the code: + +- Option 1 (lightweight): a small Maven plugin / `exec` step that scans the `service`, `data`, and + `client` modules for `Base*Controller`/`Base*Service`/`Base*Client` and emits `catalog.json` during + `package`, failing the build if the committed file differs. +- Option 2 (richer): an annotation processor driven by a new `@SynapseOperation(verb=…, path=…)` + meta-annotation on each base class, which both documents the class and feeds the generator. + +Until B1 lands, treat `catalog.json` as needing a refresh whenever a base class signature changes, and +keep the `bump-version` workflow honest (see B5). + +### B2 — Resolve the dual imperative API families (biggest ambiguity) + +There are **two** parallel imperative hierarchies and an agent cannot tell which to use: + +- `io.americanexpress.synapse.service.rest.*` — granular: `BaseCreateController` + `BaseCreateService` + with `executeCreate/executeRead/executeUpdate/executeDelete`. **All samples use this.** +- `io.americanexpress.synapse.api.rest.imperative.*` — unified: `BaseService.execute` / + `doExecute`, `PageResponse`, different paths (`/inquiry-results`, `/multiple-results`). + +Mixing them compiles but misbehaves. Recommendation: pick one as canonical and signal the other. + +```diff +// api/synapse-api-rest-imperative/.../controller/BaseCreateImperativeRestController.java ++/** ++ * @deprecated Experimental unified API family. Prefer the granular ++ * {@code io.americanexpress.synapse.service.rest.controller.BaseCreateController} ++ * used by all reference samples. The two families must not be mixed within one service. ++ */ ++@Deprecated(since = "0.4.28", forRemoval = false) + public abstract class BaseCreateImperativeRestController< ... > +``` + +If `api.rest.imperative` is in fact the intended future direction, do the inverse and migrate the +samples — but the repository should express a single recommended path either way. (`@Deprecated` is a +published-API signal, hence team review.) + +### B3 — Normalize per-DB datasource property keys + +Property keys differ by data module, which is a frequent silent misconfiguration for agents: + +- `synapse-data-postgres` / `-jdbc`: `spring.datasource.{url,username,password,driver-class-name}` +- `synapse-data-mysql`: `spring.mysql.datasource.{url,username,password,port}` ← non-standard prefix + +Recommendation: standardize all relational modules on the Spring Boot convention `spring.datasource.*` +(optionally `spring.datasource..*` for multi-datasource), with a one-release deprecation that reads +the old keys and logs a warning. This is a behavioral/config-contract change → team review + CHANGELOG. + +### B4 — Roll `@implSpec` out across all base classes + +A5 demonstrates the pattern on the create pair. Apply the same self-describing Javadoc (which extension +point to override, what not to annotate, how to signal errors) to every `Base*Controller`/`Base*Service` +/`Base*ReactiveController`/`Base*Client` and to `BaseServiceRequest` (state it's an interface → +`implements`) and `BaseServiceResponse` (abstract, carries `id` → `extends`). Pure documentation, but +broad — best done as one reviewed sweep. + +### B5 — Make `bump-version` also bump the archetype catalog + +`.github/workflows/bump-version.yml` runs `mvn versions:set` which does **not** update +`archetype/archetype-catalog.xml` (a plain XML list) — that is how the A4 drift happened. Add a step to +rewrite the catalog versions (and ideally `.synapse/catalog.json`'s `frameworkVersion`) as part of the +bump, or tokenize the catalog and filter it at build time. Prevents A4 from recurring. + +### B6 — Promote the ArchUnit rules into a shared module + +Move `docs/agent/archunit/SynapseConventionTest.java` into a new `synapse-architecture-test` module that +application teams depend on (test scope), so the conventions are versioned and enforced everywhere rather +than copy-pasted. Wire it into the reactor so CI compiles/runs it. + +### B7 — Scaffold tests by default + +`synapse-service-test` ships `BaseControllerTest` / `BaseControllerIT` / `Base*ControllerUnitTest`, but +the archetypes and skill don't always emit a test. Have both always generate a test extending the right +base, so agent-generated modules ship green, not just with `main` code. + +--- + +## Priority + +1. **B2** (resolve dual family) and **B1** (generate the catalog) — structural correctness-by-construction. +2. **B5**, **B7**, **B6** — cheap, high-yield, prevent regressions. +3. **B3**, **B4** — valuable normalization, larger surface. diff --git a/docs/agent/archunit/README.md b/docs/agent/archunit/README.md new file mode 100644 index 000000000..cd720b7a9 --- /dev/null +++ b/docs/agent/archunit/README.md @@ -0,0 +1,39 @@ +# Synapse convention tests (ArchUnit) + +`SynapseConventionTest.java` turns Synapse's implicit conventions into **executable, fail-fast checks**. +Its purpose is to give AI agents (and humans) a deterministic verify loop: generate code → `mvn test` +→ read the precise failure → fix. This catches the exact mistakes that are easy to make against +Synapse's two-families / `execute*` / interface-vs-abstract design. + +## What it enforces + +- `@RestController`s extend a Synapse `Base*Controller`. +- Controllers live in a `..controller..` package and **do not** declare their own `@PostMapping`/etc. + (the base owns the verb). +- Synapse services live in a `..service..` package. +- No hand-written `@RestControllerAdvice` (the layer config auto-registers `ControllerExceptionHandler`). +- `@Entity` classes extend Synapse `BaseEntity`. + +## Use in an application module + +1. Add the dependency (test scope): + ```xml + + com.tngtech.archunit + archunit-junit5 + 1.3.0 + test + + ``` +2. Copy `SynapseConventionTest.java` into the module's `src/test/java`. +3. Set `APP_PACKAGE` to your application's root package. +4. `mvn test`. + +## Status + +⚠️ **Build-unverified.** This is a ready-to-use template under `docs/agent`; it is intentionally not +yet part of the Maven reactor, so it has not been compiled/run in CI here. The recommended permanent +home is a shared, freezable rule set in a new `synapse-architecture-test` module that application +modules depend on — see `docs/agent/RECOMMENDATIONS.md` (#4). Class/method names referenced by the +rules were taken from the verified base classes (`BaseController`, `BaseService`, `BaseEntity`); if the +framework renames them, update the constants at the top of the test. diff --git a/docs/agent/archunit/SynapseConventionTest.java b/docs/agent/archunit/SynapseConventionTest.java new file mode 100644 index 000000000..217fc0cd5 --- /dev/null +++ b/docs/agent/archunit/SynapseConventionTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2020 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.americanexpress.synapse.conventions; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.lang.ArchRule; +import org.junit.jupiter.api.Test; + +/** + * ArchUnit guardrails that make Synapse conventions machine-enforceable, so AI-generated (and + * human) modules fail fast with a precise message instead of compiling into a subtly-wrong shape. + * + *

Drop this into a Synapse application module's test sources, set {@link #APP_PACKAGE} to the + * app's root package, and run {@code mvn test}. An agent runs it as its verify loop and self-corrects + * from the failure messages. + * + *

Wiring (test scope): + *

{@code
+ * 
+ *   com.tngtech.archunit
+ *   archunit-junit5
+ *   1.3.0
+ *   test
+ * 
+ * }
+ * + * NOTE: provided as a ready-to-use template under docs/agent — it is not yet wired into the reactor + * build, so it has not been compiled in CI. See docs/agent/RECOMMENDATIONS.md (#4) for promoting it + * into a shared {@code synapse-architecture-test} module. + */ +class SynapseConventionTest { + + /** Root package of the application module under test. */ + private static final String APP_PACKAGE = "com.example.app"; + + private static final String CONTROLLER_BASE = "io.americanexpress.synapse.service.rest.controller."; + private static final String SERVICE_BASE = "io.americanexpress.synapse.service.rest.service."; + + private final JavaClasses appClasses = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages(APP_PACKAGE); + + @Test + void restControllersExtendASynapseBaseController() { + ArchRule rule = classes() + .that().areAnnotatedWith("org.springframework.web.bind.annotation.RestController") + .should().beAssignableTo(CONTROLLER_BASE + "BaseController") + .because("Synapse REST endpoints must extend a Base*Controller so the verb mapping, " + + "validation, Swagger and exception handling are inherited"); + rule.check(appClasses); + } + + @Test + void controllersLiveInAControllerPackage() { + ArchRule rule = classes() + .that().areAssignableTo(CONTROLLER_BASE + "BaseController") + .should().resideInAPackage("..controller..") + .because("Synapse modules keep controllers in a 'controller' package"); + rule.check(appClasses); + } + + @Test + void controllersDoNotDeclareTheirOwnVerbMappings() { + ArchRule rule = noClasses() + .that().areAssignableTo(CONTROLLER_BASE + "BaseController") + .should().beAnnotatedWith("org.springframework.web.bind.annotation.PostMapping") + .orShould().beAnnotatedWith("org.springframework.web.bind.annotation.GetMapping") + .orShould().beAnnotatedWith("org.springframework.web.bind.annotation.PutMapping") + .orShould().beAnnotatedWith("org.springframework.web.bind.annotation.DeleteMapping") + .because("the Base*Controller already maps the HTTP verb; concrete controllers only " + + "declare @RestController and the @RequestMapping base path"); + rule.check(appClasses); + } + + @Test + void servicesLiveInAServicePackage() { + ArchRule rule = classes() + .that().areAssignableTo(SERVICE_BASE + "BaseService") + .should().resideInAPackage("..service..") + .because("Synapse modules keep services in a 'service' package"); + rule.check(appClasses); + } + + @Test + void noHandwrittenExceptionHandlers() { + ArchRule rule = noClasses() + .that().resideInAPackage(APP_PACKAGE + "..") + .should().beAnnotatedWith("org.springframework.web.bind.annotation.RestControllerAdvice") + .because("Synapse auto-registers ControllerExceptionHandler via the layer config; a " + + "custom advice silently overrides the standard ErrorResponse contract"); + rule.check(appClasses); + } + + @Test + void entitiesExtendSynapseBaseEntity() { + ArchRule rule = classes() + .that().areAnnotatedWith("jakarta.persistence.Entity") + .should().beAssignableTo("io.americanexpress.synapse.data.jpa.entity.BaseEntity") + .because("entities inherit the id + audit columns from Synapse BaseEntity"); + rule.check(appClasses); + } +} From 4268c2fb4304f9e88ac5d06620bd1d0451bc2779 Mon Sep 17 00:00:00 2001 From: jrmartinez Date: Sat, 30 May 2026 02:17:27 -0400 Subject: [PATCH 3/6] refactor(agent-tooling): make agent + skill provider-agnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the synapse-engineer agent and skill out of the Claude-specific .claude/ directory into the provider-agnostic .synapse/ home, alongside catalog.json and the MCP server. They are plain Markdown any AI runtime can load — nothing about them was Claude-specific except their location. - .claude/agents/synapse-engineer.md -> .synapse/agents/synapse-engineer.md - .claude/skills/synapse-engineer/SKILL.md -> .synapse/skills/synapse-engineer/SKILL.md - Untrack .claude/ entirely and gitignore it as local Claude Code state (a scheduled_tasks.lock harness artifact had been committed by mistake). - Update references in CLAUDE.md, RECOMMENDATIONS.md, and the MCP README; note Claude Code users can symlink .claude/{agents,skills} -> .synapse/. Co-Authored-By: Claude Opus 4.8 (1M context) --- {.claude => .synapse}/agents/synapse-engineer.md | 0 .synapse/mcp/README.md | 6 ++++-- {.claude => .synapse}/skills/synapse-engineer/SKILL.md | 0 docs/agent/RECOMMENDATIONS.md | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) rename {.claude => .synapse}/agents/synapse-engineer.md (100%) rename {.claude => .synapse}/skills/synapse-engineer/SKILL.md (100%) diff --git a/.claude/agents/synapse-engineer.md b/.synapse/agents/synapse-engineer.md similarity index 100% rename from .claude/agents/synapse-engineer.md rename to .synapse/agents/synapse-engineer.md diff --git a/.synapse/mcp/README.md b/.synapse/mcp/README.md index cc07b1282..56ee88381 100644 --- a/.synapse/mcp/README.md +++ b/.synapse/mcp/README.md @@ -4,8 +4,10 @@ A zero-dependency [Model Context Protocol](https://modelcontextprotocol.io) serv Synapse base-class catalog (`.synapse/catalog.json`) to AI coding agents, so any MCP-capable runtime generates correct, idiomatic Synapse code instead of re-deriving signatures from source. -It is the runtime-agnostic counterpart to the `synapse-engineer` Claude Code agent/skill: the skill is -for Claude Code; this server works with **any** MCP client (Claude Code, Cursor, custom agents, …). +It complements the provider-agnostic `synapse-engineer` agent/skill in `.synapse/agents/` and +`.synapse/skills/` (plain Markdown any runtime can load): those carry the human-readable recipe, while +this server exposes the same catalog as callable tools to **any** MCP client (Claude Code, Cursor, +custom agents, …). ## Requirements diff --git a/.claude/skills/synapse-engineer/SKILL.md b/.synapse/skills/synapse-engineer/SKILL.md similarity index 100% rename from .claude/skills/synapse-engineer/SKILL.md rename to .synapse/skills/synapse-engineer/SKILL.md diff --git a/docs/agent/RECOMMENDATIONS.md b/docs/agent/RECOMMENDATIONS.md index 61ac2bdcf..5e1b7398e 100644 --- a/docs/agent/RECOMMENDATIONS.md +++ b/docs/agent/RECOMMENDATIONS.md @@ -17,7 +17,7 @@ verify loop. |---|---|---| | A1 | **Machine-readable catalog** of every base class, operation, override method, generics, HTTP verb/path, config triple, ErrorCodes, archetypes. The single source of truth for the skill + MCP server. | `.synapse/catalog.json`, `.synapse/catalog.schema.json` | | A2 | **MCP server** (Python stdlib, zero deps) exposing `list_base_classes`, `scaffold_operation`, `validate_module` to any MCP client. | `.synapse/mcp/`, `.mcp.json` | -| A3 | **Claude Code agent + skill** (`synapse-engineer`) with verified base-class decision tables and skeletons. | `.claude/agents/`, `.claude/skills/` | +| A3 | **Provider-agnostic agent + skill** (`synapse-engineer`, plain Markdown) with verified base-class decision tables and skeletons; any AI runtime can load them. | `.synapse/agents/`, `.synapse/skills/` | | A4 | **Archetype catalog version fix** — `archetype/archetype-catalog.xml` was pinned to `0.3.32-SNAPSHOT` while the project is `0.4.28-SNAPSHOT`, so headless `archetype:generate` from the catalog resolved the wrong (often unavailable) version. Synced to `0.4.28-SNAPSHOT`. | `archetype/archetype-catalog.xml` | | A5 | **`@implSpec` Javadoc** demonstrated on the create base classes — self-describing "extend me, override `executeCreate`, don't add `@PostMapping`" guidance agents read directly. | `BaseCreateController.java`, `BaseCreateService.java` | | A6 | **ArchUnit convention tests** template — a deterministic verify loop. | `docs/agent/archunit/` | From 24ec8782b37d5058dc56461c3dffc5aa41addb02 Mon Sep 17 00:00:00 2001 From: jrmartinez Date: Sat, 30 May 2026 02:19:31 -0400 Subject: [PATCH 4/6] docs: point CLAUDE.md at provider-agnostic .synapse agent/skill; ignore .claude/ Finish the provider-agnostic move: CLAUDE.md's tooling list referenced the old .claude/ location; point it at .synapse/agents + .synapse/skills with a symlink hint for Claude Code users. Ignore the whole .claude/ directory as provider-specific local state. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 4 ++-- CLAUDE.md | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 2baf15dcf..70f5e4ba2 100644 --- a/.gitignore +++ b/.gitignore @@ -161,5 +161,5 @@ buildNumber.properties !/.mvn/wrapper/maven-wrapper.jar -# Claude Code harness artifact -.claude/scheduled_tasks.lock +# Claude Code local state (provider-specific; the agent/skill live in .synapse/) +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md index dc0a585b3..cd69f93c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,9 +108,11 @@ from source. When implementing code *with* Synapse, start here: - **`.synapse/mcp/`** + **`.mcp.json`** — a zero-dependency MCP server exposing the same catalog to any MCP client as `list_base_classes` / `scaffold_operation` / `validate_module`. Self-test: `python3 .synapse/mcp/synapse_mcp.py --selftest`. -- **`.claude/agents/synapse-engineer.md`** + **`.claude/skills/synapse-engineer/`** — a Claude Code agent - (delegate to it for Synapse work) and a skill with the decision tables, copy-ready skeletons, test - scaffolding, and the `mvn archetype:generate` recipe. +- **`.synapse/agents/synapse-engineer.md`** + **`.synapse/skills/synapse-engineer/`** — a provider-agnostic + agent persona and scaffolding skill (plain Markdown, no Claude-specific format) with the decision tables, + copy-ready skeletons, test scaffolding, and the `mvn archetype:generate` recipe. Any AI runtime can load + them. Claude Code users symlink them in once: `ln -s ../../.synapse/agents .claude/agents` and + `ln -s ../../.synapse/skills .claude/skills` (`.claude/` is gitignored as provider-specific local state). - **`docs/agent/archunit/`** — ArchUnit convention tests (template) giving agents a deterministic verify loop. - **`docs/agent/RECOMMENDATIONS.md`** — proposed framework-level changes for `@americanexpress/synapse-team` (resolve the dual imperative API family, normalize per-DB property keys, generate the catalog from source, From aa69f20a942148cfb1aba333faa4d9a6ef5a7cb4 Mon Sep 17 00:00:00 2001 From: jrmartinez Date: Sat, 30 May 2026 02:23:53 -0400 Subject: [PATCH 5/6] docs(context): add CONTEXT.md living context + maintenance contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add CONTEXT.md — the fast-changing "where things stand now" companion to CLAUDE.md (stable orientation) and .synapse/catalog.json (machine-readable facts): current versions, AI-tooling map, critical gotchas, open items, and an explicit Update protocol. Make the synapse-engineer agent responsible for keeping it current: add a "Maintenance duty" section to the agent and a "Step 6 — Update CONTEXT.md" to the skill, both pointing at the Update protocol. Point CLAUDE.md's AI agent tooling section at CONTEXT.md as the first thing to read. Note: this is a committed, provider-agnostic responsibility (any runtime loading the agent/skill honors it), not a harness-enforced guarantee — an agent only updates the file when it runs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .synapse/skills/synapse-engineer/SKILL.md | 14 ++++ CLAUDE.md | 3 + CONTEXT.md | 87 +++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 CONTEXT.md diff --git a/.synapse/skills/synapse-engineer/SKILL.md b/.synapse/skills/synapse-engineer/SKILL.md index 1624e5374..bd3851b00 100644 --- a/.synapse/skills/synapse-engineer/SKILL.md +++ b/.synapse/skills/synapse-engineer/SKILL.md @@ -229,3 +229,17 @@ into `src/test/java`, set `APP_PACKAGE`, add `com.tngtech.archunit:archunit-juni per operation, grouped under a shared `@RequestMapping` base path. - **Build/test**: `./mvnw -pl -am clean test` (root reactor: `./mvnw clean package`). - When in doubt, read the nearest `*-samples` module and mirror it. + +## Step 6 — Update `CONTEXT.md` (do not skip) + +`/CONTEXT.md` is the repo's living context for AI agents; keeping it current is part of every task. +Before you finish, update it in the **same change** if your work touched anything it records: + +- Base-class signature / new operation → update `.synapse/catalog.json`, then `CONTEXT.md` "Critical facts". +- Framework or catalog version bump → snapshot line + `_Last updated_` stamp. +- Tooling added/moved/removed → "AI-agent tooling map". +- Tracked item resolved/opened (PR merged, CI fixed) → "Open items". +- Branch/PR status change → "Current snapshot". + +Always refresh `_Last updated_` and append a one-line entry to the `CONTEXT.md` "Update log". The full +rules live in `CONTEXT.md` → **Update protocol**. A stale `CONTEXT.md` is a defect; fix it now, not later. diff --git a/CLAUDE.md b/CLAUDE.md index cd69f93c2..817ea740d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,6 +98,9 @@ place to see how the base classes are meant to be extended. This repo ships a layer that lets AI coding agents use Synapse correctly without re-deriving signatures from source. When implementing code *with* Synapse, start here: +- **`CONTEXT.md`** — living "where things stand now" context (current versions, tooling map, open items, + critical gotchas). Read it first to get oriented; the `synapse-engineer` agent keeps it current per its + [Update protocol](CONTEXT.md#update-protocol). - **`.synapse/catalog.json`** — authoritative machine-readable catalog of every base class: generics, the exact `protected abstract execute*` method to override, HTTP verb/path, the controller↔service↔config triple, `ErrorCode`s, and archetype coordinates (schema in `.synapse/catalog.schema.json`). Prefer it diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 000000000..b57420a0a --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,87 @@ +# CONTEXT.md + +> **Living context for AI agents working on Synapse.** This is the fast-changing "where things +> stand right now" companion to `CLAUDE.md` (stable orientation) and `.synapse/catalog.json` +> (machine-readable base-class facts). Read this first to get current; update it when you change +> anything it describes. +> +> **Maintained by the `synapse-engineer` agent.** Keeping this file current is part of that agent's +> job — see [Update protocol](#update-protocol). Any agent or human that changes the framework +> surface, the tooling, or the open-items list below should update this file in the same change. + +_Last updated: 2026-05-30 · reflects catalog `catalogVersion: 1` · framework `0.4.28-SNAPSHOT`_ + +--- + +## Current snapshot + +- **Framework:** `io.americanexpress.synapse` `0.4.28-SNAPSHOT`, Java 21, Spring Boot 3.5.x, `jakarta.*`. +- **Default branch:** `develop`. **Active branch:** `chore/claude-md-and-dev` → **PR #481** (base `develop`). +- **CI status:** CodeQL is **red due to a GitHub Actions billing lock** on the org (jobs never start; + not a code problem). Maven/CircleCI build not verifiable in the dev sandbox (offline BOMs). See + [Open items](#open-items). +- **What PR #481 adds:** the AI-agent adaptation layer (this file, `.synapse/` tooling, `docs/agent/`), + the archetype-catalog version fix, and `@implSpec` Javadoc on the create base classes. + +## AI-agent tooling map (what exists, where) + +| Path | Role | +|---|---| +| `.synapse/catalog.json` (+ `catalog.schema.json`) | Authoritative machine-readable base-class catalog. Source of truth. | +| `.synapse/mcp/` (+ `/.mcp.json`) | Zero-dep MCP server: `list_base_classes` / `scaffold_operation` / `validate_module`. | +| `.synapse/agents/synapse-engineer.md` | Provider-agnostic agent persona for building Synapse code. | +| `.synapse/skills/synapse-engineer/SKILL.md` | Step-by-step scaffolding recipe + skeletons. | +| `docs/agent/RECOMMENDATIONS.md` | Framework-level proposals needing `@americanexpress/synapse-team` review. | +| `docs/agent/archunit/` | ArchUnit convention tests (template; build-unverified). | +| `CLAUDE.md` | Stable repo orientation (build, architecture, CI, conventions). | + +All AI tooling is under provider-agnostic `.synapse/` / `docs/agent/`. `.claude/` is gitignored +(Claude Code users symlink `.claude/{agents,skills}` → `../../.synapse/{agents,skills}`). + +## Critical facts agents must not get wrong + +(Condensed; full detail in `.synapse/catalog.json`.) + +- Extend a `Base*Controller` + `Base*Service` pair; override the **`protected abstract execute*`** method + (`executeCreate`/`executeRead`/`executeUpdate`/`executeDelete`), **not** the public method. +- `BaseServiceRequest` is an **interface** (`implements`); `BaseServiceResponse` is an **abstract class** + with a `String id` (`extends`). +- Reads are **POST with a body** at `/inquiry_results` (one) and `/multiple_results` (many); + only get-by-id is a real `GET /{id}`. `BaseReadPolyService.executeRead` returns `Page`. +- Repositories extend Spring Data `JpaRepository` directly — **no** Synapse base repo. +- App config `@Import`s the layer config (`ServiceRestConfig`) which **auto-registers** the + `ControllerExceptionHandler` — never write your own `@RestControllerAdvice`. +- Errors: `ApplicationClientException(devMsg, ErrorCode, String...)` (4XX) / + `ApplicationServerException(Throwable)` (5XX). +- **Two imperative families exist** — prefer `service.rest.*` (granular, used by all samples) over + `api.rest.imperative.*` (unified `BaseService.execute`). Don't mix them. (See RECOMMENDATIONS #B2.) + +## Open items + +| Item | Status | Where | +|---|---|---| +| CodeQL / Actions billing lock | **Blocked** — needs org admin to clear billing; then re-run checks | PR #481 checks | +| Stale CodeQL workflow (`checkout@v2`, `codeql-action@v1`) | Proposed follow-up PR (`v4`/`v3` + `setup-java@v4`) | `.github/workflows/codeql-analysis.yml` | +| Resolve dual imperative API family | Proposed, needs team decision | RECOMMENDATIONS #B2 | +| Normalize per-DB datasource property keys | Proposed, needs team decision | RECOMMENDATIONS #B3 | +| Generate `catalog.json` from source (anti-drift) | Proposed | RECOMMENDATIONS #B1 | +| `bump-version.yml` should also bump archetype catalog + catalog.json | Proposed | RECOMMENDATIONS #B5 | +| ArchUnit rules → shared `synapse-architecture-test` module | Proposed | RECOMMENDATIONS #B6 | +| Local `./mvnw … compile` to confirm `@implSpec` edits | Pending (sandbox is offline) | `service/synapse-service-rest` | + +## Update protocol + +Keep this file true. When a change touches any of the following, update the relevant section **in the +same commit** and refresh the `_Last updated_` line: + +1. **A base class signature / new operation** → update `.synapse/catalog.json` first, then the + "Critical facts" section here if a headline fact changed; bump `catalogVersion` if the contract changed. +2. **Framework or catalog version** → update the snapshot line and `_Last updated_`. +3. **Tooling added/moved/removed** → update the "AI-agent tooling map". +4. **An open item resolved or added** (PR merged, CI fixed, proposal accepted) → update "Open items". +5. **Branch/PR status changes** → update "Current snapshot". + +Add a one-line entry to the log below for each material update. + +### Update log +- 2026-05-30 — Created. Established the AI-agent tooling layer; PR #481 open; CodeQL blocked on billing. From e3d118af66d1200a4bd3c43293d293decc7f0cdd Mon Sep 17 00:00:00 2001 From: jrmartinez Date: Sat, 30 May 2026 02:25:01 -0400 Subject: [PATCH 6/6] docs(agent): add CONTEXT.md maintenance duty to synapse-engineer agent The agent persona was missing the maintenance contract that the skill, CLAUDE.md, and CONTEXT.md already reference (a prior edit silently failed to match). Add the 'Maintenance duty' section so the agent itself owns keeping CONTEXT.md current. Co-Authored-By: Claude Opus 4.8 (1M context) --- .synapse/agents/synapse-engineer.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.synapse/agents/synapse-engineer.md b/.synapse/agents/synapse-engineer.md index 9527a5457..01a617b84 100644 --- a/.synapse/agents/synapse-engineer.md +++ b/.synapse/agents/synapse-engineer.md @@ -136,5 +136,21 @@ service: `synapse-service-rest`, `-reactive-rest`, `-graphql`, `-test` · client 6. Verify: `./mvnw -pl -am clean test` (root reactor: `./mvnw clean package`). 7. When fuzzy, read the nearest `*-samples` module and copy its structure. +## Maintenance duty — keep `CONTEXT.md` current + +You own the freshness of **`/CONTEXT.md`** (the repo's living context for AI agents). Before finishing +any task, check whether your change affects what it records and, if so, update it **in the same change**: + +- Changed a base-class signature / added an operation → update `.synapse/catalog.json` first, then + `CONTEXT.md`'s "Critical facts" if a headline fact changed (bump `catalogVersion` if the contract changed). +- Bumped the framework or catalog version → update the snapshot line and the `_Last updated_` stamp. +- Added/moved/removed tooling → update the "AI-agent tooling map". +- Resolved or opened a tracked item (PR merged, CI fixed, proposal accepted) → update "Open items". +- Branch/PR status changed → update "Current snapshot". + +Always refresh the `_Last updated_` line and append a one-line entry to the `CONTEXT.md` "Update log". +Follow the Update protocol in `CONTEXT.md` verbatim. Treat a stale `CONTEXT.md` as a defect — fix it as +part of the work, don't defer it. + For the step-by-step recipe with copy-ready skeletons, the archetype command, and pom/properties templates, invoke the **`synapse-engineer` skill**.