diff --git a/README.md b/README.md index 4d4015b..78ddf4c 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,128 @@ # code-generation-rules -Shared engineering rules and agent tooling for the organization, mounted into -projects as a git submodule. +Shared agent guidance and deterministic coding hooks for Vality services. -The repository carries three things: +The repository is mounted into a consuming project as `.agent-rules`. The persistent +agent context stays intentionally small: durable cross-project invariants plus routes +to more specific guidance. Detailed conventions are read only for tasks that need +them. -- `rules/` — common rules and opt-in profiles, as plain markdown. Single source - of truth. -- `hooks/` — scripts wired into agent lifecycle events (Claude Code and Codex). -- `install.sh` / `check.sh` — wire the above into a consuming project, idempotently. +Repository-local code, tests, build configuration, `AGENTS.md`, and `CLAUDE.md` remain +the primary evidence for local architecture and implementation patterns. External +contract skills may define a different authoritative source for the wire contract +itself. -## What belongs here +## Guidance model -Top-level files in `rules/` hold rules that apply to the whole organization. -Rules shared by one family of services live in `rules/profiles/` and are selected -by the consuming project. Anything tied to one service — its build quirks and -local conventions — stays in that service's own `AGENTS.md` / `CLAUDE.md`, -outside the synced block. +Use the narrowest mechanism that reliably owns a rule: -## Adding to a project +- `guidance/core.md` — durable organization-wide invariants and routing only; +- `references/` — task-specific checklists and defaults loaded when relevant; +- `skills/provider-adapter/` — the reusable workflow for external provider adapters; +- repository-local code/instructions — service-specific architecture and conventions; +- hooks, generators, linters, tests, and CI — deterministic checks that should not be + implemented mainly through prose. + +A rule should not become always-on merely because it is good engineering advice. Keep +it persistent when omitting it repeatedly causes a material wrong choice across the +whole scope. Otherwise put it in a narrower reference/skill, leave it to repository +evidence, or enforce it with tooling. + +## Repository layout + +- `guidance/core.md` — compact guidance installed into persistent agent context. +- `references/` — focused guidance for database, generated contracts, OpenAPI, and + cross-layer code decisions. +- `skills/provider-adapter/` — contract-first provider workflow and narrow references. +- `hooks/` — deterministic lifecycle checks; currently Kotlin formatting/linting. +- `agents/` — Claude Code and Codex hook fragments. +- `install.sh` — installs or refreshes the managed agent block and hooks. +- `check.sh` — verifies that a consuming repository matches the pinned submodule. +- `ci/github-actions/agent-rules-drift.yml` — optional consuming-repository drift check. + +## Install + +From the consuming repository: ```bash git submodule add .agent-rules ./.agent-rules/install.sh ``` -`install.sh` is idempotent and touches only what it owns: +The installer is idempotent. It owns only: -- registers the Kotlin format hook in `.claude/settings.json` and `.codex/hooks.json` -- writes `@`-imports of the selected rule files into `CLAUDE.md` -- syncs the rule text into `AGENTS.md` between `` and - `` +- content between `` and `` in + `AGENTS.md` and `CLAUDE.md`; +- hook entries containing this repository's `format-kotlin.sh` marker in + `.codex/hooks.json` and `.claude/settings.json`. -Everything outside those markers is yours and is never rewritten. +Content outside those owned areas is preserved. On Codex, a one-time migration may +move legacy root-level `Stop`/`SubagentStop` groups into the current top-level `hooks` +object; unrelated handlers inside those groups are preserved. -Commit the resulting changes together with the submodule pointer. +Requirements: `git` and `jq`. The Kotlin hook uses Maven only when the target project +contains `ktlint-maven-plugin`. -## Rule profiles +## Legacy profile compatibility -Without configuration, `install.sh` applies only the common rules. A consuming -project can commit `.agent-rules-profile` with one of these values: +Guidance routing is task-driven rather than profile-driven. New consuming repositories +do not need `.agent-rules-profile`. -- `common` — common rules only; -- `openapi` — common rules and contract-first OpenAPI conventions; -- `adapter` — common rules and external-adapter conventions. +For compatibility with repositories already using the previous interface, +`.agent-rules-profile` and `--profile common|openapi|adapter` are still accepted by +`install.sh` / `check.sh`, but those values no longer remove routes from the managed +agent block. This lets existing CI and project configuration migrate without changing +which task-specific guidance is available. -For example: +## Agent routing -```text -openapi -``` +The managed block does not copy `references/` or `skills/` into every task. It routes +the coding agent: -The profile can be overridden for a single command. The same option is accepted -by `check.sh`: +- schema/migration/repository/transaction work → `references/database.md`; +- generated-source or Protobuf work → `references/code-generation.md`; +- cross-layer architecture/client/converter decisions not settled by local code → + `references/code-conventions.md`; +- OpenAPI contract/generation work → `references/openapi.md`; +- changes that cross an external provider boundary or provider flow — request/response, + authentication, callbacks, polling, provider error mapping, provider integration + tests, or adapting another provider → `skills/provider-adapter/SKILL.md`. -```bash -./.agent-rules/install.sh --profile openapi -./.agent-rules/check.sh --profile openapi -``` +Ordinary internal bugs/refactors do not load the provider skill merely because they are +in an adapter repository. The provider skill distinguishes the provider contract from +local implementation evidence, starts from the closest working local flow, and loads +only references needed for the concrete operation. Routes may compose: for example, an +adapter change that also modifies OpenAPI or persistence can load both relevant paths. -The command-line value takes precedence over `.agent-rules-profile`. Unknown or -empty profile values are rejected. +## Hooks -## Updating +`hooks/format-kotlin.sh` runs at `Stop`/`SubagentStop` when the current agent surface +supports project hooks. When Kotlin changed and the project uses +`ktlint-maven-plugin`, it runs formatting and checking and can return remaining +violations to the agent. + +The Codex fragment follows the current `.codex/hooks.json` schema with a top-level +`hooks` object. `install.sh` also removes legacy root-level `Stop`/`SubagentStop` +entries previously installed by this repository before writing the current shape. + +## Update and drift check ```bash git submodule update --remote .agent-rules ./.agent-rules/install.sh +git diff +./.agent-rules/check.sh ``` -Review the diff, then commit. The bump is explicit per project — rules never -change under a project without a commit in it. - -## Keeping projects honest - -`check.sh` runs `install.sh --check` with the configured profile: it writes -nothing and exits non-zero when a project has drifted from the submodule it pins. -Wire it into CI with -`ci/github-actions/agent-rules-drift.yml` — note the `submodules: true` on -checkout, without it the check runs against an empty directory. - -## The Kotlin format hook - -`hooks/format-kotlin.sh` runs on the agent's `Stop` event — once per turn, after -the code is generated, in both Claude Code and Codex. - -When the turn touched Kotlin, it runs `ktlint:format` and `ktlint:check` in one -maven invocation. Both goals are needed: `format` fixes what it can but exits -successfully while staying silent about the rest, so only `check` surfaces the -violations that need a human-shaped fix. Those are handed back to the agent, -which then has to correct them before the turn can end. +Review and commit the submodule pointer together with generated managed-block/hook +changes. `check.sh` writes nothing and exits non-zero on drift. -It is deliberately quiet and cheap: with no changed `.kt`/`.kts` files, or in a -project with no ktlint, it exits in well under a tenth of a second without -starting a JVM. +The optional GitHub Actions example is +`ci/github-actions/agent-rules-drift.yml`; consuming workflows must checkout +submodules. -Note that `ktlint:format` covers the whole module, not just the changed files. -In a project where CI already enforces `ktlint:check`, everything committed is -formatted anyway, so this is a no-op on untouched code. +## Project-specific environment -If a project needs specific environment to run its build (a particular -`JAVA_HOME`, a locale), put it in `.agent-rules.env` in the project root — the -hook sources it when present. That file belongs to the project, not here. +If the Kotlin hook needs project-local environment such as `JAVA_HOME`, the consuming +repository may provide `.agent-rules.env` at its root. The hook sources that file when +present. Do not commit secrets in that file. diff --git a/agents/codex/hooks.json b/agents/codex/hooks.json index 137390b..3671a5c 100644 --- a/agents/codex/hooks.json +++ b/agents/codex/hooks.json @@ -1,26 +1,28 @@ { - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "if [ -x .agent-rules/hooks/format-kotlin.sh ]; then exec .agent-rules/hooks/format-kotlin.sh; fi", - "statusMessage": "Formatting Kotlin sources", - "timeout": 300 - } - ] - } - ], - "SubagentStop": [ - { - "hooks": [ - { - "type": "command", - "command": "if [ -x .agent-rules/hooks/format-kotlin.sh ]; then exec .agent-rules/hooks/format-kotlin.sh; fi", - "statusMessage": "Formatting Kotlin sources", - "timeout": 300 - } - ] - } - ] + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "if [ -x .agent-rules/hooks/format-kotlin.sh ]; then exec .agent-rules/hooks/format-kotlin.sh; fi", + "statusMessage": "Formatting Kotlin sources", + "timeout": 300 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "if [ -x .agent-rules/hooks/format-kotlin.sh ]; then exec .agent-rules/hooks/format-kotlin.sh; fi", + "statusMessage": "Formatting Kotlin sources", + "timeout": 300 + } + ] + } + ] + } } diff --git a/guidance/core.md b/guidance/core.md new file mode 100644 index 0000000..40fec87 --- /dev/null +++ b/guidance/core.md @@ -0,0 +1,23 @@ +# Shared agent guidance + +Treat current repository code, tests, build configuration, and service-specific +instructions outside this managed block as the primary evidence for this project's +architecture, tooling, and implementation patterns. + +Keep these organization-level invariants: + +- Do not edit generated sources. Change their source contract or generator instead. +- Preserve published wire/storage compatibility unless the task explicitly requires a + coordinated breaking change or migration. +- Never hardcode, expose, or log credentials, tokens, personal data, PANs, + bank-account data, or other payment-sensitive values. +- Prefer an established local implementation pattern over introducing a new framework, + package layout, abstraction, or dependency without a concrete need. +- Do not promote an implementation choice observed in one service into an + organization-wide rule without stronger repository or contract evidence. +- Run the project's existing focused tests, linters, generators, and compatibility + checks that cover the changed area. +- Do not load or apply detailed guidance that is unrelated to the current task. + +Detailed references are defaults and checklists, not permission to override stronger +repository evidence or explicit task requirements. diff --git a/install.sh b/install.sh index c377f2f..92084a9 100755 --- a/install.sh +++ b/install.sh @@ -1,24 +1,18 @@ #!/usr/bin/env bash -# Wires the shared rules into the project that mounts this submodule. -# -# ./.agent-rules/install.sh apply common rules -# ./.agent-rules/install.sh --profile openapi apply a rule profile -# ./.agent-rules/install.sh --check [--profile ...] report drift, write nothing -# -# Everything here is idempotent and owns a bounded piece of each file: the hook -# entries it registered, and the text between the agent-rules markers. Whatever -# else the project keeps in CLAUDE.md, AGENTS.md or its agent settings is left -# untouched. +# Installs compact agent routing and deterministic hooks into a consuming repository. +# Detailed references and skills stay in .agent-rules and are loaded only when needed. set -uo pipefail -RULES_DIR="$(cd -- "$(dirname -- "$0")" && pwd)" +RULES_DIR="$(cd -- "$(dirname -- "$0")" && pwd -P)" BEGIN_MARKER="" END_MARKER="" HOOK_MARKER="format-kotlin.sh" CHECK_ONLY=0 DRIFT=0 +# Legacy profile selectors remain accepted so existing consumers and CI do not break. +# They no longer filter guidance; task routing is always installed. PROFILE_OVERRIDE="" usage() { @@ -63,7 +57,7 @@ PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || { case "$RULES_DIR" in "$PROJECT_ROOT"/*) ;; *) - printf 'agent-rules: %s is not inside %s — run install.sh from the project that mounts it\n' \ + printf 'agent-rules: %s is not inside %s — mount it inside the consuming project\n' \ "$RULES_DIR" "$PROJECT_ROOT" >&2 exit 1 ;; @@ -73,9 +67,8 @@ PROFILE="common" PROFILE_FILE="$PROJECT_ROOT/.agent-rules-profile" if [ -f "$PROFILE_FILE" ]; then - PROFILE="$(cat "$PROFILE_FILE")" + PROFILE="$(tr -d '\r\n' <"$PROFILE_FILE")" fi - if [ -n "$PROFILE_OVERRIDE" ]; then PROFILE="$PROFILE_OVERRIDE" fi @@ -102,7 +95,6 @@ report() { fi } -# Writes $2 to $1 unless --check, in which case it only records the difference. apply_file() { local path="$1" desired="$2" label="$3" @@ -117,19 +109,51 @@ apply_file() { printf '%s\n' "$desired" >"$path" } -# --- agent hook registration ------------------------------------------------- +normalize_legacy_codex_hooks() { + local base="$1" + + # Older revisions of this repository wrote Stop/SubagentStop at the JSON root. + # Current Codex expects events under the top-level "hooks" object. Move the + # complete legacy groups so unrelated user hooks are preserved as well. + printf '%s' "$base" | jq ' + reduce ["Stop", "SubagentStop"][] as $event ( + .; + if (.[$event]? | type) == "array" then + .hooks = (.hooks // {}) + | .hooks[$event] = ((.hooks[$event] // []) + .[$event]) + | del(.[$event]) + else + . + end + ) + ' +} -# Drops any previously registered entry for our hook, then appends the current -# one. That makes the merge both idempotent and self-healing when a project has -# edited the command by hand. merge_hooks() { - local target="$1" fragment="$2" root_path="$3" label="$4" + local target="$1" fragment="$2" root_path="$3" label="$4" migrate_legacy="${5:-0}" local base desired base='{}' [ -f "$target" ] && base="$(cat "$target")" + if [ "$migrate_legacy" -eq 1 ]; then + base="$(normalize_legacy_codex_hooks "$base")" || { + printf 'agent-rules: failed to normalize legacy hooks in %s\n' "$target" >&2 + exit 1 + } + fi + desired="$(printf '%s' "$base" | jq --slurpfile frag "$fragment" --arg marker "$HOOK_MARKER" --arg root "$root_path" ' + def without_marker($marker): + map( + if (.hooks? | type) == "array" then + .hooks |= map(select((((.command // "") | contains($marker))) | not)) + else + . + end + ) + | map(select((.hooks? // []) | length > 0)); + ($frag[0] | getpath($root | split(".") | map(select(length > 0)))) as $events | reduce ($events | keys[]) as $event ( .; @@ -137,7 +161,7 @@ merge_hooks() { ($root | split(".") | map(select(length > 0))) + [$event]; ( (getpath(($root | split(".") | map(select(length > 0))) + [$event]) // []) - | map(select([.hooks[]?.command // ""] | map(contains($marker)) | any | not)) + | without_marker($marker) ) + $events[$event] ) @@ -147,7 +171,6 @@ merge_hooks() { exit 1 } - # Compare normalized so that key order and indentation never look like drift. if [ -f "$target" ] && [ "$(jq -S . "$target" 2>/dev/null)" = "$(printf '%s' "$desired" | jq -S .)" ]; then return 0 @@ -160,48 +183,59 @@ merge_hooks() { printf '%s' "$desired" | jq . >"$target" } -# --- markdown block sync ----------------------------------------------------- +routing_body() { + cat "$RULES_DIR/guidance/core.md" -rule_files() { - find "$RULES_DIR/rules" -maxdepth 1 -name '*.md' -not -name 'index.md' | sort + cat <<'EOF' - case "$PROFILE" in - openapi) - printf '%s\n' "$RULES_DIR/rules/profiles/openapi.md" - ;; - adapter) - printf '%s\n' "$RULES_DIR/rules/profiles/adapter.md" - ;; - esac -} +## Load targeted guidance only when relevant -# Path of a rule file relative to the project root, e.g. .agent-rules/rules/x.md -rule_rel_path() { - printf '%s' "${1#"$PROJECT_ROOT"/}" +- Database/schema/repository/transaction work: read `.agent-rules/references/database.md`. +- Generated-source or Protobuf work: read `.agent-rules/references/code-generation.md`. +- OpenAPI contract/generation work: read `.agent-rules/references/openapi.md`. +- Cross-layer architecture/client/converter decisions not settled by local code: consult + `.agent-rules/references/code-conventions.md`. +- When a task changes an external provider boundary or provider flow — request/response, + authentication, callback, polling, provider error mapping, provider integration tests, + or adaptation of another provider — read `.agent-rules/skills/provider-adapter/SKILL.md` + and follow its progressive-disclosure workflow. Do not load that skill for ordinary + internal bugs or refactors that do not cross the provider boundary. +EOF } -claude_block_body() { - printf '%s\n' "Shared organization rules, synced by .agent-rules/install.sh." - printf '\n' - rule_files | while IFS= read -r file; do - printf '@%s\n' "$(rule_rel_path "$file")" - done -} - -agents_block_body() { - printf '%s\n' "Shared organization rules, synced by .agent-rules/install.sh. Do not edit by hand." - rule_files | while IFS= read -r file; do - printf '\n' - cat "$file" - done +validate_managed_block() { + local path="$1" + + [ -f "$path" ] || return 0 + + awk -v begin="$BEGIN_MARKER" -v end="$END_MARKER" ' + index($0, begin) == 1 { + begin_count++ + if (end_count > 0) invalid = 1 + next + } + index($0, end) == 1 { + end_count++ + if (begin_count == 0) invalid = 1 + } + END { + if (begin_count == 0 && end_count == 0) exit 0 + if (begin_count == 1 && end_count == 1 && !invalid) exit 0 + exit 1 + } + ' "$path" || { + printf 'agent-rules: malformed managed block in %s; expected one BEGIN marker followed by one END marker\n' \ + "$path" >&2 + return 1 + } } -# Replaces the marked block in $1 with $2, keeping everything outside it. Creates -# the file, or appends the block, when either is missing. render_with_block() { local path="$1" body="$2" local block existing + validate_managed_block "$path" || return 1 + block="$BEGIN_MARKER $body $END_MARKER" @@ -225,7 +259,17 @@ $END_MARKER" ' "$path" } -# --- run --------------------------------------------------------------------- +sync_markdown_file() { + local path="$1" body="$2" label="$3" desired + + desired="$(render_with_block "$path" "$body")" || exit 1 + apply_file "$path" "$desired" "$label" +} + +# Refuse malformed managed blocks before touching any project file. This keeps a +# missing/duplicated marker from turning a bounded sync into destructive truncation. +validate_managed_block "$PROJECT_ROOT/CLAUDE.md" || exit 1 +validate_managed_block "$PROJECT_ROOT/AGENTS.md" || exit 1 merge_hooks "$PROJECT_ROOT/.claude/settings.json" \ "$RULES_DIR/agents/claude/settings.hooks.json" \ @@ -234,16 +278,14 @@ merge_hooks "$PROJECT_ROOT/.claude/settings.json" \ merge_hooks "$PROJECT_ROOT/.codex/hooks.json" \ "$RULES_DIR/agents/codex/hooks.json" \ - "" \ - ".codex/hooks.json" + "hooks" \ + ".codex/hooks.json" \ + 1 -apply_file "$PROJECT_ROOT/CLAUDE.md" \ - "$(render_with_block "$PROJECT_ROOT/CLAUDE.md" "$(claude_block_body)")" \ - "CLAUDE.md" +BODY="$(routing_body)" -apply_file "$PROJECT_ROOT/AGENTS.md" \ - "$(render_with_block "$PROJECT_ROOT/AGENTS.md" "$(agents_block_body)")" \ - "AGENTS.md" +sync_markdown_file "$PROJECT_ROOT/CLAUDE.md" "$BODY" "CLAUDE.md" +sync_markdown_file "$PROJECT_ROOT/AGENTS.md" "$BODY" "AGENTS.md" if [ "$CHECK_ONLY" -eq 1 ] && [ "$DRIFT" -eq 1 ]; then printf '\nagent-rules: project has drifted from .agent-rules — run ./.agent-rules/install.sh\n' >&2 diff --git a/references/code-conventions.md b/references/code-conventions.md new file mode 100644 index 0000000..3609265 --- /dev/null +++ b/references/code-conventions.md @@ -0,0 +1,69 @@ +# Code conventions reference + +Use this reference when a task materially changes application architecture, transport +boundaries, conversion, external clients, configuration, or replaces an existing +cross-layer mechanism. It is not part of the always-on prompt. + +## First inspect the repository + +Before applying any convention below, identify the nearest production implementation +of the same responsibility. Prefer the project's established framework and package +structure when it is coherent. + +Do not introduce Spring `Converter`, Lombok, a fixed package taxonomy, a new +handler/service split, or any other abstraction solely because it appears in this +reference. + +When replacing or removing a library or mechanism, inventory its material behavior at +the boundary before deleting it. Preserve or intentionally replace required behavior +and verify the behavior that could otherwise disappear behind a successful compile or +a narrower test. + +## Durable boundaries + +These are useful defaults when the local codebase does not provide stronger evidence: + +- Transport resources own protocol validation and protocol error mapping, not business + orchestration. +- Services coordinate business scenarios. Transaction boundaries belong at the + narrowest layer that can complete the business transition atomically; do not keep + remote calls or unrelated work inside a database transaction. +- Persistence access stays behind repositories or an equivalent persistence boundary. +- External systems stay behind local clients/interfaces so generated stubs and retry + mechanics do not leak into unrelated business code. +- Converters map data; they should not write to the database or perform remote calls. +- Typed DTOs are preferred for stable external contracts over unstructured maps. +- Preserve the distinction between an omitted value and an explicit empty/default + value whenever the contract gives those states different meaning. +- Collection conversion should not introduce N+1 remote or database calls. +- Request/correlation identifiers should be propagated across transport boundaries + when the existing system supports them. +- Expected domain failures should remain distinguishable at the transport boundary. + +## Configuration and clients + +Use the project's existing configuration mechanism. In Spring projects, typed +`@ConfigurationProperties` is generally preferable to scattered string lookups. + +Keep transport, mapping, and business-scenario responsibilities separable. Retry, +backoff, authentication, base URL resolution, and serialization should have one +obvious owner rather than being duplicated across handlers. + +Every remote call should have a finite timeout. Retries should be bounded and used +only when repeating the operation is safe or protected by an idempotency mechanism. + +## Logging + +Use the project's logging library and style. Preserve identifiers useful for +diagnostics, but do not log sensitive payloads. Large external payloads should not be +promoted to normal INFO-level logging merely for convenience. + +## Testing + +Choose the narrowest test that exercises the changed responsibility. Prefer observable +events over fixed sleeps in asynchronous tests. For external clients, assert both the +outbound request and the mapped result when that behavior is part of the change. + +A fixture, mock, or test name is not evidence by itself. Confirm that the assertion +observes the material path the system actually uses and that the test would fail for +the regression it is meant to prevent. diff --git a/references/code-generation.md b/references/code-generation.md new file mode 100644 index 0000000..22e4cdd --- /dev/null +++ b/references/code-generation.md @@ -0,0 +1,25 @@ +# Generated contracts and Protobuf reference + +Read this only for generated sources, Protobuf, OpenAPI generation, or another +published generated contract. + +## Generated code + +- Never edit generated output as the source of a fix. +- Change the source schema/specification/generator and regenerate. +- Generation should be reproducible in CI from repository state. +- Review generated diffs for unrelated churn. + +## Protobuf + +- Published field numbers are immutable. +- Reserve removed field numbers and names. +- Preserve wire-compatible field types. +- Use explicit field presence when an omitted scalar must remain distinguishable + from its default value. +- Use the repository's established package/versioning strategy. +- When consumers may receive new enum values, `oneof` variants, or messages, add or + update compatibility tests and tolerant handling as appropriate. + +If a change is intentionally breaking, make the break explicit and coordinate the +producer/consumer migration rather than silently weakening these rules. diff --git a/references/database.md b/references/database.md new file mode 100644 index 0000000..83f426b --- /dev/null +++ b/references/database.md @@ -0,0 +1,67 @@ +# Database change reference + +Read this only for schema, migration, repository, persistence, or transactional-state +work. + +This file intentionally separates compatibility/integrity concerns from historical +implementation preferences. Inspect the target repository before choosing a database +style. + +## Hard compatibility and integrity checks + +- Applied migrations are immutable; introduce a new migration for a schema change. +- Trace a schema change through migration, generated model (if any), write path, read + path, conversion, and tests. +- Keep database writes that form one business transition in one transaction. Place + the transaction at the narrowest layer that can complete the transition atomically, + and do not keep remote calls or unrelated work inside it. +- Protect real business keys against concurrent races with an appropriate database + constraint and domain handling. +- Do not build SQL by concatenating untrusted values. +- Batch related reads when a collection path would otherwise produce N+1 queries. +- Paginated reads need deterministic ordering with a unique tie-breaker; use the + repository's established pagination strategy. +- Verify state-changing batch operations when a partial update would violate the + business transition. +- When a state transition can be replayed, preserve idempotent replay and reject a + transition that conflicts with an already-final state according to the domain model. + +## Transactional event delivery, when present + +If the repository uses an outbox or database-backed delivery queue, preserve the +pattern's correctness properties rather than copying one implementation shape: + +- commit the domain write and event record atomically; +- keep enough immutable event identity/payload to retry the same logical delivery; +- do not hold a database row lock while a remote call is in flight; +- make duplicate/ambiguous delivery safe through the project's idempotency mechanism; +- keep retries bounded/backed off and exhausted delivery observable. + +## Project-specific choices: verify before applying + +The following may be valid conventions in existing Vality repositories, but they are +not universal database truths. Preserve them where the target project already relies +on them; do not introduce them into a new architecture merely because they are listed +here: + +- Flyway location and migration naming; +- jOOQ generation layout; +- use or absence of foreign keys; +- soft-delete lifecycle; +- `TIMESTAMP WITHOUT TIME ZONE` + `LocalDateTime` interpreted as UTC; +- particular upsert patterns; +- ShedLock/Flyway generator exclusions. + +If the task is specifically to define an organization-wide database policy, decide +these points explicitly and enforce deterministic parts with migration/CI tooling +rather than relying only on agent prose. + +## Testing + +For PostgreSQL-specific behavior, migrations, locking, conflicts, or transaction +semantics, prefer a real PostgreSQL integration test (for example Testcontainers) +over a substitute database. + +Cover the failure mode introduced by the change: conflict, rollback, concurrent claim, +idempotent replay, empty result, pagination boundary, or retry exhaustion as +applicable. diff --git a/references/openapi.md b/references/openapi.md new file mode 100644 index 0000000..0fcf13a --- /dev/null +++ b/references/openapi.md @@ -0,0 +1,19 @@ +# OpenAPI contract reference + +Read this only when the task changes an OpenAPI document, generated API artifact, or +contract compatibility. + +- Treat the repository's root OpenAPI document as the contract source, not generated + server/client code. +- Preserve stable `operationId` values unless the change intentionally coordinates a + breaking API migration. +- Reuse shared parameters, error schemas, security schemes, and components rather than + copying equivalent definitions. +- Make request/response constraints explicit when they are part of the public + contract: required/nullable state, formats, enums, bounds, and collection limits. +- Keep the project's existing request/correlation-id and typed-error conventions. +- Preserve authentication and security requirements as part of the public contract; + do not silently drop or weaken them while restructuring the specification. +- Run the repository's OpenAPI validation and regenerate affected artifacts in the + same change. +- Review the generated diff for accidental contract churn. diff --git a/rules/code-conventions.md b/rules/code-conventions.md deleted file mode 100644 index 61e9744..0000000 --- a/rules/code-conventions.md +++ /dev/null @@ -1,114 +0,0 @@ -# Code conventions - -## Architecture and dependencies - -- Transport resources handle protocol concerns only: they validate the transport - contract, delegate to a service, and translate failures into protocol errors. -- Services implement business scenarios and define the order of operations. -- Complex changes to aggregate parts are delegated to focused handlers instead of - growing a single service class. -- Repositories encapsulate persistence and return database or domain models. They do - not build transport responses. -- External systems are hidden behind local client or service interfaces; generated - stubs and retry mechanics do not leak into business code. -- Dependencies point toward service and domain abstractions; circular dependencies - between packages or modules are not introduced. -- Spring dependencies are provided through constructor injection and stored in - `final`/`val` fields. Java components use Lombok's `@RequiredArgsConstructor` - instead of handwritten constructors when no custom initialization is required. - -## Project structure - -- Code is organized into the `config`, `config.properties`, `resource`, - `servlet`, `service`, `repository`, `repository.model`, `scheduler`, `client`, - `client.model`, `converter`, and `extensions` packages. -- Standalone classes and models are placed in separate files. -- Types and members use the narrowest practical visibility. Implementation details - are not exposed only to make tests easier to write. - -## DTOs and converters - -- External API requests and responses are represented by typed DTOs, without - `Map`. -- Transport models are converted before reaching repositories. Simple entities may - use generated persistence models; aggregates use local domain models. -- JSON property names are specified with Jackson annotations only when they differ - from the corresponding field or property name. Closed sets of values are - represented by enums. -- Model conversion, including creation of requests and responses, is performed by - dedicated `@Component` classes implementing Spring's `Converter`. -- Converters map data but do not write to the database or call external systems. -- Optional fields are set only when present. An omitted value and an explicitly - empty value remain distinct when the API contract distinguishes them. -- Unsupported conversion directions fail explicitly instead of returning `null`. -- When a contract schema changes, converters and tests are reviewed so every new field - is either mapped or intentionally ignored. - -## REST-to-gRPC gateways - -- Generated REST interfaces define the transport contract. Controllers and resources - implement them, validate transport concerns, and delegate without duplicating the - contract or containing business orchestration. -- Orchestration services build typed gRPC requests, invoke generated clients, and use - dedicated converters for REST-to-Protobuf and Protobuf-to-REST mapping. -- A request or correlation identifier received at the public boundary is propagated to - every downstream request and included in logs and typed error responses. -- gRPC failures are mapped centrally to the API's declared error model. At minimum, - invalid input, unauthenticated, forbidden, not found, conflict, throttling, deadline, - downstream unavailability, and unexpected internal failures remain distinguishable. -- Transport failures never produce an untyped or accidentally empty error response. - -## Kotlin style - -- Calls to regular functions and methods use positional arguments. -- Named arguments are allowed for constructors and annotations. -- Constants belonging to a single class are placed in its `private companion object`. -- Shared constants are placed in the appropriate `constants/*.kt` file. -- Nullable values are handled explicitly; `!!` and unchecked casts are not used when - validation or a typed alternative can express the invariant. - -## Configuration - -- Settings are grouped into typed `@ConfigurationProperties`; required values use - validation constraints and the properties are validated with `@Validated`. -- Invalid required configuration fails application startup. Environment-specific - values and credentials are not hardcoded as production defaults. -- Retry policies, backoff, and asynchronous executors are configured centrally and - injected by name. - -## External clients - -- The client is responsible for transport, the converter for mapping, and the service - for the business scenario. -- A client owns its generated stub and applies the configured retry policy in one - place. -- Every remote call has an explicit finite timeout. Retry policies are bounded and - apply only when repeating the operation is safe or protected by idempotency. -- Missing recipients or input for an optional side effect causes an early return - without an external call. -- Asynchronous entry points catch and log failures that cannot be returned to the - caller. - -## Errors and logging - -- Expected domain failures use specific exception types. REST resources, controllers, - and other protocol entry points map them to protocol-specific response codes at the - application boundary; business services do not depend on HTTP or gRPC status types. -- Logs use parameterized placeholders instead of string concatenation and include - available request and domain identifiers. -- Large payloads and user content are logged only at `DEBUG` or `TRACE`. -- Credentials, tokens, personal data, and other sensitive values are redacted at every - log level, including exception messages and structured logging fields. -- Transport resources log request boundaries; services and handlers log business - steps without duplicating the full payload. - -## Testing - -- Pure converters and external-client orchestration are covered by unit tests, - including optional values, empty collections, invalid input, retries, and early - returns. -- Tests replace external integrations with mock or stub beans and assert the generated - request as well as the returned result. -- Asynchronous tests wait for an observable event instead of using a fixed `sleep`. -- Tests are deterministic, independent of execution order, and assert observable - behavior instead of private implementation details. diff --git a/rules/code-generation.md b/rules/code-generation.md deleted file mode 100644 index 33e5d21..0000000 --- a/rules/code-generation.md +++ /dev/null @@ -1,4 +0,0 @@ -# Code generation - -- Generated sources are never edited manually. -- Generation must be deterministic and runnable in CI without repository-local state. diff --git a/rules/database-conventions.md b/rules/database-conventions.md deleted file mode 100644 index 1c625e7..0000000 --- a/rules/database-conventions.md +++ /dev/null @@ -1,105 +0,0 @@ -# Database conventions - -## Stack and migrations - -- Migrations are run by Flyway from `src/main/resources/db/migration`. -- Every schema change is introduced by a new immutable migration; an applied - migration is never rewritten. -- Migration names follow `V__.sql` and describe the - schema or index changes they contain. -- `IF NOT EXISTS` is used for supported PostgreSQL objects. - -## Schema design and data lifecycle - -- Primary keys, constraints, and indexes are defined explicitly and - given meaningful names. -- Storage invariants use database defaults and `NOT NULL` constraints and are also - represented consistently in converters and repositories. -- When designing, priority is given to soft-delete. - -## jOOQ and repositories - -- Flyway runs before jOOQ code generation. -- Generated classes are created in `target/generated-sources/jooq`. -- Generated tables, records, POJOs, and enums are used. -- Flyway and ShedLock tables are excluded from jOOQ code generation. -- When a schema change affects application data, update every affected layer in the - same change: the migration, generated jOOQ model, input conversion, write query, - read model, output conversion, and tests. -- Repositories use `DSLContext` and keep jOOQ queries out of services and transport - resources. -- Inserts populate the complete model and map it to a generated record. Updates set - only fields that the operation is allowed to change. -- Query aliases match read-model property names when results are mapped with - `fetchInto`. -- Empty collections are handled before `IN` queries and collection writes. -- Upsert is used only with a defined business key. Conflict columns and the minimal - set of updated columns are listed explicitly. -- Related data for result collections is fetched in batches to avoid N+1 queries. -- Type-safe jOOQ DSL is preferred. PostgreSQL-specific plain SQL uses bind values or - `inline(...)`, never string concatenation of user input. -- Paginated queries use a deterministic total order with a unique tie-breaker. Prefer - keyset pagination. -- Absence from a single-row query is represented consistently. - -## Transaction boundaries - -- Transactions are placed at the narrowest layer that can complete the operation - atomically. Keep a transaction inside one repository method when possible; move - it to a service only when the operation coordinates several repository calls. -- Transactions contain only the calls required for atomic persistence and do not - include remote calls or unrelated work. -- Replacing related records and updating the owning aggregate happen in one transaction. - -## State transitions - -- A state transition, its validation, and all resulting writes run in one transaction. -- Repeating the same transition with the same business data is idempotent. A transition - that conflicts with an existing final state fails with a specific domain error. -- Business keys are protected by explicit unique constraints. Upsert or conflict - handling complements domain validation when concurrent requests may race. -- Batch state changes verify that the number of affected rows matches the expected - number; a partial update fails the transaction. - -## Transactional event delivery - -- The domain write and insertion of its delivery event are committed in the same - database transaction. A failure rolls back both. -- Every event has a stable identifier, a deterministic sequence or ordering key, a - delivery status, an attempt count, and the time at which it became eligible. -- The event payload contains the immutable data required for delivery; a retry does not - rebuild a materially different event from current mutable state. -- Concurrent workers claim disjoint events with a short transaction, for example by - using a lease or `FOR UPDATE SKIP LOCKED`. A database row lock is not held while a - remote call is in flight. -- Delivery is idempotent by event identifier. A worker records success only after the - recipient accepts the event and safely retries an ambiguous outcome. -- Retries are bounded and use configured backoff and next-attempt time. Exhausted events - move to an explicit terminal or dead-letter state and remain observable. - -## Time - -- `TIMESTAMP WITHOUT TIME ZONE` and `LocalDateTime` are used; values are - interpreted as UTC. -- Insert, update, and upsert operations create their audit timestamps explicitly in - UTC. -- All writes produced by one business operation reuse the same timestamp so their - audit and state-change times remain consistent. - -## Integration testing - -- Test credentials are allowed only for embedded PostgreSQL and Testcontainers. -- Migration, query, filter, search, and transactional changes are tested against a - real PostgreSQL instance provided by embedded PostgreSQL or Testcontainers. -- Tests clean only the data they own and do not depend on execution order. -- Repository tests assert persisted values, conflict/update behavior, and empty-result - boundaries, not only affected-row counts. -- CRUD scenarios verify create, read, update, logical deletion, and the values in both - the database and returned model. -- Filtering and search rules include positive, negative, and boundary cases; - pagination also covers page boundaries and continuation tokens. -- Stateful-operation tests cover idempotent replay, conflicting final states, - concurrent requests, affected-row mismatches, and full transactional rollback. -- Event-delivery tests cover atomic domain/event rollback, concurrent workers, ordered - delivery, duplicate replay, ambiguous responses, process restart, retry backoff, and - exhaustion of the attempt limit. diff --git a/rules/index.md b/rules/index.md deleted file mode 100644 index 254ff87..0000000 --- a/rules/index.md +++ /dev/null @@ -1,15 +0,0 @@ -# Shared engineering rules - -Top-level rules apply to every repository that mounts this submodule. Profiles -extend them for a selected family of services; anything specific to a single -service belongs in that service. - -- [Code generation](code-generation.md) -- [Code conventions](code-conventions.md) -- [Database conventions](database-conventions.md) -- [Protobuf](protobuf.md) - -## Profiles - -- [OpenAPI contract](profiles/openapi.md) -- [Adapter](profiles/adapter.md) diff --git a/rules/profiles/adapter.md b/rules/profiles/adapter.md deleted file mode 100644 index 6edf3ce..0000000 --- a/rules/profiles/adapter.md +++ /dev/null @@ -1,76 +0,0 @@ -# Adapter conventions - -These rules extend the common rules for services that integrate with external -providers. - -## Architecture and flow - -- Transport entry points validate the transport contract and delegate to services. -- Services coordinate the integration scenario; step-specific behavior is placed in - focused handlers selected by an explicit state or operation type. -- State transitions and transport intents are built centrally instead of being - assembled independently by handlers. -- Provider request and response models, converters, constants, and error handling - stay behind the provider client boundary. - -## Configuration and clients - -- Runtime configuration keys, provider method names, URL paths, statuses, and error - codes are declared centrally as constants or enums. -- Per-operation runtime options are validated by a dedicated validator before a - converter or handler accesses them as non-null values. -- Provider calls use the application's configured `RestClient` and `ObjectMapper`. -- Base URLs, environment selection, request paths, and authorization headers are - resolved centrally. -- Provider requests and responses use typed DTOs. Internal configuration and helper - fields that are not part of the wire contract are excluded from serialization. -- An empty response body or a body that cannot be parsed is handled explicitly and - mapped to a stable integration error. -- HTTP status errors, provider errors, and response parsing errors are distinguished - before being mapped to domain failures. - -## Secrets and logging - -- Provider credentials and tokens are obtained through the configured secret service, - such as Vault. They are never hardcoded or included in logs. -- Kotlin files use a file-level `private val log = KotlinLogging.logger {}` and lazy - logging blocks. -- PANs, phone numbers, bank accounts, tokens, and other sensitive fields are masked - before logging or storing diagnostic metadata. -- External request, response, and callback payloads pass through the shared log - sanitizer before being logged. -- DTOs containing sensitive values provide a safe `toString()` or are never logged as - complete objects. - -## State and polling - -- Multi-step operation state is held in a dedicated context. Store it in the deepest - continuation scope that reliably survives every step of the specific scenario and - reuse the established serialization path for that scope. -- Missing continuation state creates a new context; malformed state fails explicitly. -- Serialized context changes are backward compatible with states produced by the - previous deployed version and are covered by compatibility tests. -- Polling metadata, including the deadline and next interval, is stored with the - operation state. -- Polling is bounded by a deadline. Pending and unknown non-final statuses schedule - the next attempt using the configured backoff instead of looping immediately. -- Final success, final failure, timeout, transport failure, and malformed provider - responses produce distinct, deterministic outcomes. -- Callback handlers dispatch by an explicit callback type and are idempotent. A - repeated callback must not overwrite completed state or repeat a side effect. - -## Testing - -- Provider HTTP integration tests use WireMock with the application context and real - client serialization. -- Every provider method covers success, provider failure, HTTP failure, empty body, - malformed body, and required-field validation where applicable. -- Stateful flows cover pending-to-success, pending-to-failure, polling timeout, and - callback replay. -- Retry exhaustion and duplicate side effects are covered where the corresponding - behavior exists. -- Tests assert outbound method, path, headers, and body as well as the mapped result. -- Shared flow fixtures and builders contain transport mechanics; test cases describe - scenario-specific mocks, actions, and assertions. -- New provider scenarios and tests start from the closest existing template or flow - fixture, reuse established mechanics, and keep provider-specific changes minimal. diff --git a/rules/profiles/openapi.md b/rules/profiles/openapi.md deleted file mode 100644 index 682acd5..0000000 --- a/rules/profiles/openapi.md +++ /dev/null @@ -1,19 +0,0 @@ -# OpenAPI contract conventions - -These rules extend the common rules for repositories that own an OpenAPI contract and -publish generated server or client artifacts. - -## Contract structure - -- One root OpenAPI document is the source entry point. Paths and reusable components - are split into focused files and connected through local `$ref` references. -- Every operation has a stable, unique `operationId`, an appropriate tag, and explicit - request parameters, request body, responses, and security requirements. -- Common parameters, error responses, schemas, and security schemes are defined once - under `components` and reused instead of being copied between operations. -- Public operations require and document a request or correlation identifier and use a - shared typed error schema. -- Schema fields declare `required`, `nullable`, formats, enums, bounds, and collection - constraints explicitly whenever they are part of the contract. -- Existing operations and schemas evolve backward compatibly. Breaking changes require - an explicit API version and a documented deprecation and consumer migration plan. diff --git a/rules/protobuf.md b/rules/protobuf.md deleted file mode 100644 index bbf5a1f..0000000 --- a/rules/protobuf.md +++ /dev/null @@ -1,10 +0,0 @@ -# Protobuf - -- Use versioned packages and directories. -- Keep field types wire-compatible. -- Field numbers are immutable after publication. -- Removed fields and names are reserved. -- Field presence is represented explicitly when an omitted value must be distinguished - from the scalar default value. -- Add compatibility tests when a consumer may receive a newly added `oneof` variant - or enum value. diff --git a/skills/provider-adapter/SKILL.md b/skills/provider-adapter/SKILL.md new file mode 100644 index 0000000..b460fcc --- /dev/null +++ b/skills/provider-adapter/SKILL.md @@ -0,0 +1,77 @@ +--- +name: provider-adapter +description: Implement or modify an external payment/provider adapter, including provider clients, callbacks, polling, continuation state, error mapping, or adapter integration tests. Do not use for ordinary internal service changes that do not cross a provider boundary. +--- + +# Provider adapter + +Use different sources of truth for different questions: + +- the provider's current API specification and supplied documentation define the + external request/response/authentication/callback contract; +- the target repository's current code, tests, build configuration, and local + instructions define its architecture, flow mechanics, and verification conventions. + +Existing adapters, templates, fixtures, and tests are implementation evidence and +starting points, not proof of the current provider contract. + +## Invariants + +Keep these when they apply to the concrete flow: + +- send and consume only provider fields required by the current contract or actually + needed by the adapter flow; do not forward optional upstream data merely because a + template exposes it; +- preserve functionally required correlation/callback data even when the provider + schema marks it optional; +- repeated callbacks must not repeat a completed side effect or overwrite final state; +- polling must be bounded and use the repository's configured retry/backoff mechanism; +- continuation/state changes must remain readable across deployment when state can + survive a version change, unless the task explicitly coordinates a migration; +- credentials, tokens, PANs, account identifiers, and other sensitive values must not + leak through source code, logs, exceptions, DTO string rendering, or diagnostics; +- transport failures, provider-declared failures, malformed responses, timeout, and + final business outcomes must not collapse accidentally into one ambiguous path. + +## Load additional guidance only when relevant + +- provider request/response DTOs, serialization, client errors, or sensitive logging → + `references/provider-boundary.md`; +- callback, polling, replay, or multi-step continuation state → + `references/state-and-idempotency.md`; +- new adapter work or substantial adaptation of a previous-provider template → + `references/template-adaptation.md`; +- before finalizing a provider behavior change → `references/testing.md`; +- OpenAPI contract changes → `../../references/openapi.md`; +- Protobuf/generated contract changes → `../../references/code-generation.md`; +- persistence/schema changes → `../../references/database.md`. + +## Workflow + +1. Identify the operation and map the provider surface the adapter actually uses: + request, response, authentication, status polling, callbacks, and error responses as + applicable. Distinguish schema-required fields from fields that are functionally + required by the local flow. +2. Find the nearest production adapter(s) and map the local boundaries: transport entry + point, scenario/service, provider client, state/context, error mapping, and tests. +3. Read only the references that match the concrete change. +4. Implement the smallest contract-backed change that preserves the local architecture. + Access optional/union-based upstream data only when the provider request needs it, + handle absence explicitly, and do not invent defaults without accepted domain + meaning. +5. Run focused tests plus the repository's existing formatter, linter, generator, and + compatibility checks for touched code. Confirm that the intended suites actually ran. +6. When the task permits a safe provider sandbox/probe and credentials are available, + verify the material request/response behavior against the real provider as described + in `references/testing.md`. Otherwise do not claim real-provider proof. +7. Review the final diff for unintended removal of callback, polling, correlation, + error, or state behavior. If repository evidence conflicts with this skill on a + non-safety architectural preference, follow the repository and record the + discrepancy rather than rewriting the project toward a generic template. + +## What not to encode here + +Package names, Spring annotations, specific converter interfaces, logging libraries, +and DTO layout are implementation choices unless the target repository already +standardizes them. Consult `../../references/code-conventions.md` only when the task +needs an architectural decision not settled by local code. diff --git a/skills/provider-adapter/references/provider-boundary.md b/skills/provider-adapter/references/provider-boundary.md new file mode 100644 index 0000000..4e38246 --- /dev/null +++ b/skills/provider-adapter/references/provider-boundary.md @@ -0,0 +1,58 @@ +# Provider boundary + +Use for provider client, configuration, request/response models, serialization, error +mapping, or sensitive logging work. + +## Request contract + +- Build the smallest request justified by the provider contract and the local flow. + Do not read or forward optional upstream fields merely because they exist in Thrift, + DTOs, unions, or a copied template. +- Access optional objects and union branches only when their value is required. Handle + absent objects and inactive branches explicitly. +- Do not synthesize defaults unless the provider field is required and the default has + an accepted domain meaning. Preserve omitted versus explicit empty/default values + when the provider contract distinguishes them. +- Preserve fields that are functionally required for asynchronous processing or + correlation, such as callback URLs or external transaction references, even when + the provider schema marks them optional. +- Keep distinct domain values distinct. Do not place payment links, deeplinks, phone + numbers, QR payloads, or other values into a different generic field merely because + a template lacks the right mapping; propagate the correct domain field instead. + +## Client and response contract + +Keep provider-specific transport details behind one clear boundary. Centralize base +URL/environment selection, authentication, request paths, and serialization according +to the target repository's existing client stack. + +Use typed request/response DTOs for stable provider contracts, but model only fields +the adapter consumes unless additional fields are required for validation, security, +correlation, or behavior. + +Do not assume successful and error responses have the same shape. Verify how the HTTP +client exposes non-2xx responses before deciding where provider error fields belong. +Error responses may omit normal success fields such as identifiers, statuses, +requisites, or payment data. + +Treat empty bodies, malformed bodies, unexpected status codes, and provider-declared +errors explicitly. Preserve the provider's most specific useful error code/message in +the repository's existing domain error model. If several provider fields can supply +that value, define and test an explicit provider-specific priority rather than relying +on incidental deserialization order. + +Runtime configuration required for an operation should be validated before business +logic assumes it is present. Every provider call should have a finite timeout. Retries +should be bounded and used only when the repeated operation is safe or protected by +provider/application idempotency. + +## Secrets and diagnostics + +Obtain credentials from the project's configured secret mechanism. Never hardcode +them. + +Sanitize provider request/response/callback data before logging. Review both raw HTTP +logging/sanitizers and structured DTO logging; securing only one path is insufficient. +Do not rely on a default `toString()` for DTOs that may contain credentials, tokens, +PANs, account numbers, phone numbers, personal identifiers, callback signatures, +external references, QR payloads, payment links, or deeplinks. diff --git a/skills/provider-adapter/references/state-and-idempotency.md b/skills/provider-adapter/references/state-and-idempotency.md new file mode 100644 index 0000000..909f344 --- /dev/null +++ b/skills/provider-adapter/references/state-and-idempotency.md @@ -0,0 +1,54 @@ +# State, polling, callbacks, and idempotency + +Use for multi-step flows, continuation state, polling, callbacks, or replay handling. + +## Continuation state + +Use the repository's established state storage and serialization path. Some flows need +state in a deeper continuation scope than others; choose the deepest scope that +reliably survives every step of the concrete scenario rather than imposing one +universal storage location. + +Have one obvious serializer/decoder for continuation state. Missing state may create a +new context when the protocol defines that behavior; malformed state should fail +deterministically rather than being silently interpreted as a new operation. + +Before changing serialized context, inspect states produced by the previous deployed +version. Add a compatibility test when old state can survive a deployment. + +## Polling + +Persist enough metadata to continue safely after restart when the surrounding +framework does not already own it. Polling must have a finite deadline/termination +condition and use the existing retry/backoff mechanism rather than tight-looping. + +Keep timeout distinct from provider failure and malformed response. Verify both +creation and status-query behavior when the operation is asynchronous and uses both. + +## Callbacks + +Keep callback models limited to fields the adapter consumes, plus fields required for +validation, security, or correlation. + +Preserve the adapter's established asynchronous trigger. Adding optional polling must +not silently remove callback handling unless the intended flow explicitly switches +from one mechanism to the other. + +Parse callbacks using the provider's real content type and parameter names rather than +a convenient local representation. + +Treat callback delivery as at-least-once unless the provider contract proves +otherwise. A replay must be safe: completed state should not be overwritten and a side +effect should not be emitted twice. + +When a provider callback is only a notification, correlate the internal transaction +and obtain the authoritative status through the provider's status operation instead +of trusting an unverified callback status. If the provider contract explicitly makes +the callback authoritative, follow that contract instead. + +Receiving a callback signature is not the same as verifying it. Do not claim signature +validation unless the algorithm, secret/key handling, verification behavior, and tests +exist. + +When idempotency depends on persistence or a business key, enforce it at a layer that +survives concurrent requests; an in-memory guard is insufficient. diff --git a/skills/provider-adapter/references/template-adaptation.md b/skills/provider-adapter/references/template-adaptation.md new file mode 100644 index 0000000..a0def6a --- /dev/null +++ b/skills/provider-adapter/references/template-adaptation.md @@ -0,0 +1,28 @@ +# Template adaptation and drift control + +Use when implementing a new provider from an existing adapter/template or substantially +remapping an existing adapter to a new provider contract. + +Before editing, separate generic local infrastructure from assumptions belonging to +the previous provider. A copied field, status mapping, callback format, fixture, +polling rule, or request option needs evidence in the new provider contract or the +actual local flow; similarity to the template is not enough. + +Prefer the closest working local template for mechanics, but minimize copied +provider-specific behavior. Do not carry optional request fields forward unless the +new flow needs them. + +After implementation: + +- search for stale provider names, old endpoints/statuses, unused constants, + unreachable branches, obsolete fixtures, and response fields no longer consumed; +- review every removed callback, polling, correlation, error, and continuation-state + branch and confirm the removal is intentional; +- confirm every new fixture is referenced by a test and every material error fixture + has an assertion capable of observing the mapped failure; +- review the final diff for unrelated template cleanup that should not be part of the + provider change. + +The goal is not to make the new adapter resemble the template. The goal is to reuse +proven local mechanics while making every provider-specific element traceable to the +current provider contract or the concrete adapter flow. diff --git a/skills/provider-adapter/references/testing.md b/skills/provider-adapter/references/testing.md new file mode 100644 index 0000000..21edb49 --- /dev/null +++ b/skills/provider-adapter/references/testing.md @@ -0,0 +1,68 @@ +# Provider adapter verification + +Use the smallest relevant subset. Verification depth should follow the behavior and +risk of the changed operation rather than a ceremonial fixed test count. + +## Evidence levels + +Keep these claims distinct: + +1. local model/serialization verification; +2. mocked HTTP/integration-flow verification; +3. real provider contract verification; +4. production callback or end-to-end runtime proof. + +Do not present one level as proof of another. + +When credentials and a safe provider test environment are available and the task +permits it, probe the same endpoint, headers, serialization, and minimal field set the +production client uses. Real probes must use unique external references, avoid +destructive actions, redact credentials/sensitive values, and report any provider test +objects they create. For asynchronous flows, verify creation and the material status +query; verify callbacks against real provider examples or deliveries when safely +possible. + +If a real response is unavailable, specification-derived fixtures are acceptable but +should be identifiable as such rather than presented as captured provider evidence. + +## Request and response behavior + +For each changed provider operation, consider only cases the contract can actually +produce: + +- success; +- pending/status transitions when applicable; +- provider-declared final failure; +- non-2xx response with structured provider error; +- empty, malformed, or incomplete response where relevant; +- missing/invalid required runtime data. + +Assert material outbound method/path/headers/body, including required fields being +present and unnecessary optional fields being absent. Also assert the mapped domain +result or failure. A generic assertion that some exception occurred is insufficient +when the provider supplies a structured error that the adapter is expected to +preserve. + +Use the repository's established HTTP test mechanism. In Spring/WireMock projects, +prefer real serialization through the application client over mocking the client +itself when wire compatibility is the risk. + +## Fixtures and path coverage + +Every committed response fixture should be used by a test or explicitly documented as +deferred contract material. Remove duplicate and stale fixtures. + +A fixture's presence is not proof that its path is tested. Trace the material scenario +from resource loading through the mock/client, conversion, and final assertion. + +Where practical, integration tests should exercise the complete local processing path: +request conversion → HTTP client → response conversion → domain result or mapped +failure. + +For a newly added happy-path operation, add the material negative path when the +provider uses a distinct error response or mapping. Do not weaken assertions merely to +make copied template tests pass. + +If the adapter change also includes a build/platform migration, follow the target +repository's migration guide and required clean build; successful compilation alone +does not prove that expected tests, generators, or runtime-relevant checks executed. diff --git a/tests/install.sh b/tests/install.sh new file mode 100755 index 0000000..acf01ab --- /dev/null +++ b/tests/install.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd -- "$(dirname -- "$0")/.." && pwd)" + +command -v git >/dev/null +command -v jq >/dev/null + +bash -n "$ROOT/install.sh" +bash -n "$ROOT/check.sh" +bash -n "$ROOT/hooks/format-kotlin.sh" +bash -n "$ROOT/hooks/lib/common.sh" + +jq -e ' + (.hooks.Stop | type == "array") and + (.hooks.SubagentStop | type == "array") and + (has("Stop") | not) and + (has("SubagentStop") | not) +' "$ROOT/agents/codex/hooks.json" >/dev/null + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +CONSUMER="$TMP/consumer" +mkdir -p "$CONSUMER/.agent-rules" +( + cd "$ROOT" + tar --exclude=.git -cf - . +) | ( + cd "$CONSUMER/.agent-rules" + tar -xf - +) + +git -C "$CONSUMER" init -q +git -C "$CONSUMER" config user.email test@example.invalid +git -C "$CONSUMER" config user.name agent-rules-test +mkdir -p "$CONSUMER/.codex" + +cat >"$CONSUMER/.codex/hooks.json" <<'JSON' +{ + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "if [ -x .agent-rules/hooks/format-kotlin.sh ]; then exec .agent-rules/hooks/format-kotlin.sh; fi" + }, + { + "type": "command", + "command": "echo keep-me" + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "if [ -x .agent-rules/hooks/format-kotlin.sh ]; then exec .agent-rules/hooks/format-kotlin.sh; fi" + } + ] + } + ] +} +JSON + +( + cd "$CONSUMER" + ./.agent-rules/install.sh + ./.agent-rules/check.sh +) + +grep -q 'references/database.md' "$CONSUMER/AGENTS.md" +grep -q 'references/code-generation.md' "$CONSUMER/AGENTS.md" +grep -q 'references/code-conventions.md' "$CONSUMER/AGENTS.md" +grep -q 'references/openapi.md' "$CONSUMER/AGENTS.md" +grep -q 'provider-adapter/SKILL.md' "$CONSUMER/AGENTS.md" +grep -q 'do not cross the provider boundary' "$CONSUMER/AGENTS.md" +cp "$CONSUMER/AGENTS.md" "$CONSUMER/AGENTS.common" +cmp "$CONSUMER/AGENTS.md" "$CONSUMER/CLAUDE.md" + +jq -e ' + (has("Stop") | not) and + (has("SubagentStop") | not) and + ([.hooks.Stop[].hooks[]?.command | select(. == "echo keep-me")] | length == 1) and + ([.hooks.Stop[].hooks[]?.command | select(contains("format-kotlin.sh"))] | length == 1) and + ([.hooks.SubagentStop[].hooks[]?.command | select(contains("format-kotlin.sh"))] | length == 1) +' "$CONSUMER/.codex/hooks.json" >/dev/null + +# Legacy profile selectors remain accepted but must not change task routing. +printf 'adapter\n' >"$CONSUMER/.agent-rules-profile" +( + cd "$CONSUMER" + ./.agent-rules/install.sh + ./.agent-rules/check.sh +) +cmp "$CONSUMER/AGENTS.common" "$CONSUMER/AGENTS.md" + +printf 'openapi\n' >"$CONSUMER/.agent-rules-profile" +( + cd "$CONSUMER" + ./.agent-rules/install.sh + ./.agent-rules/check.sh +) +cmp "$CONSUMER/AGENTS.common" "$CONSUMER/AGENTS.md" + +( + cd "$CONSUMER" + ./.agent-rules/install.sh --profile common + ./.agent-rules/check.sh --profile adapter +) +cmp "$CONSUMER/AGENTS.common" "$CONSUMER/AGENTS.md" + +sed -i.bak 's/# Shared agent guidance/# Shared agent guidance DRIFT/' "$CONSUMER/AGENTS.md" +rm -f "$CONSUMER/AGENTS.md.bak" +if ( + cd "$CONSUMER" + ./.agent-rules/check.sh >/dev/null 2>&1 +); then + echo "check.sh did not detect managed guidance drift" >&2 + exit 1 +fi + +# A malformed managed block must fail closed before any project file is rewritten. +BROKEN="$TMP/broken" +mkdir -p "$BROKEN/.agent-rules" +( + cd "$ROOT" + tar --exclude=.git -cf - . +) | ( + cd "$BROKEN/.agent-rules" + tar -xf - +) +git -C "$BROKEN" init -q +git -C "$BROKEN" config user.email test@example.invalid +git -C "$BROKEN" config user.name agent-rules-test +cat >"$BROKEN/AGENTS.md" <<'EOF_BROKEN' +local instructions before + +valuable local content that must not be truncated +EOF_BROKEN +cp "$BROKEN/AGENTS.md" "$BROKEN/AGENTS.before" +if ( + cd "$BROKEN" + ./.agent-rules/install.sh >/dev/null 2>&1 +); then + echo "install.sh accepted a malformed managed block" >&2 + exit 1 +fi +cmp "$BROKEN/AGENTS.before" "$BROKEN/AGENTS.md" +if [ -e "$BROKEN/.codex/hooks.json" ] || [ -e "$BROKEN/.claude/settings.json" ]; then + echo "install.sh mutated hooks before rejecting a malformed managed block" >&2 + exit 1 +fi + +echo "agent-rules install tests passed"