@openora/create 0.4.1-canary.31 → 0.4.1-canary.310

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 (91) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/generated/core-version.d.ts +1 -1
  3. package/dist/generated/core-version.d.ts.map +1 -1
  4. package/dist/generated/core-version.js +1 -1
  5. package/dist/generated/core-version.js.map +1 -1
  6. package/dist/index.js +47 -92
  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 +7 -5
  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 +6 -1
  17. package/template/__dot__rulesync/commands/adr.md +32 -0
  18. package/template/__dot__rulesync/commands/check.md +11 -10
  19. package/template/__dot__rulesync/commands/doctor.md.tpl +17 -0
  20. package/template/__dot__rulesync/commands/scaffold-module.md +3 -2
  21. package/template/__dot__rulesync/commands/scaffold-plugin.md +19 -3
  22. package/template/__dot__rulesync/commands/scaffold-route.md +1 -0
  23. package/template/__dot__rulesync/commands/start.md +2 -2
  24. package/template/__dot__rulesync/hooks/_shared.mjs +5 -0
  25. package/template/__dot__rulesync/hooks/guard-core.mjs.tpl +75 -26
  26. package/template/__dot__rulesync/hooks/guard-generated.mjs +12 -5
  27. package/template/__dot__rulesync/hooks/guard-mr.mjs +81 -0
  28. package/template/__dot__rulesync/hooks/guard-subagent.mjs +22 -6
  29. package/template/__dot__rulesync/hooks/post-edit.mjs.tpl +106 -0
  30. package/template/__dot__rulesync/hooks.json +27 -4
  31. package/template/__dot__rulesync/rules/conventions.md +114 -0
  32. package/template/__dot__rulesync/rules/db-conventions.md +6 -72
  33. package/template/__dot__rulesync/rules/e2e-conventions.md +52 -0
  34. package/template/__dot__rulesync/rules/frontend-conventions.md.tpl +35 -0
  35. package/template/__dot__rulesync/rules/oss-boundaries.md.tpl +78 -0
  36. package/template/__dot__rulesync/rules/overview.md +18 -73
  37. package/template/__dot__rulesync/skills/create-plugin/{SKILL.md → SKILL.md.tpl} +13 -24
  38. package/template/__dot__rulesync/skills/create-pr/SKILL.md.tpl +38 -0
  39. package/template/__dot__rulesync/skills/create-task/{SKILL.md → SKILL.md.tpl} +14 -22
  40. package/template/__dot__rulesync/skills/create-ui-module/SKILL.md.tpl +60 -0
  41. package/template/__dot__rulesync/skills/enhance-prompt/{SKILL.md → SKILL.md.tpl} +4 -4
  42. package/template/__dot__rulesync/skills/handoff/SKILL.md +95 -0
  43. package/template/__dot__rulesync/skills/review/SKILL.md.tpl +277 -0
  44. package/template/__dot__rulesync/skills/ship/SKILL.md.tpl +149 -0
  45. package/template/__dot__rulesync/skills/ship/handoff.md.tpl +48 -0
  46. package/template/__dot__rulesync/skills/ship/watch-pr.mjs +168 -0
  47. package/template/__dot__rulesync/subagents/builder.md.tpl +115 -0
  48. package/template/__dot__rulesync/subagents/compliance-reviewer.md +78 -0
  49. package/template/__dot__rulesync/subagents/debugger.md.tpl +86 -0
  50. package/template/__dot__rulesync/subagents/deployer.md.tpl +54 -0
  51. package/template/__dot__rulesync/subagents/expert.md +28 -34
  52. package/template/__dot__rulesync/subagents/planner.md.tpl +39 -0
  53. package/template/__dot__rulesync/subagents/quality-reviewer.md.tpl +112 -0
  54. package/template/__dot__rulesync/subagents/security-reviewer.md.tpl +85 -0
  55. package/template/__dot__rulesync/subagents/tester.md.tpl +88 -0
  56. package/template/__dot__rulesync/sync.json.tpl +26 -0
  57. package/template/apps/api/package.json.tpl +1 -2
  58. package/template/apps/api/src/extensions.config.ts.tpl +12 -0
  59. package/template/apps/api/src/main.ts.tpl +1 -4
  60. package/template/apps/api/src/seed.ts.tpl +8 -1
  61. package/template/docker-compose.yml +13 -0
  62. package/template/docs/agents/cross-repo.md.tpl +37 -0
  63. package/template/docs/agents/forge.md.tpl +54 -0
  64. package/template/docs/agents/issue-tracker.md.tpl +42 -0
  65. package/template/docs/standards/comments.md +16 -0
  66. package/template/docs/standards/database.md +77 -0
  67. package/template/docs/standards/enforcement.md +12 -0
  68. package/template/docs/standards/errors.md +26 -0
  69. package/template/docs/standards/frontend.md +60 -0
  70. package/template/docs/standards/functions.md +38 -0
  71. package/template/docs/standards/git-delivery.md +20 -0
  72. package/template/docs/standards/module-structure.md +37 -0
  73. package/template/docs/standards/testing.md +72 -0
  74. package/template/docs/standards/types.md +31 -0
  75. package/template/package.json.tpl +11 -5
  76. package/template/rulesync.jsonc +1 -1
  77. package/template/tools/oss-worktree.mjs.tpl +105 -0
  78. package/template/tools/review-precheck.mjs +531 -0
  79. package/template/tools/sync-agents.mjs +257 -0
  80. package/template/turbo/generators/config.ts +1 -1
  81. package/template/turbo.json +14 -2
  82. package/template/__dot__rulesync/hooks/post-edit.mjs +0 -57
  83. package/template/__dot__rulesync/skills/add-feature/SKILL.md +0 -113
  84. package/template/__dot__rulesync/skills/add-feature/handoff.md +0 -58
  85. package/template/__dot__rulesync/skills/code-review/SKILL.md +0 -111
  86. package/template/__dot__rulesync/skills/create-pr/SKILL.md +0 -55
  87. package/template/__dot__rulesync/subagents/builder.md +0 -93
  88. package/template/__dot__rulesync/subagents/debugger.md +0 -83
  89. package/template/__dot__rulesync/subagents/deployer.md +0 -66
  90. package/template/__dot__rulesync/subagents/qa.md +0 -88
  91. package/template/apps/api/src/migrate.ts.tpl +0 -46
