Imported from bkper/bkper-apps (
merge-duplicates/AGENTS.md). Install upstream withnpx skills add bkper/bkper-apps --skill merge-duplicates. Copyright stays with the author.
Bkper App Standards
This section contains reusable architecture and quality guidance. It is editable, but preserve it by default and change it only when explicitly requested. Never replace this file wholesale merely to specialize the app. Preserve both guidance marker pairs and maintain the app-specific section as the app evolves.
Architecture principles
This template is intentionally opinionated. It should be enough to bootstrap an app and provide a strong basis for growing it.
Treat the app API as a first-class product surface:
- Expose reusable app behavior through typed
/api/v1/*routes whenever it may be used by more than one caller. - The shipped web client is only one consumer of the API; scripts, external clients, and agents should be able to call the same routes.
- Keep business behavior in
server/src/services/and expose it through thin routes inserver/src/api/routes.ts. - Keep route contracts in
server/src/api/schemas.tsand document them through the generated OpenAPI spec at/openapi.json. - Keep Lit components focused on rendering and user intent. Do not hide app behavior only in UI components.
- Prefer adding meaning with typed request/response schemas and properties before adding new structural layers.
When adding API behavior, update the server schema and route, add or update unit tests, regenerate the typed client API types with bun run api, and intentionally update the OpenAPI snapshot when the public contract changes.
Authentication
Do not implement custom OAuth flows, redirect handling, or token refresh.
| Context | Pattern | Location |
|---|---|---|
| Web client direct API | @bkper/web-auth provides the token and refresh operation configured in bkper-js |
client/src/auth/auth-session.ts, client/src/services/book-service.ts |
| Client app API calls | The typed API client uses auth.authenticatedFetch(); other callers send Authorization: Bearer <token> to /api/v1/* |
client/src/api/app-api.ts |
| Server API routes | Platform validates bearer auth and injects auth for server-side new Bkper() calls |
server/src/api/routes.ts |
| Event handlers | Platform routes /events; handler uses new Bkper() with outbound auth injection |
server/src/events/routes.ts |
| Local dev | Vite client auth and local outbound both use your CLI credentials (bkper auth login) |
client/vite.config.ts, bkper app dev |
Project structure
The root package orchestrates install, dev, test, build, and deploy. Keep browser UI dependencies in client/package.json, Worker dependencies in server/package.json, and root dependencies limited to cross-package tooling such as the Bkper CLI, Miniflare, and OpenAPI generation.
Keep components focused on rendering and user intent. Register only the Web Awesome components used by the app in web-awesome.ts, and style with @bkper/web-design tokens. Put auth mechanics in auth/, Bkper/client API calls in services/ and api/, and page loading/navigation flow in app/.
Keep server route handlers thin. Put API shape and validation in api/, event transport concerns in events/, and business behavior in services/.
UI grounding
Web Awesome official Agent Skills are generated resources synced from the installed @awesome.me/webawesome package by running:
bun run agent:skills
Before UI work, agents MUST read:
.agents/skills/webawesome/SKILL.md.agents/skills/webawesome-design/SKILL.md
Preserve the template UI foundation:
- Use Web Awesome components as the primary UI building blocks.
- Style with
@bkper/web-designtokens instead of ad-hoc design constants. - Keep the first-paint dark mode script in
client/index.htmlbefore/src/index.ts. - Keep the client/server layering described above.
- Keep the typed API/OpenAPI workflow: update schemas/routes/tests, run
bun run api, and review the OpenAPI snapshot for public contract changes.
API contract
The app's public API lives under /api/v1/* and is documented at /openapi.json.
| Concern | File |
|---|---|
| OpenAPI document metadata | server/src/api/openapi.ts |
| Request/response schemas | server/src/api/schemas.ts |
| API route definitions | server/src/api/routes.ts |
| Server business behavior | server/src/services/ |
| Generated client types | client/src/api/generated/types.d.ts |
| Shipped web client wrapper | client/src/api/app-api.ts |
Design API operations around the app's domain behavior, not around the current UI. The UI should call the same routes that another authenticated client could call.
API evolution rules:
/openapi.jsonis the single canonical public contract. It may contain multiple API versions over time.- Keep
/api/v1/*and its existing schema names stable once clients may depend on them. - Do not rename or version schemas just for additive changes. Old generated clients should keep working until they explicitly upgrade.
- Safe changes are additive: new routes, new optional request fields, and new optional response fields.
- Breaking changes include removing or renaming fields, changing field types or meaning, changing route semantics, narrowing accepted input, or making optional inputs required.
- Put breaking changes in a new namespace such as
/api/v2/*; do not mutate existingv1contracts. - Add new versioned schemas only when a breaking payload shape is needed. Keep the old schema available for old routes.
- Mark old operations with OpenAPI
deprecated: trueonly after a migration path exists. - The committed contract snapshot is
server/test/openapi.snapshot.json; update it only after reviewing the API change.
Agent API change checklist:
- Classify the requested API change before editing code: additive or breaking.
- If additive, keep the existing API version and schema names; update routes, schemas, tests, generated client types, and the OpenAPI snapshot.
- If breaking, add a new API version such as
/api/v2/*; preserve the old route handlers and schemas for existing clients. - Never remove, rename, or tighten a published
v1field or route unless the user explicitly asks to break compatibility.
Authentication for /api/v1/* callers is always bearer-token based:
Authorization: Bearer <bkper-oauth-token>
Inside server API routes, do not read or forward that token manually. Use server-side new Bkper() and let the platform validate inbound auth and inject outbound auth for Bkper API calls.
Development
bkper auth login
bun install
bun run dev
This runs:
vite dev— client dev server with HMRbkper app dev— one Miniflare Worker and an event tunnel to the same Worker when events are configured
Verification
Before considering a code change complete, run the deterministic root check:
bun run check
This first validates the guidance markers, then regenerates derived API/environment types, typechecks and tests the app against them, verifies production builds, checks formatting, and fails if tracked generated files are stale.
Build and deploy
bun run deploy:preview # local build + explicit preview deployment
bun run deploy # local build + explicit production deployment
Git push never deploys. Managed sync/deploy safely push a clean committed attached branch and deployment verifies that exact remote commit before uploading the existing local build. This is best-effort source provenance, not reproducible remote CI. External and monorepo workflows retain direct upload behavior.
For a managed rollback, create an attached rollback/<name> branch at the selected commit, build locally, and deploy explicitly. To clone managed source, run bkper app clone <appId> [path], enter the clone, and run bun install; clone never executes repository lifecycle scripts.
Build output:
- OpenAPI client types →
client/src/api/generated/types.d.ts - Vite client build →
dist/client/ - Worker bundle →
dist/server/
Merge Duplicates
Purpose and invariant
Merge Duplicates is a Bkper sidebar app for human-reviewed duplicate detection. It never creates a movement itself and never reconstructs merge behavior. Confirmed pairs go through Book.mergeTransactions, preserving Core's canonical zero-sum merge operation.
Jev scores deterministically eligible transaction pairs as independent typed evaluations. The app ranks those scores globally and selects non-overlapping suggestions in code. Every evaluated pair has already passed deterministic amount, date, draft-recovery, and movement-side constraints. Model output cannot initiate writes. Browser memory owns full transaction payloads and accepted/rejected decisions; there is no persisted scan resource.
Workflow
- Capture the Book transaction query and selected Account/Group context from the menu URL, then reject Viewers before listing transactions.
- List and submit at most 200 unique full transaction payloads from the browser with
Book.listTransactionsand/api/v1/analyze. - On the server, independently enforce permission and exclude checked, trashed, locked, and malformed rows before AI allowance is consumed.
- Identify deterministic candidate pairs: equal amounts, dates within seven calendar days, and either a shared Account on the same movement side or at least one draft with non-empty descriptions on both transactions. This draft recovery rule keeps incorrect Account discovery from suppressing candidates.
- Submit minimized candidate snapshots to Bkper AI
POST /v1/evaluationswith modeljev. Ask one simple three-level Score question per pair and pack as many independent questions as fit within the 100 KB request budget, splitting only when required. The levels map directly to Different, Possible, and Strong. Code supplies only the exact Account-path relationship (SAME,COMPATIBLE_PARTIAL,INSUFFICIENT, orCONFLICTING) and calendar days apart; Jev judges whether the descriptions and visible references identify one movement. Missing Accounts are unknown, so complementary partial paths from separate imports can identify one movement even without a shared Account. Keep the seven-day candidate window for processing delays, weekends and holidays. There is no language-model fallback. - Reject incomplete or invalid evaluation responses. Classify each pair by its most probable Jev level, resolving ties conservatively toward Different; discard Different pairs, rank Strong before Possible and then by score, and greedily select non-overlapping pairs with canonical index tie-breakers. Map selected pairs back to the original full payloads and build explanations only from deterministic Account and date facts.
- Require final human confirmation, then merge accepted pairs sequentially through ID-only payloads while continuing after failures.
- Save rejected pairs in one batch as lines in visible property
merge_duplicate_examples; retain the newest 50 lines within 90,000 characters on Account, Group, or Book context.
While embedded, treat the validated iframe App URL as the canonical active scope. Automatically accept Bkper bkper:app-url-changed messages and rescan unless the user changed a selection, confirmation is open, or merges are being applied. Preserve protected reviews against the latest pending URL until the user explicitly updates results; never interrupt applying work or hide its completion results.
Owner and Editor collaborators may merge and learn. Post collaborators may merge but receive a learning-skip notice. Viewers are rejected before transaction listing or AI inference.
Public routes
| Behavior | Route |
|---|---|
| Analyze submitted Book transactions | POST /api/v1/analyze |
| Canonically merge one pair | POST /api/v1/merge |
| Save rejected examples | POST /api/v1/learn |
| OpenAPI contract | GET /openapi.json |
There are no event subscriptions, KV bindings, app secrets, custom prompt overrides, or durable scan state.
Local development
- Vite client:
5178 - Worker:
8795
bun run dev
bun run check
Do not sync, deploy, install, publish, or run any command that writes to a live Book without separate explicit approval.
Key files
| Concern | File |
|---|---|
| Sidebar UI | client/src/components/merge-duplicates-app.ts |
| Browser review state | client/src/app/review-session.ts |
| Typed API client | client/src/api/app-api.ts |
| API schemas/routes | server/src/api/schemas.ts, server/src/api/routes.ts |
| Analyze orchestration | server/src/services/analyze-service.ts |
| Deterministic candidate logic | server/src/services/candidate-service.ts |
| Bkper AI request | server/src/services/bkper-ai-service.ts |
| Canonical merge call | server/src/services/merge-service.ts |
| Plain-text learning | server/src/services/learning-service.ts |