@openora/create 0.4.1-canary.35 → 0.4.1-canary.351

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/generated/versions.d.ts +11 -0
  3. package/dist/generated/versions.d.ts.map +1 -0
  4. package/dist/generated/versions.js +11 -0
  5. package/dist/generated/versions.js.map +1 -0
  6. package/dist/index.js +48 -93
  7. package/dist/index.js.map +1 -1
  8. package/dist/render.d.ts +15 -0
  9. package/dist/render.d.ts.map +1 -0
  10. package/dist/render.js +125 -0
  11. package/dist/render.js.map +1 -0
  12. package/package.json +8 -6
  13. package/template/README.md.tpl +12 -7
  14. package/template/__dot__claude/settings.json.tpl +22 -12
  15. package/template/__dot__env.example +10 -0
  16. package/template/__dot__gitignore +5 -0
  17. package/template/__dot__rulesync/commands/check.md +11 -10
  18. package/template/__dot__rulesync/commands/doctor.md.tpl +17 -0
  19. package/template/__dot__rulesync/commands/scaffold-module.md +3 -2
  20. package/template/__dot__rulesync/commands/scaffold-plugin.md +19 -3
  21. package/template/__dot__rulesync/commands/scaffold-route.md +1 -0
  22. package/template/__dot__rulesync/commands/start.md +2 -2
  23. package/template/__dot__rulesync/hooks/_shared.mjs +26 -13
  24. package/template/__dot__rulesync/hooks/guard-core.mjs.tpl +76 -28
  25. package/template/__dot__rulesync/hooks/guard-generated.mjs +15 -13
  26. package/template/__dot__rulesync/hooks/guard-mr.mjs +81 -0
  27. package/template/__dot__rulesync/hooks/guard-subagent.mjs +22 -6
  28. package/template/__dot__rulesync/hooks/post-edit.mjs.tpl +116 -0
  29. package/template/__dot__rulesync/hooks.json +27 -4
  30. package/template/__dot__rulesync/rules/conventions.md +99 -119
  31. package/template/__dot__rulesync/rules/db-conventions.md +6 -72
  32. package/template/__dot__rulesync/rules/e2e-conventions.md +33 -7
  33. package/template/__dot__rulesync/rules/frontend-conventions.md.tpl +35 -0
  34. package/template/__dot__rulesync/rules/oss-boundaries.md.tpl +78 -0
  35. package/template/__dot__rulesync/rules/overview.md +18 -82
  36. package/template/__dot__rulesync/skills/create-plugin/{SKILL.md → SKILL.md.tpl} +12 -23
  37. package/template/__dot__rulesync/skills/create-pr/SKILL.md.tpl +38 -0
  38. package/template/__dot__rulesync/skills/create-task/{SKILL.md → SKILL.md.tpl} +14 -22
  39. package/template/__dot__rulesync/skills/create-ui-module/{SKILL.md → SKILL.md.tpl} +12 -10
  40. package/template/__dot__rulesync/skills/enhance-prompt/{SKILL.md → SKILL.md.tpl} +4 -4
  41. package/template/__dot__rulesync/skills/handoff/SKILL.md +95 -0
  42. package/template/__dot__rulesync/skills/review/SKILL.md.tpl +277 -0
  43. package/template/__dot__rulesync/skills/ship/SKILL.md.tpl +157 -0
  44. package/template/__dot__rulesync/skills/ship/handoff.md.tpl +48 -0
  45. package/template/__dot__rulesync/skills/ship/watch-pr.mjs +172 -0
  46. package/template/__dot__rulesync/skills/ui-verify/SKILL.md.tpl +120 -0
  47. package/template/__dot__rulesync/skills/ui-verify/layout-check.check.sh +58 -0
  48. package/template/__dot__rulesync/skills/ui-verify/layout-check.js +163 -0
  49. package/template/__dot__rulesync/subagents/builder.md.tpl +115 -0
  50. package/template/__dot__rulesync/subagents/compliance-reviewer.md +78 -0
  51. package/template/__dot__rulesync/subagents/debugger.md.tpl +86 -0
  52. package/template/__dot__rulesync/subagents/deployer.md.tpl +54 -0
  53. package/template/__dot__rulesync/subagents/expert.md +28 -34
  54. package/template/__dot__rulesync/subagents/planner.md.tpl +39 -0
  55. package/template/__dot__rulesync/subagents/quality-reviewer.md.tpl +112 -0
  56. package/template/__dot__rulesync/subagents/security-reviewer.md.tpl +85 -0
  57. package/template/__dot__rulesync/subagents/tester.md.tpl +102 -0
  58. package/template/__dot__rulesync/sync.json.tpl +26 -0
  59. package/template/apps/api/package.json.tpl +2 -2
  60. package/template/apps/api/src/extensions.config.ts.tpl +12 -0
  61. package/template/apps/api/src/main.ts.tpl +1 -4
  62. package/template/apps/api/src/seed.ts.tpl +8 -1
  63. package/template/docker-compose.yml +13 -0
  64. package/template/docs/agents/cross-repo.md.tpl +37 -0
  65. package/template/docs/agents/forge.md.tpl +54 -0
  66. package/template/docs/agents/issue-tracker.md.tpl +42 -0
  67. package/template/docs/standards/comments.md +16 -0
  68. package/template/docs/standards/database.md +77 -0
  69. package/template/docs/standards/enforcement.md +12 -0
  70. package/template/docs/standards/errors.md +30 -0
  71. package/template/docs/standards/frontend.md +60 -0
  72. package/template/docs/standards/functions.md +38 -0
  73. package/template/docs/standards/git-delivery.md +20 -0
  74. package/template/docs/standards/module-structure.md +37 -0
  75. package/template/docs/standards/testing.md +88 -0
  76. package/template/docs/standards/types.md +31 -0
  77. package/template/package.json.tpl +15 -9
  78. package/template/rulesync.jsonc +3 -3
  79. package/template/tools/oss-worktree.mjs.tpl +105 -0
  80. package/template/tools/review-precheck.mjs +531 -0
  81. package/template/tools/sync-agents.mjs +257 -0
  82. package/dist/generated/core-version.d.ts +0 -2
  83. package/dist/generated/core-version.d.ts.map +0 -1
  84. package/dist/generated/core-version.js +0 -2
  85. package/dist/generated/core-version.js.map +0 -1
  86. package/template/__dot__rulesync/commands/doctor.md +0 -16
  87. package/template/__dot__rulesync/hooks/post-edit.mjs +0 -57
  88. package/template/__dot__rulesync/rules/oss-boundaries.md +0 -31
  89. package/template/__dot__rulesync/skills/add-feature/SKILL.md +0 -113
  90. package/template/__dot__rulesync/skills/add-feature/handoff.md +0 -58
  91. package/template/__dot__rulesync/skills/create-pr/SKILL.md +0 -55
  92. package/template/__dot__rulesync/skills/review/SKILL.md +0 -110
  93. package/template/__dot__rulesync/subagents/builder.md +0 -93
  94. package/template/__dot__rulesync/subagents/debugger.md +0 -82
  95. package/template/__dot__rulesync/subagents/deployer.md +0 -66
  96. package/template/__dot__rulesync/subagents/qa.md +0 -88
  97. package/template/__dot__rulesync/subagents/quality-reviewer.md +0 -67
  98. package/template/__dot__rulesync/subagents/security-reviewer.md +0 -54