@@ -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,26 @@
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
+ - `ORPCError.message` is an English fallback for logs, not player-facing copy - UI copy keys off
10
+ `.code` plus typed `.data` fields.
11
+ - Money/critical paths are transactional AND idempotent: a DB guard inside the transaction, not
12
+ just an `idempotencyKey` (delivery is at-least-once).
13
+
14
+ ```ts
15
+ // bad - swallows the error, no context, no rethrow
16
+ try {
17
+ await chargeWallet(tx, amount);
18
+ } catch {
19
+ return null;
20
+ }
21
+ // good - a DB guard makes the mutation idempotent under at-least-once delivery
22
+ await db.transaction(async (t) => {
23
+ if (await ledgerExists(t, idempotencyKey)) return;
24
+ await insertLedger(t, { idempotencyKey, amount });
25
+ });
26
+ ```
@@ -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
+ ```
@@ -0,0 +1,20 @@
1
+ # Git and delivery
2
+
3
+ Read this before a commit, PR, or branch operation.
4
+
5
+ - Conventional commits, enforced by commitlint (husky locally, CI on the PR/MR title): `feat`, `fix`, `refactor`, `chore`,
6
+ `docs`, `test`, `ci`, `perf`. E.g. `feat(wallet): atomic debit command port`.
7
+ - Subject starts lowercase, acronyms included (`feat(pam): kyc status filter`). This applies to
8
+ the PR title too - squash merges derive the commit message from it.
9
+ - One PR = one concern. Stage files explicitly; never `git add -A` when foreign changes are in
10
+ the tree.
11
+ - Green before review: `pnpm verify` on the committed tree. It is the full gate and records the
12
+ verified commit; the `guard-mr` hook blocks opening the PR/MR without it.
13
+ - Branch off `dev`; never commit directly to a shared branch; never push without an explicit
14
+ per-action "yes push", except the ticket branch `/ship` opens its MR from.
15
+ - PR description carries intent: what / why / acceptance criteria / ticket link. No secrets,
16
+ internal hostnames, or PII - it is the public record.
17
+ - A PR that adds or changes a flow across components (request path, state machine, async job or
18
+ webhook, a new module, overlay or vendor binding) carries a Mermaid `flowchart` or
19
+ `sequenceDiagram` of it: nodes named as in the code, under ~15 nodes, refusal and failure
20
+ branches included. No flow, no diagram.
@@ -0,0 +1,37 @@
1
+ # Package and module structure
2
+
3
+ Read this before creating a package, an overlay add-on, or wiring a new module's internal layout.
4
+
5
+ - One package = one concern, named `@<scope>/<kebab>`, with an explicit `exports` map. The
6
+ entrypoint IS the public API; everything else is internal and off-limits to consumers
7
+ (`oss-boundaries`).
8
+ - Overlay/add-on packages mirror the platform module shape: `contract/`, `schema/`, `service/`,
9
+ `router/`, `adapters/`, `__tests__/`, plus `plugin.ts` at the root as the single wiring point.
10
+
11
+ | Layer | File | Holds | Must NOT hold |
12
+ | -------- | --------------------------- | --------------------------------------------------------------- | -------------------------------- |
13
+ | schema | `schema/index.ts` | Drizzle `pgTable`s; row types via `$inferSelect`/`$inferInsert` | logic |
14
+ | contract | `contract/index.ts` | oRPC route contract + req/res Zod schemas | logic, transport wiring |
15
+ | service | `service/<name>.service.ts` | ALL business logic; emits events after DB commit | HTTP/transport knowledge |
16
+ | router | `router/index.ts` | thin oRPC wiring: resolve caller, call service, map errors | business rules |
17
+ | plugin | `plugin.ts` | DI wiring only: `ctx.provide(...)`, route registration | logic |
18
+ | adapters | `adapters/<vendor>/` | concrete impls of adapter ports | being imported by another module |
19
+
20
+ - Each overlay owns its `drizzle.config.ts` and its own migration history - never share one
21
+ migration folder across packages.
22
+ - Pin exact dependency versions in every package; a package never depends on an app.
23
+ - Add a dependency deliberately - std lib or a few lines often beat a tree.
24
+
25
+ ```ts
26
+ // bad - a service reaches into the container and hides what it depends on
27
+ export class WalletService {
28
+ constructor(private readonly container: Container) {}
29
+ }
30
+ // good - deps are constructor params of their port type; plugin.ts does the resolving
31
+ export class WalletService {
32
+ constructor(
33
+ private readonly db: DrizzleService,
34
+ private readonly payments: PaymentAdapter,
35
+ ) {}
36
+ }
37
+ ```
@@ -0,0 +1,72 @@
1
+ # Testing
2
+
3
+ Read this before adding or restructuring a test.
4
+
5
+ ## When a test is written
6
+
7
+ A feature or fix PR carries **its own tests**, written in the same branch from the manual
8
+ verification pass that proves the change works. There is no separate test PR.
9
+
10
+ Before the PR opens:
11
+
12
+ 1. Drive the change against the running stack. Reach for the **Playwright CLI** first
13
+ (`playwright-cli` with `video-start` / `video-stop`, or a throwaway spec with
14
+ `test.use({ video: 'on' })` that clicks through the flow) - it costs a fraction of the tokens a
15
+ browser MCP does. Use a browser MCP only for a live console or network read the CLI cannot give
16
+ you.
17
+ 2. Capture the evidence: one short video per acceptance criterion, ending on the outcome it names,
18
+ and the before/after pair when you fixed something. A screenshot only where a video adds
19
+ nothing, such as a second theme. An API-only change attaches the request/response trace instead.
20
+ 3. Attach that evidence **to the PR description**, not only to your report - a reviewer confirms a
21
+ change by looking at it.
22
+ 4. Write the tests that pass exercised, in this branch, at the tier the table below names. Include
23
+ the hostile paths and the authz negatives. A fix carries the test that fails without it.
24
+ 5. End the description with a **"Tests"** list: one line per test added, naming its tier
25
+ (unit / API E2E / browser E2E) and path.
26
+
27
+ A changed behaviour with no test is a review `[WARN]`, never a `[BLOCK]`.
28
+
29
+ ## Pick the tier
30
+
31
+ Pick the OUTERMOST tier that can reach the behaviour - a test earns its keep by running real code,
32
+ not by being cheap.
33
+
34
+ | What you changed | Tier |
35
+ | ------------------------------------------------------------- | --------------------------------------------- |
36
+ | A screen, a form, a player/admin journey | Browser E2E (`apps/e2e/tests/<app>/**`) |
37
+ | An API route, an overlay, a vendor adapter, anything with SQL | API E2E (`apps/e2e/tests/api/**`) |
38
+ | A pure function - parser, resolver, mapper, money calculation | Unit (`src/__tests__/<name>.test.ts`, Vitest) |
39
+
40
+ The API tier drives the real API over HTTP against real Postgres, with each external vendor
41
+ replaced by a stub HTTP server the API is pointed at by env. It is the default tier for
42
+ `apps/api/src/extensions/**`: an overlay is wiring, and wiring is exactly what a unit test skips.
43
+
44
+ ## What is real, what is doubled
45
+
46
+ - **Anything that touches the database is tested against real Postgres.** Never fake a query
47
+ builder: a stubbed chain proves a call order, not a result, so it misses the regressions that
48
+ matter - a unique-index conflict, a wrong `where`, a lost race, a rollback that never happened.
49
+ - **External vendors are stubbed at their HTTP boundary** (a `node:http` stand-in the API's
50
+ base-URL env var points at), never by mocking our own adapter - a mocked adapter skips exactly
51
+ the wiring the test exists to check. Every vendor adapter therefore accepts a base-URL override.
52
+ - **A spy assertion is never the point of a test.** `expect(client.x).toHaveBeenCalledWith(...)`
53
+ only restates the line above it. A spy may stand in for an outbound vendor call, never for the
54
+ thing under test.
55
+
56
+ ```ts
57
+ // bad - a mocked builder chain proves a call order, not a result
58
+ const db = { select: vi.fn().mockReturnValue({ from: vi.fn().mockResolvedValue([row]) }) };
59
+ expect(db.select).toHaveBeenCalled();
60
+ // good - assert the outcome
61
+ expect(await service.get(id)).toEqual(row);
62
+ ```
63
+
64
+ ## How to write them
65
+
66
+ - Test behavior, not implementation - tests must survive a safe refactor (assert outputs, not
67
+ private caches).
68
+ - Cover the change in the same PR as the change; always include the authz negatives.
69
+ - Drive a vendor's inbound side the way the vendor does: post the real webhook shape to the real
70
+ route with a signature the stub's key material produces. Never call the adapter directly.
71
+ - Deterministic and isolated: no shared mutable state, no real outbound network, seedable data.
72
+ Own the rows a test creates and clean them up - the API suite shares one database.
@@ -0,0 +1,31 @@
1
+ # Types
2
+
3
+ Read this before adding or changing a schema, type, or enum-like value set.
4
+
5
+ - One source of truth per shape - infer, never hand-write a type that already exists:
6
+ `z.infer<typeof UserSchema>`, `typeof users.$inferSelect`, `Omit<User, 'id'>`.
7
+ - Schema-first at every boundary (HTTP, config, env, messages, events): validate once at the edge,
8
+ trust the type after. oRPC + Zod does this for routes; do the same elsewhere.
9
+ - Never re-infer a schema you imported - the owning contract exports the type once; import it.
10
+ - No `any` outside tests - `unknown` + narrowing.
11
+ - No `as` casts to silence the compiler - fix the root cause (`as const` is fine).
12
+ - Never `!` non-null assertions - narrow explicitly, or restructure so the value is provably
13
+ present (carry it on the object instead of re-looking it up).
14
+ - Under `noUncheckedIndexedAccess`: `.at()`, destructure-with-default
15
+ (`const [first = ''] = parts`), or an explicit guard - never `arr[i]!`.
16
+ - `type` over `interface` (lint-enforced).
17
+ - Type entity ids through their owning type (`playerId: Player['id']`), never a bare `string`.
18
+ - Derive related schemas with `.pick()/.omit()/.partial()/.extend()/.merge()` - never re-type
19
+ fields.
20
+ - Enum-like value sets are a values + schema + type triple declared once on the contract surface:
21
+ `X_STATUSES = [...] as const` -> `XStatusSchema = z.enum(X_STATUSES)` -> inferred `XStatus`.
22
+ Never a TS `enum`, never a second hand-typed copy - import the one from `@openora/*`.
23
+ - Make illegal states unrepresentable - discriminated unions
24
+ (`{ status: 'ok'; data } | { status: 'error'; error }`) over optional-flag soup.
25
+
26
+ ```ts
27
+ // bad - hand-written duplicate of an inferrable type
28
+ type User = { id: string; email: string; roles: string[] };
29
+ // good - infer from the schema that already validates the shape
30
+ type User = z.infer<typeof UserSchema>;
31
+ ```
@@ -6,15 +6,21 @@
6
6
  "scripts": {
7
7
  "dev": "turbo run dev",
8
8
  "build": "turbo run build",
9
- "typecheck": "turbo run typecheck",
10
- "lint": "oxlint .",
11
- "sync:agents": "rulesync generate",
12
- "prepare": "rulesync generate",
13
- "db:migrate": "pnpm -F @{{name}}/api db:migrate",
9
+ "check:types": "turbo run check:types",
10
+ "check:lint": "oxlint .",
11
+ "test:unit": "turbo run test:unit",
12
+ "verify": "turbo run check:types check:lint test:unit && node .rulesync/hooks/guard-mr.mjs --stamp",
13
+ "gen:agents": "rulesync generate",
14
+ "sync:agents": "node tools/sync-agents.mjs",
15
+ "oss:worktree": "node tools/oss-worktree.mjs",
16
+ "review:precheck": "node tools/review-precheck.mjs",
17
+ "prepare": "node tools/sync-agents.mjs && rulesync generate",
18
+ "db:migrate": "pnpm -F @{{name}}/api exec openora-migrate",
14
19
  "db:seed": "pnpm -F @{{name}}/api db:seed",
15
20
  "gen": "turbo gen"
16
21
  },
17
22
  "devDependencies": {
23
+ "@openora/create": "{{coreVersion}}",
18
24
  "@openora/mcp": "latest",
19
25
  "@turbo/gen": "2.9.14",
20
26
  "@types/node": "25.9.0",
@@ -1,6 +1,6 @@
1
1
  // rulesync config - single source of truth for all AI-agent instruction files.
2
2
  // Source lives in .rulesync/ (rules, subagents, commands, mcp.json).
3
- // Regenerate with `pnpm sync:agents`; CI checks with `pnpm sync:agents:check`.
3
+ // Regenerate with `pnpm gen:agents`.
4
4
  {
5
5
  "$schema": "https://github.com/dyoshikawa/rulesync/releases/latest/download/config-schema.json",
6
6
 
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ // A git worktree of the sibling OSS checkout - the only place an agent in this repo may change
3
+ // OSS code (the guard-core hook allows {{ossFromRoot}}/.worktrees/ and denies the rest of the
4
+ // checkout). Same command for every agent and every human, so a paired change looks the same
5
+ // on every machine.
6
+ //
7
+ // pnpm oss:worktree <branch> create or reuse the worktree, install its deps
8
+ // pnpm oss:worktree <branch> --link ...then build it and point `pnpm link:oss` at it
9
+ // pnpm oss:worktree <branch> --remove remove it; relinks the main checkout if it was linked
10
+ //
11
+ // Use the same branch name as this repo's branch: that is how skills pair the two requests.
12
+ import { execFileSync } from 'node:child_process';
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { join, relative, resolve } from 'node:path';
15
+ // oxlint-disable no-console
16
+
17
+ // This repo is often checked out as a git worktree, whose `.git` is a file pointing at
18
+ // <main>/.git/worktrees/<name>. The sibling OSS checkout sits next to the MAIN checkout, so
19
+ // resolve from there or every path below lands inside the worktree.
20
+ const mainCheckoutOf = (dir) => {
21
+ try {
22
+ const pointer = readFileSync(join(dir, '.git'), 'utf8');
23
+ const gitdir = pointer.match(/^gitdir:\s*(.*[/\\]worktrees[/\\][^/\\]+?)\s*$/m)?.[1];
24
+ return gitdir ? resolve(dir, gitdir, '..', '..', '..') : dir;
25
+ } catch {
26
+ return dir; // `.git` is a directory (the main checkout) or missing
27
+ }
28
+ };
29
+
30
+ const OSS = resolve(mainCheckoutOf(process.cwd()), '{{ossFromRoot}}');
31
+ const USAGE = 'usage: pnpm oss:worktree <branch> [--link | --remove]';
32
+
33
+ const die = (message) => {
34
+ console.error(`oss:worktree - ${message}`);
35
+ process.exit(1);
36
+ };
37
+
38
+ const [branch, flag] = process.argv.slice(2);
39
+ if (!branch || branch.startsWith('--') || (flag && !['--link', '--remove'].includes(flag))) {
40
+ die(USAGE);
41
+ }
42
+ if (!existsSync(join(OSS, '.git'))) {
43
+ die(`no OSS checkout at ${OSS} - clone it next to this repo first.`);
44
+ }
45
+
46
+ const worktree = join(OSS, '.worktrees', branch.replaceAll('/', '+'));
47
+ const worktreeFromHere = relative(process.cwd(), worktree);
48
+ // A fresh worktree has no node_modules yet, so the checkout's own post-checkout hook would run
49
+ // against tools it cannot resolve and fail the command. The install below does that setup.
50
+ const GIT = ['-C', OSS, '-c', 'core.hooksPath=/dev/null'];
51
+ const git = (...args) =>
52
+ execFileSync('git', [...GIT, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] }).trim();
53
+ const gitOrEmpty = (...args) => {
54
+ try {
55
+ return execFileSync('git', [...GIT, ...args], { encoding: 'utf8', stdio: 'pipe' }).trim();
56
+ } catch {
57
+ return '';
58
+ }
59
+ };
60
+ const run = (cmd, args) => execFileSync(cmd, args, { stdio: 'inherit' });
61
+ const hasLinkScript = () =>
62
+ Boolean(JSON.parse(readFileSync('package.json', 'utf8')).scripts?.['link:oss']);
63
+ const isLinkedTo = (dir) =>
64
+ existsSync('pnpm-workspace.yaml') &&
65
+ readFileSync('pnpm-workspace.yaml', 'utf8').includes(`link:${relative(process.cwd(), dir)}/`);
66
+
67
+ if (flag === '--remove') {
68
+ const wasLinked = isLinkedTo(worktree);
69
+ if (existsSync(worktree)) {
70
+ try {
71
+ git('worktree', 'remove', worktree);
72
+ } catch {
73
+ die(`${worktreeFromHere} has uncommitted changes - commit or discard them, then rerun.`);
74
+ }
75
+ }
76
+ if (wasLinked && hasLinkScript()) {
77
+ run('pnpm', ['link:oss']);
78
+ }
79
+ console.log(`removed ${worktreeFromHere}`);
80
+ process.exit(0);
81
+ }
82
+
83
+ if (!existsSync(worktree)) {
84
+ git('fetch', 'origin', '--quiet');
85
+ const base = gitOrEmpty('rev-parse', '--abbrev-ref', 'origin/HEAD') || 'origin/dev';
86
+ if (gitOrEmpty('rev-parse', '--verify', '--quiet', `refs/heads/${branch}`)) {
87
+ git('worktree', 'add', worktree, branch);
88
+ } else if (gitOrEmpty('rev-parse', '--verify', '--quiet', `refs/remotes/origin/${branch}`)) {
89
+ git('worktree', 'add', '--track', '-b', branch, worktree, `origin/${branch}`);
90
+ } else {
91
+ git('worktree', 'add', '--no-track', '-b', branch, worktree, base);
92
+ }
93
+ run('pnpm', ['-C', worktree, 'install', '--frozen-lockfile']);
94
+ }
95
+
96
+ if (flag === '--link') {
97
+ if (!hasLinkScript()) {
98
+ die('this repo has no `link:oss` script - link the worktree through pnpm overrides by hand.');
99
+ }
100
+ // @openora/* resolve from dist/, so a linked worktree works only once it is built.
101
+ run('pnpm', ['-C', worktree, 'build']);
102
+ run('pnpm', ['link:oss', worktreeFromHere]);
103
+ }
104
+
105
+ console.log(worktreeFromHere);