@@ -0,0 +1,102 @@
1
+ ---
2
+ targets:
3
+ - '*'
4
+ name: tester
5
+ description: >-
6
+ Tester for a downstream igaming built on @openora/*. Drives a built change
7
+ against the operator's local stack, checks every test case and acceptance
8
+ criterion, attacks it like a player, a tester and a fraudster would, and
9
+ returns a per-case verdict table. Adds the tests its pass exercised;
10
+ never fixes app code. Records the per-AC videos only when asked, once,
11
+ on the final commit. Use as step 3 (verify) and step 4b (evidence) of
12
+ ship, or to verify a branch by hand.
13
+ claudecode:
14
+ model: sonnet
15
+ effort: high
16
+ ---
17
+
18
+ You verify a built change by hand against the running stack, debug failures with Chrome DevTools, and triage bugs - OSS core vs operator overlay. After the pass you add the tests it exercised, in the same branch. You start no subagents of your own: a domain doubt or a bug goes in your reply, and the orchestrator routes it.
19
+
20
+ ## Ground first
21
+
22
+ 1. `list-routes` (oss MCP) - full API surface (platform + operator routes).
23
+ 2. `catalog-overview` - active modules and expected behavior.
24
+ 3. `apps/api/src/extensions.config.ts` - active overlays and adapters.
25
+
26
+ ## Local stack
27
+
28
+ The platform is headless (API + modules); the player app and backoffice are this operator's own frontends. API :3001, player app :3000, backoffice :3002. Seed credentials (after `pnpm db:seed`): `admin@oss.dev` / `password1234`. Confirm ports and which UIs exist with the operator if they differ - an api-only consumer has no browser specs. If the stack isn't running, start what the cases need yourself (the API, plus the player app or backoffice for a browser case) per the repo's worktree rules, and stop it when you are done. Ports held by another worktree: never kill it - the cases that need it are UNCHECKED, saying so.
29
+
30
+ ## Your pass: verify by hand, then add the tests
31
+
32
+ A feature or fix PR carries its own tests (`docs/standards/testing.md`). Your deliverable is:
33
+
34
+ 1. List every test case from the plan you are handed and every acceptance criterion from the ticket (read per `docs/agents/issue-tracker.md`), numbered, before touching the app. An AC hidden in a comment or a linked page counts.
35
+ 2. Drive each AC against the running stack yourself, including the error states and the authz negatives. Assume the change is broken until the app proves otherwise - the developer's description and the videos already on the PR are claims, not evidence.
36
+ 3. **Try to break it** (below).
37
+ 4. No videos in this pass: the code can still change after review, and a clip recorded now gets thrown away. The one exception is a bug's "before" clip, recorded once when you reproduce it on the unfixed code.
38
+ 5. A **verdict table**: one row per case and AC - `case | PASS / FAIL / UNCHECKED | what happened, evidence (spec run, trace, screenshot)`. Never PASS because a screen exists - the outcome the case names must be observed, and the row names where: the test that ran it, the response, or the screenshot path. A browser case checked only through the API, or a case nothing ran, is UNCHECKED, saying why.
39
+ 6. **Mutation check** on a money, KYC, responsible-gambling or authz change (`docs/standards/testing.md`, "Tests that catch the bug"): the repo's `test:mutation` script scoped to the changed files where it has one, otherwise break each changed check by hand (flip a condition, move a boundary by one, drop an owner check, change the sign of an amount), run the tests that cover it, and restore the file with `git checkout -- <file>`. One row per break in the verdict table: `mutation <file:line> <break> | KILLED / SURVIVED / UNCHECKED | the test that failed`. A break no test catches: write the test that catches it, run the break again. Still SURVIVED, or a break you could not run, stays in the table as such.
40
+ 7. The tests that pass exercised, committed in this branch (section below), and a **"Tests"** list: one line per test, tier (unit / API E2E / browser E2E) and path - it goes into the MR description.
41
+ 8. Clean up: drop the scratch data you made (rows, a database or Redis db you created for the pass, `apps/e2e/.auth` files you regenerated) and stop what you started. Say what you could not remove.
42
+
43
+ A broken flow is a FAIL row with the exact steps, expected vs actual and the file:line if you found it - the orchestrator sends it to `builder`. A failing spec you did not write: run it on the base commit too and name that run in the row; failing there as well is pre-existing, it is not this change's FAIL. Never call a spec pre-existing from reading code: unrun on the base, it is a FAIL.
44
+
45
+ ## Try to break it
46
+
47
+ After the happy path, attack the change the way a player, a bored tester and a fraudster would. Log every attempt with its result - a list of what you tried that held is part of the report. Only the attacks that fit the change: a copy fix needs none, a new money flow needs all of them.
48
+
49
+ - Input: empty, zero, negative, decimals past the currency's precision, the max and max+1, very long strings, unicode, pasted whitespace.
50
+ - Timing: double submit, rapid repeat clicks, refresh and browser back mid-flow, the same flow in two tabs, an expired session.
51
+ - Ownership: another player's ids in the URL or request body, a non-admin on an admin route, a logged-out call.
52
+ - State: a limit already spent, a self-excluded or unverified player, an insufficient balance, a bonus already active.
53
+ - Money paths: replay the request and check the ledger moved once.
54
+
55
+ ## Tests
56
+
57
+ They live in the change's own branch and follow the `conventions` tier: a route -> API E2E in `apps/e2e/tests/api/**` (happy + one hostile path); a screen -> browser spec; pure logic -> unit. An in-process test that mocks the database or a service is a defect - delete it and cover the behaviour with the API E2E.
58
+
59
+ E2E specs live in `apps/e2e/tests/<app>/<domain>/<scenario>.spec.ts` (Playwright projects `api`, `web` and `backoffice`) and follow the `e2e-conventions` rule: real seeded stack by default (that run blocks merge; `USE_MOCKS` only for a state you cannot produce for real), import `test`/`expect` from `fixtures.ts` (never `@playwright/test`), typed fixtures in `mocks/`, `data-testid` selectors, functional page objects. If `apps/e2e` is missing, scaffold it: `mkdir -p apps/e2e && cd apps/e2e && pnpm init && pnpm add -D @playwright/test && npx playwright install chromium`.
60
+
61
+ Debug and verify with the **Playwright CLI** first (`pnpm -F {{scope}}/e2e test <spec>`, `npx playwright screenshot <url> <file>`, or a throwaway spec with `page.screenshot`) - it costs a fraction of the tokens a browser MCP does. Reach for the **chrome-devtools** MCP only for a live console or network read the CLI cannot give you: navigate, reproduce the action, read console messages (JS errors, unhandled rejections), inspect network requests (status codes, response shapes), evaluate a script for DOM state, screenshot the failure.
62
+
63
+ **Screenshot review on every changed screen**, part of the verify pass. Nobody reads a video back, so layout defects are caught on stills, by you. Read `.claude/skills/ui-verify/SKILL.md` and follow its "Screenshot review" yourself - do not invoke the skill, it forks a subagent: four shots per screen, `layout-check.js` in each, then read each shot. Every shot gets a verdict-table row `screen <name> <viewport> <theme> | PASS / FAIL | <png path>, what you saw`; a defect is a FAIL for `builder`. A UI change with no screenshot rows is not verified.
64
+
65
+ ## Evidence pass: videos, once
66
+
67
+ Only when the orchestrator asks for it: after the verify pass is green and review is clean, on the final commit. Nothing is verified or attacked again here; it records what already passed.
68
+
69
+ **Evidence is mandatory** before the MR: one short video per acceptance criterion (aim under 30 s, ending on the outcome the AC names; the "after" twin of a fix's "before" clip), saved under `apps/e2e/test-results/evidence/<ac>-<slug>.webm`, and the file paths listed in the report so they reach the PR description - a human confirms the change by watching it, not by reading a description.
70
+
71
+ 1. Specs first: run every browser spec that drives an AC, whether this branch added it or it already existed, in one command with video on (the repo's record switch where it has one, otherwise `test.use({ video: 'on' })`), and copy each `test-results/<test>/video.webm` to its evidence name. One run films every AC a spec drives, at no token cost per step.
72
+ 2. `playwright-cli` (`video-start`, `video-chapter "AC<n>: ..."`, drive it, `video-stop`) only for an AC no spec drives.
73
+ 3. A spec that fails here is a FAIL row back to the orchestrator, never a re-recorded workaround.
74
+
75
+ API-only changes attach the request/response trace instead. Scale it to the change: a footer link needs one clip, a new money flow needs the whole journey. After a fix to an open MR, record again only the ACs that fix touched.
76
+
77
+ ## Triage - the key question
78
+
79
+ Is the bug in OSS core or the operator's overlay?
80
+
81
+ - Fails in a fresh consumer scaffold / with no overlays active -> OSS core -> report upstream.
82
+ - Fails only with this operator's plugins/adapters -> a FAIL row for `builder`.
83
+ - Technically consistent but violates igaming rules -> a question for `expert` in your reply.
84
+
85
+ Severity: P0 blocks money/auth/game loop; P1 wrong business logic (wrong balance, bad geo-block); P2 broken UI / wrong API shape; P3 cosmetic (a row note, not a FAIL).
86
+
87
+ ## Core flows (priority order)
88
+
89
+ 1. Auth: register, login, logout, bad credentials.
90
+ 2. Wallet: balance, deposit, withdraw, history.
91
+ 3. Gaming: catalogue, start/end round, balance deduction.
92
+ 4. Bonus: claim, wagering progress.
93
+ 5. Compliance: deposit limits, geo-block.
94
+ 6. Backoffice: admin login, player list, KYC status.
95
+ 7. Each active operator overlay.
96
+
97
+ ## Rules
98
+
99
+ - Never modify platform or overlay code - a test that fails on app code is a FAIL row, not a fix. The one exception is a mutation-check break, restored before you run anything else; `git status` shows no app file changed when you report.
100
+ - Assert on user-visible outcomes, not component internals.
101
+ - Commit only the tests you added, then run the repo's gate (`/check`) and the specs you touched once more on HEAD. Never push.
102
+ - Videos and screenshots stay under `apps/e2e/test-results/evidence/`, never committed; the orchestrator embeds them in the MR.
@@ -0,0 +1,26 @@
1
+ {
2
+ "vars": {
3
+ "name": "{{name}}",
4
+ "scope": "{{scope}}",
5
+ "trackerKey": "{{trackerKey}}",
6
+ "jiraSite": "{{jiraSite}}",
7
+ "jiraCloudId": "{{jiraCloudId}}",
8
+ "wikiSpace": "{{wikiSpace}}",
9
+ "teamChannel": "{{teamChannel}}",
10
+ "gitRemotePath": "{{gitRemotePath}}",
11
+ "ossDir": "{{ossDir}}",
12
+ "ossFromRoot": "{{ossFromRoot}}",
13
+ "mrTarget": "{{mrTarget}}"
14
+ },
15
+ "reviewPrecheck": {
16
+ "reactCompilerPaths": ["apps/web", "apps/backoffice"],
17
+ "extraSkipGlobs": [],
18
+ "domainPatterns": { "security": [], "compliance": [] }
19
+ },
20
+ "consumerOwned": [
21
+ ".rulesync/rules/overview.md",
22
+ ".rulesync/mcp.json",
23
+ ".rulesync/sync.json",
24
+ "docs/agents/forge.md"
25
+ ]
26
+ }
@@ -14,7 +14,7 @@
14
14
  "@openora/core": "{{coreVersion}}"
15
15
  },
16
16
  "devDependencies": {
17
- "tsx": "4.22.2",
18
- "typescript": "6.0.3"
17
+ "tsx": "{{tsxVersion}}",
18
+ "typescript": "{{typescriptVersion}}"
19
19
  }
20
20
  }
@@ -1,4 +1,5 @@
1
1
  import { corePlugins, type PluginEntry } from '@openora/core/server';
2
+ import { createRequire } from 'node:module';
2
3
  import { dirname, resolve } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
 
@@ -7,16 +8,27 @@ import { fileURLToPath } from 'node:url';
7
8
  // later registration of a DI token wins, so an adapter override must come after
8
9
  // the module it replaces. Opt a core module out with a filter, eg
9
10
  // `corePlugins().filter((p) => p.id !== 'chat')`.
11
+ //
12
+ // Every `path` must be absolute: the plugin host imports it from inside
13
+ // @openora/core, where a relative path or a bare package name resolves from
14
+ // core's location instead of this file's.
10
15
 
11
16
  // Local overlays live alongside this file, under src/extensions/<name>/.
12
17
  const LOCAL = resolve(dirname(fileURLToPath(import.meta.url)), 'extensions');
13
18
 
19
+ // Add-on packages installed from a registry resolve from this app.
20
+ const fromApp = createRequire(import.meta.url).resolve;
21
+
14
22
  export const extensions: PluginEntry[] = [
15
23
  ...corePlugins(),
16
24
 
17
25
  // Your overlays. Generate with `pnpm gen plugin` / `pnpm gen adapter`.
18
26
  // Example (loaded after `wallet` so its PAYMENT_ADAPTER binding wins):
19
27
  // { id: 'my-payment', path: `${LOCAL}/my-payment/plugin.ts` },
28
+
29
+ // An add-on package, after the modules it depends on:
30
+ // { id: 'igaming-poker', path: fromApp('@acme/igaming-poker/plugin') },
20
31
  ];
21
32
 
22
33
  void LOCAL;
34
+ void fromApp;
@@ -67,18 +67,15 @@ process.env['EXTENSIONS_CONFIG'] ??= resolve(
67
67
  );
68
68
 
69
69
  async function bootstrap() {
70
- const { listen, emitOpenApiSpec } = await createApp({
70
+ const { listen } = await createApp({
71
71
  plugins: await loadExtensions(),
72
72
  contract,
73
73
  authSchema: { user, session, account, verification, twoFactor },
74
74
  igaming,
75
75
  port: Number(process.env['PORT'] ?? 3001),
76
- cors: { origins: process.env['CORS_ORIGINS']?.split(',') ?? '*' },
77
- openapi: { info: { title: '{{name}} API', version: '0.1.0' } },
78
76
  });
79
77
 
80
78
  await listen();
81
- await emitOpenApiSpec();
82
79
  }
83
80
 
84
81
  void bootstrap();
@@ -10,6 +10,9 @@
10
10
  */
11
11
  import { createDrizzleDb } from '@openora/core/server';
12
12
  import { seedRoles } from '@openora/core/iam/seed';
13
+ import { seedTag } from '@openora/core/pam/tag/seed';
14
+ import { seedAutoWithdrawalConfig } from '@openora/core/wallet/seed';
15
+ import { seedDefaultWeightProfile } from '@openora/core/promo/seed/bonus';
13
16
  // import additional module seeders here as you enable them
14
17
 
15
18
  async function main() {
@@ -27,7 +30,11 @@ async function main() {
27
30
  const db = createDrizzleDb(databaseUrl);
28
31
 
29
32
  await seedRoles(db);
30
- // await seedOtherModule(db);
33
+ await seedTag(db);
34
+ await seedAutoWithdrawalConfig(db);
35
+ // Without it, a bonus with no offer behind it - a gift, a rain drop, a hand-issued grant -
36
+ // resolves no weight profile and is refused.
37
+ await seedDefaultWeightProfile(db);
31
38
  console.log('Reference data seeded.');
32
39
  }
33
40
 
@@ -17,8 +17,20 @@ services:
17
17
 
18
18
  redis:
19
19
  image: redis:7-alpine
20
+ command:
21
+ [
22
+ 'redis-server',
23
+ '--maxmemory-policy',
24
+ 'noeviction',
25
+ '--appendonly',
26
+ 'yes',
27
+ '--appendfsync',
28
+ 'everysec',
29
+ ]
20
30
  ports:
21
31
  - '6379:6379'
32
+ volumes:
33
+ - redis_data:/data
22
34
  healthcheck:
23
35
  test: ['CMD', 'redis-cli', 'ping']
24
36
  interval: 5s
@@ -27,3 +39,4 @@ services:
27
39
 
28
40
  volumes:
29
41
  postgres_data:
42
+ redis_data:
@@ -0,0 +1,37 @@
1
+ # Working across this repo and the OSS checkout
2
+
3
+ Some changes need both repos: an OSS core change plus this repo's adaptation to it. Agents and humans do those the same way, from any agent tool.
4
+
5
+ ## Conventions
6
+
7
+ - **OSS code is changed only in a worktree**: `pnpm oss:worktree <branch>` creates or reuses `{{ossDir}}/.worktrees/<branch, / as +>` and installs its dependencies. The `guard-core` hook denies writes anywhere else in `{{ossDir}}` and in `node_modules`, in every tool rulesync renders hooks for. The main `{{ossDir}}` checkout stays on its branch: other sessions build against it.
8
+ - **Core gets generic changes only**: before touching core, state why it cannot live here and why another operator would want it. Operator-specific behavior stays here; core gets only the seam.
9
+ - **Any core change can be made here**, new features included, in the worktree. The `handoff` skill is for work that should run in a separate session rooted in the OSS repo.
10
+ - **Review before push**: `pnpm -C <worktree> verify`, then the OSS repo's own review workflow and its contract checklist on the worktree diff.
11
+ - **Same branch name in both repos** pairs the two requests. Skills find the OSS half from this repo's branch.
12
+ - **No links between the requests**: the shared branch name is the pairing. The OSS repo is public, so its PR never names the operator; this repo's request may be read by people outside the team, so it never names or links the OSS repo.
13
+ - **Run against the change**: `pnpm oss:worktree <branch> --link` builds the worktree and points `pnpm link:oss` at it; `pnpm link:oss` alone restores the main checkout. This repo runs core's built `dist/`, not its source, so after every core edit run `pnpm -C <worktree> build` before this repo or its e2e sees the change.
14
+ - **Review covers both halves**: the `review` skill finds the pair, reviews the OSS diff by the OSS repo's own rules, and cross-checks the contract between them.
15
+ - **Clean up** after both requests merge: `pnpm oss:worktree <branch> --remove`.
16
+
17
+ The full procedure lives in "Changing OSS core" in the `oss-boundaries` rule.
18
+
19
+ ## Example asks
20
+
21
+ One session covers both halves; start it here with `{{ossDir}}` added (see below).
22
+
23
+ - "{{trackerKey}}-123: the withdrawal list needs a `currency` filter. Add it to the OSS contract in a worktree, then use it here."
24
+ - "The OSS bug behind {{trackerKey}}-456: fix it in a worktree, link it, and rerun this repo's e2e against it."
25
+ - "Review this branch and its OSS pair."
26
+ - "Both requests merged: bump `@openora/*` and remove the worktree."
27
+
28
+ ## Giving each tool access to `{{ossDir}}`
29
+
30
+ Grant the whole `{{ossDir}}`, not only `.worktrees/`: a worktree keeps its git metadata in `{{ossDir}}/.git`, so committing there writes to it. The hook still limits file edits to the worktree.
31
+
32
+ - **Claude Code**: `permissions.additionalDirectories: ["{{ossDir}}"]` in `.claude/settings.json`, checked in. Takes effect once the workspace is trusted. That grants file access only; `claude --add-dir {{ossDir}}` (or `/add-dir {{ossDir}}`) also loads the OSS repo's skills and agents, and with `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1` in the `env` block of `.claude/settings.json`, checked in, its `CLAUDE.md` and `.claude/rules/`.
33
+ - **Codex CLI**: `codex --add-dir {{ossDir}}`. `.codex/config.toml` is generated by rulesync, so a checked-in `writable_roots` entry would be overwritten. A personal alternative is `[sandbox_workspace_write] writable_roots` in `~/.codex/config.toml` with an absolute path.
34
+ - **GitHub Copilot CLI**: `copilot --add-dir {{ossDir}}`, or `/add-dir {{ossDir}}` in the session. No project-level key exists.
35
+ - **Gemini CLI**: `gemini --include-directories {{ossDir}}`, or `/directory add {{ossDir}}`. `.gemini/settings.json` is generated by rulesync.
36
+
37
+ Each tool reads only this repo's instruction files at start. The rules therefore tell every agent to read the OSS repo's `AGENTS.md`, `.rulesync/rules/`, and `docs/standards/` from the worktree before editing or reviewing there, so no tool depends on loading a second repo's instructions.
@@ -0,0 +1,54 @@
1
+ # Code forge: GitHub
2
+
3
+ Code review for `{{gitRemotePath}}` happens on GitHub pull requests, driven by the [`gh`](https://cli.github.com) CLI. Every skill that opens, reads, or comments on a pull request reads this file instead of hardcoding commands, so a repo on a different forge rewrites this one file and the skills keep working.
4
+
5
+ Swapping forge: keep the headings below and replace the commands. For GitLab that is `glab`, "merge request" for "pull request", `glab mr create/view/diff/note`, and `glab api "projects/<path-with-%2F>/merge_requests/<n>/discussions"` with a `position` object for inline comments.
6
+
7
+ ## Coordinates
8
+
9
+ - Repo: `{{gitRemotePath}}`. CLI: `gh` (authenticate once with `gh auth login`).
10
+ - Noun: pull request, "PR" for short. The default target branch is `{{mrTarget}}`.
11
+
12
+ ## Where a change lands
13
+
14
+ - Topic branch (`feat/*`, `fix/*`, `{{trackerKey}}-*`) -> `{{mrTarget}}`.
15
+ - A repo that promotes through environment branches adds them here (for example `{{mrTarget}}` -> `stage` -> `prod`) and never opens a PR straight from a topic branch to the last one.
16
+ - If the current branch is not listed, target `{{mrTarget}}`.
17
+
18
+ ## Open a pull request
19
+
20
+ ```
21
+ gh pr create --base <target> --head <current> --title "<type>: <summary>" --body "<body>"
22
+ ```
23
+
24
+ Reuse an open PR for the same head -> base instead of opening a duplicate: `gh pr list --head <current> --base <target>`. Never delete a long-lived branch on merge.
25
+
26
+ ## Read a pull request
27
+
28
+ - Intent and discussion: `gh pr view <n> --comments`
29
+ - Patch: `gh pr diff <n>`
30
+ - Review comments (resolved and unresolved - this endpoint has no resolved state): `gh api --paginate "repos/{{gitRemotePath}}/pulls/<n>/comments"`
31
+ - CI: `gh pr checks <n>`; wait for it to finish with `gh pr checks <n> --watch` (GitLab: `glab ci status --live`)
32
+ - Source to review, without switching the working tree: `git fetch origin "$(gh pr view <n> --json headRefName -q .headRefName)" && git worktree add --detach .claude/worktrees/review-<n> "$(gh pr view <n> --json headRefOid -q .headRefOid)"`
33
+ - Last review marker (`review` skill §2a): `gh api --paginate "repos/{{gitRemotePath}}/issues/<n>/comments" --jq '[.[] | select(.body | contains("<!-- review:sha=")) | select(.author_association == "OWNER" or .author_association == "MEMBER" or .author_association == "COLLABORATOR") | select(.user.login != "<pr-author>")] | last | .body' | grep -o 'review:sha=[0-9a-f]*' | tail -1`, with `<pr-author>` from `gh pr view <n> --json author -q .author.login`. Both filters are load-bearing: anyone can post a comment containing the marker text for any SHA they like, so an outside contributor's marker and the PR author's own (a member can be both) are never trusted as a completed review. The marker only narrows an interactive review; `--ci` ignores it.
34
+
35
+ ## Attach evidence
36
+
37
+ `gh` has no command to upload an image. Open the PR in the browser and drag each file into the description or a comment; GitHub inserts the embed markdown, keep it. If a file cannot be uploaded, say in the description that it is available from the author. Never paste a local path.
38
+
39
+ ## Inline review comments
40
+
41
+ Anchor each comment to a line of the diff on the head commit:
42
+
43
+ ```
44
+ gh api "repos/{{gitRemotePath}}/pulls/<n>/comments" \
45
+ -f body="<comment>" -f commit_id="<head-sha>" -f path="<file>" -F line=<new-line> -f side=RIGHT
46
+ ```
47
+
48
+ Take `<head-sha>` from `gh pr view <n> --json headRefOid`. Anchor on the NEW-file line of an added line. Verify the response carries a non-null `line`; if it came back as a plain issue comment, delete it and retry rather than leaving an unanchored note. Post the verdict itself as one summary comment: `gh pr comment <n> --body "<one sentence>"`.
49
+
50
+ ## Rules
51
+
52
+ - Never push without an explicit per-action confirmation from the user.
53
+ - Never merge a pull request unless the user asks for that exact action.
54
+ - Never resolve someone else's review thread.
@@ -0,0 +1,42 @@
1
+ # Issue tracker: Jira (project `{{trackerKey}}`) + Confluence + Slack
2
+
3
+ Work for this operator is tracked in Jira project `{{trackerKey}}` on `{{jiraSite}}`. Requirements and reasoning live in Confluence space `{{wikiSpace}}`. Decisions that never made it into a ticket live in Slack `{{teamChannel}}`. Code review happens on `{{gitRemotePath}}` - see `docs/agents/forge.md`.
4
+
5
+ Every skill that says "fetch the relevant ticket" reads this file.
6
+
7
+ ## Keys and where they appear
8
+
9
+ - Ticket key: `{{trackerKey}}-<n>`. `{{trackerKey}}-0` means "no ticket" (chores).
10
+ - Branch: `<type>/{{trackerKey}}-<n>/<slug>`. MR title: conventional commit subject; MR description ends with `Closes {{trackerKey}}-<n>` when it finishes the ticket, `Refs {{trackerKey}}-<n>` when it is one of several MRs for it. If your forge's tracker integration keys on `Closes`, `Refs` never moves the ticket.
11
+ - Resolve the key from, in order: the argument the user gave, the MR description, the MR title, the branch name, commit subjects.
12
+
13
+ ## What "read the ticket" means
14
+
15
+ A ticket is read when ALL of this has been seen - never from the description alone:
16
+
17
+ 1. Description and acceptance criteria (task items render as `- [ ]` / `- [x]`).
18
+ 2. Every comment, with author and date - decisions and scope changes hide there.
19
+ 3. Every attachment and every inline image, downloaded and viewed as pixels. Filenames, thumbnails, `blob:` links and metadata prove nothing.
20
+ 4. Parent epic and linked issues named by the AC.
21
+ 5. Every Confluence page the ticket links - its body, its images, its inline and footer comments. The AC are the spec; the page behind them is the reasoning.
22
+ 6. A Slack thread, only when the ticket or MR says the design or decision lives there ("shared in chat", a Slack link). Optional otherwise.
23
+
24
+ How:
25
+
26
+ - With the `atlassian-read` skill installed: `python3 ~/.claude/skills/atlassian-read/scripts/read.py issue {{trackerKey}}-<n>` then `page <id>` for each linked page. It downloads everything and prints a manifest mapping each image to the comment it came from.
27
+ - Without it, REST with an Atlassian API token: Jira `GET /rest/api/3/issue/{{trackerKey}}-<n>?fields=summary,status,description,attachment,comment,issuelinks,parent`, attachments `GET /rest/api/3/attachment/content/<id>`; Confluence `GET /wiki/api/v2/pages/<id>?body-format=atlas_doc_format`, `/attachments`, `/footer-comments`, `/inline-comments`, download `/wiki<downloadLink>`. Bodies are ADF: walk `text` nodes for prose and `media`/`mediaInline` nodes for images (`attrs.alt` is the Jira attachment filename; on Confluence `attrs.id` is the attachment `fileId`).
28
+ - The Atlassian MCP is fine for search, text and writes, but it returns no image bytes - it is never sufficient on its own when attachments exist.
29
+ - Slack: the `slack-reader` agent when available, else the Slack MCP, scoped to the channel and thread named.
30
+
31
+ Credentials are personal: `CONFLUENCE_EMAIL` / `CONFLUENCE_TOKEN` (an Atlassian API token) from a local, untracked env file. Never commit them, never echo them, never store curl flags holding them in a shell variable.
32
+
33
+ ## When a skill says "fetch the relevant ticket"
34
+
35
+ Run the protocol above and distill: goal in one line, AC quoted as bullets, decisions from comments (who, when), design references (which screenshot shows what), out-of-scope lines. Report "no ticket" when no key resolves and "no access" when a fetch fails - never fall back silently, never invent AC.
36
+
37
+ ## Writing
38
+
39
+ - Never comment on, transition, or edit a Jira issue or Confluence page unless the user asks for that exact write. Reading authorizes nothing.
40
+ - The one standing exception: `/ship {{trackerKey}}-<n>` is the ask for its own writes on that ticket - the Plan, questions, blocker and MR-link comments, the test cases where the `test-plan` skill keeps them, and the transitions to In Progress and Code Review. Nothing else.
41
+ - Review comments go through `review --post`, draft-and-confirm, using the commands in `docs/agents/forge.md`.
42
+ - Slack: draft only, never send.
@@ -0,0 +1,16 @@
1
+ # Comments
2
+
3
+ Read this before writing a comment or JSDoc.
4
+
5
+ - Comment WHY, never WHAT: hidden constraint, invariant, bug workaround (link it), trade-off. If a
6
+ block needs a comment to be understood, rename/extract instead.
7
+ - No section-divider comments (`// ---`, `// ===`).
8
+ - Every JSDoc is multi-line, always - `/**`, the text, and `*/` each on their own line. A
9
+ single-line `/** ... */` is never allowed, even for one sentence. Add one only on an independent
10
+ function/class (not a React component or hook) that is >~15 lines or has non-obvious params. One
11
+ sentence; document the surprising contract, not the name.
12
+ - Never JSDoc a React component or hook - a genuinely surprising note goes on the specific
13
+ prop/param type, or an inline comment at the call site.
14
+ - Deferred work: `// TODO:` with the concrete follow-up, never a bare TODO.
15
+ - Placeholder/sample data and stubs: greppable `// mock:` comment so throwaway code stays
16
+ findable.
@@ -0,0 +1,77 @@
1
+ # Database conventions (SQL / Drizzle)
2
+
3
+ Read this in full before editing a schema, Drizzle query, migration config, or seed under
4
+ `apps/api/**`. Tables live in `@openora/*` core for platform domains - never edit those; these
5
+ rules govern the tables your overlay adds
6
+ (`apps/api/src/extensions/<name>/src/schema/`). Import/boundary rules live in `oss-boundaries`;
7
+ this file is SQL only.
8
+
9
+ ## Identifiers - snake_case everywhere
10
+
11
+ Every drizzle instance sets `casing: 'snake_case'`, so the SQL name derives from the camelCase
12
+ key. Pass an explicit name only where casing can't derive it: table names, `pgEnum` types, index
13
+ names.
14
+
15
+ ```ts
16
+ // good - key derives the column; const camelCase; explicit snake_case only for table + index
17
+ export const playerNote = pgTable(
18
+ 'player_note',
19
+ {
20
+ id: uuid().primaryKey().defaultRandom(),
21
+ playerId: uuid().notNull(), // -> player_id
22
+ createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
23
+ },
24
+ (t) => [index('player_note_player_id_idx').on(t.playerId)],
25
+ );
26
+
27
+ // bad - explicit/camelCase column names, PascalCase table
28
+ pgTable('PlayerNote', { player_id: uuid('playerId') });
29
+ ```
30
+
31
+ ## Timestamps - always timestamptz
32
+
33
+ ```ts
34
+ createdAt: timestamp({ withTimezone: true }); // good - Postgres timestamptz, store UTC
35
+ createdAt: timestamp(); // bad - naive, drops the zone
36
+ ```
37
+
38
+ Every datetime column (`createdAt`, `updatedAt`, `expiresAt`, any `*At`) carries the zone.
39
+
40
+ ## Keys, references, indexes
41
+
42
+ - UUID primary keys (`uuid().primaryKey().defaultRandom()`).
43
+ - **No foreign keys across a module/overlay boundary** - store a plain ID string and resolve via
44
+ the oRPC client or a schema subpath. FKs only within the same add-on.
45
+ - `NOT NULL` by default; push defaults to the DB (`.notNull().default(...)`), not app code.
46
+ - Index every column you filter or join on; name it `<table>_<cols>_idx`.
47
+
48
+ ## Efficient operations
49
+
50
+ ```ts
51
+ // bad - N+1
52
+ for (const id of ids) await db.select().from(wallet).where(eq(wallet.playerId, id));
53
+ // good - one batched query
54
+ await db.select().from(wallet).where(inArray(wallet.playerId, ids));
55
+ ```
56
+
57
+ - Select only the columns you use; don't `select(*)` wide rows to read one field.
58
+ - **Build rows by spread + override, never hand-copy field-by-field.** When a row mostly mirrors a
59
+ validated input, spread it and set only the server-computed fields - `db.insert(x).values({
60
+ ...input, id, createdAt })` - never re-list `field: input.field` per key (it silently drifts the
61
+ moment a column is added). Keep fields explicit only for: an order-sensitive hash/signature
62
+ payload; a source carrying columns the row must not receive (spread then omit them); or a
63
+ null-vs-undefined boundary that won't coerce (`actorId: input.actorId ?? null`).
64
+ - **Money / critical paths are transactional and idempotent** - a DB guard inside the transaction,
65
+ not just an `idempotencyKey` (delivery is at-least-once).
66
+
67
+ ```ts
68
+ await db.transaction(async (t) => {
69
+ if (await ledgerExists(t, idempotencyKey)) return; // guard, not just a key
70
+ await insertLedger(t, { idempotencyKey, amount, currency });
71
+ });
72
+ ```
73
+
74
+ ## Migrations
75
+
76
+ - Never hand-edit generated migrations. Change the `pgTable`, then `pnpm db:migrate`.
77
+ - One migration per schema change; review the generated SQL before committing.
@@ -0,0 +1,12 @@
1
+ # Enforcement
2
+
3
+ Read this before working around a failing gate or adding a lint rule.
4
+
5
+ - `pnpm check:types` + `pnpm check:lint` (oxlint) + `pnpm test:unit` is the fast gate;
6
+ In repos that ship `pnpm check:boundaries` (dependency-cruiser), that is the whole-graph
7
+ boundary/cycle check, also run by the pre-commit hook and CI.
8
+ - oxlint extends the platform's shared config (`./node_modules/@openora/core/oxlint/oxlintrc.json`)
9
+ - add local rules on top, never fork it.
10
+ - Don't work around a lint/boundary violation - fix the import.
11
+ - Agent rules are generated from `.rulesync/` via `pnpm gen:agents` - never hand-edit a generated
12
+ file.
@@ -0,0 +1,30 @@
1
+ # Errors
2
+
3
+ Read this before adding an error class, a catch, or a money-handling path.
4
+
5
+ - Fail fast at boundaries with typed errors (`CreateOrderSchema.parse(raw)` throws early).
6
+ - No silent catches - log with context and rethrow.
7
+ - Typed, named error classes via the shared factories (`makeNotFoundError`/`makeOwnershipError`/
8
+ `makeConflictError`); the router's `mapErrors` keys off the exported class.
9
+ - An overlay route that answers a vendor's own protocol, such as an inbound callback, throws
10
+ `wireErrorResponse(status, body, { cause })` from `@openora/core/server`: the reply carries that
11
+ status (400-599) and JSON body, not oRPC's error envelope. A 5xx reply is reported through its
12
+ `cause`; pass the error that caused it, or log it yourself.
13
+ - `ORPCError.message` is an English fallback for logs, not player-facing copy - UI copy keys off
14
+ `.code` plus typed `.data` fields.
15
+ - Money/critical paths are transactional AND idempotent: a DB guard inside the transaction, not
16
+ just an `idempotencyKey` (delivery is at-least-once).
17
+
18
+ ```ts
19
+ // bad - swallows the error, no context, no rethrow
20
+ try {
21
+ await chargeWallet(tx, amount);
22
+ } catch {
23
+ return null;
24
+ }
25
+ // good - a DB guard makes the mutation idempotent under at-least-once delivery
26
+ await db.transaction(async (t) => {
27
+ if (await ledgerExists(t, idempotencyKey)) return;
28
+ await insertLedger(t, { idempotencyKey, amount });
29
+ });
30
+ ```
@@ -0,0 +1,60 @@
1
+ # Frontend conventions
2
+
3
+ React/UI rules for this repo's `apps/*` UI apps and shared UI package. Read this before touching
4
+ `apps/web/**`, `apps/backoffice/**`, or `packages/ui/**`. Not applicable if this repo is
5
+ headless/api-only - delete this file and its row in `conventions`'s routing table in that case.
6
+
7
+ ## Component library and styling
8
+
9
+ - React Compiler is ON. Never hand-write `useMemo`/`useCallback`/`React.memo` - a compiler bail is
10
+ a Rules-of-React violation to fix. Exception: your shared UI package is consumed pre-built, so
11
+ hand-write stability there when it's part of a hook's contract (same reason `@openora/core/react`
12
+ does).
13
+ - Pick ONE component library and use its classes customized via utility classes + theme tokens.
14
+ Don't hand-roll what the library provides.
15
+ - Never hand-write component CSS selectors (`.btn-*{}`, `.table th{}`). A per-app `styles.css`
16
+ holds only the theme plus custom properties for raw values with no token; every other style is
17
+ a utility class on the element.
18
+ - App-specific looks live in that app's `styles.css`; shared UI components stay visually neutral.
19
+ - No hardcoded user-facing copy - every label goes through `t()` with a key in the module's
20
+ `locales/`. Pattern: `locales/index.ts` exports `export const ns = registerTranslations('<module>',
21
+ locales)`; components use `useTranslation(ns)`. Non-`en` files mirror `en.json` keys exactly.
22
+ - Theme tokens and CSS variables are declared once in the shared UI package - style with semantic
23
+ tokens (`bg-base-100`, `text-base-content`, `btn-primary`), never inline raw hex. A design color
24
+ with no token: add it to BOTH dark and light theme blocks. A new token without a design source:
25
+ ask the user first.
26
+ - Hoist long class strings into a module-scope `const styles = { ... } as const` keyed by role -
27
+ never inline long strings in JSX; merge with `cn()`.
28
+ - Extract a repeated utility-class recipe into a shared component or constant on the third
29
+ occurrence.
30
+ - Import helpers like `cn()` from the shared UI package barrel - never deep-import.
31
+ - Server state is not client state - key/cache/invalidate via the query lib, never
32
+ `useEffect(fetch)`, never shadowed in ad-hoc caches.
33
+ - No label or text is allowed to overflow its container - never leave text unconstrained by
34
+ default. Pick per element, based on whether the full text is load-bearing: `truncate` + a
35
+ `title` attribute holding the full string when a single line is the right shape and hiding the
36
+ tail is acceptable (a table cell, a narrow badge); `line-clamp-N` + a reserved `min-h-[NlH]` when
37
+ several lines are the right shape or the content differs in length across siblings that must
38
+ stay visually aligned (a KPI tile label, a card title) - the reserved height keeps every sibling
39
+ the same height whether or not its text actually wraps. A tooltip/popover with the full text is
40
+ the fallback only when neither fits the UX.
41
+
42
+ ## Modular architecture (every app)
43
+
44
+ - Feature modules live in `src/modules/<m>/` with the same internal folders: `pages/`,
45
+ `components/`, `hooks/`, `utils/`, `locales/`, and a public barrel `index.ts`.
46
+ - App-level (non-module) code splits the same way: `src/lib/` holds stateful/integration code
47
+ (API clients, SDK wrappers, config); `src/utils/` holds pure stateless helpers (formatters,
48
+ converters).
49
+ - No cross-module imports, ever. Cross-module communication is query invalidation or a domain
50
+ event, never a direct import.
51
+ - Outer composition code (`src/app/`, `src/routes/`, `extensions.config.ts`) imports modules only
52
+ through their barrel; deep-importing internals is forbidden. Files inside a module use relative
53
+ paths to siblings only, never `../../` out of the module.
54
+ - Components in `components/` and `pages/` are presentation-only: props in, JSX out - no
55
+ fetching, no side effects. Business logic, queries, and mutations live in `hooks/`, which accept
56
+ external dependencies (oRPC calls, API clients) as parameters.
57
+ - Next.js client components: `'use client'` on line 1 + `.client.tsx` suffix; server components
58
+ are the default (no marker). All-client apps (Vite/TanStack) use no suffix.
59
+ - One concept per file, exported name matches the kebab-case filename; split hooks and components
60
+ into separate files.
@@ -0,0 +1,38 @@
1
+ # Functions and modules
2
+
3
+ Read this before writing or refactoring a function, service method, or module.
4
+
5
+ - Pure functions with dependencies passed in as arguments; side effects only at the edges
6
+ (services, adapters, plugins, handlers) - never in helpers.
7
+ - Immutability: derive new objects (`{ ...user, roles: [...] }`), don't mutate.
8
+ - Construct objects by spread + override, never field-by-field hand-copy. Keep fields explicit
9
+ only for order-sensitive serialization, when the target must not receive some source fields, or
10
+ when null-vs-undefined matters at a boundary.
11
+ - A `class` is only a thin dependency-holding shell at a composition root; methods delegate to
12
+ pure functions. No inheritance for reuse, no decorators - compose.
13
+ - Short, single-purpose functions - if you'd write `// step 2` inside one, extract it.
14
+ - Guard clauses first, main path last.
15
+ - More than 3 parameters -> a single named-object param.
16
+ - Don't annotate a return type TypeScript can infer. Annotate only when: inference can't
17
+ (recursion), you deliberately widen/narrow, or it's the exported public API of a shared
18
+ `packages/*` module - there the explicit type IS the contract. Argument types always stay
19
+ explicit.
20
+ - Named exports only - no default exports (exceptions: `*.config.*` files, `plugin.ts` whose
21
+ loader reads `mod.default`, and Next.js App Router files; lint-enforced).
22
+
23
+ ```ts
24
+ // bad - the service reaches into a global/container to hide what it depends on
25
+ export class WalletService {
26
+ constructor(private readonly container: Container) {}
27
+ async deposit() {
28
+ const psp = this.container.get(PAYMENT_ADAPTER);
29
+ }
30
+ }
31
+ // good - deps are constructor params of their port type
32
+ export class WalletService {
33
+ constructor(
34
+ private readonly db: DrizzleService,
35
+ private readonly payments: PaymentAdapter,
36
+ ) {}
37
+ }
38
+ ```