@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/generated/versions.d.ts +11 -0
- package/dist/generated/versions.d.ts.map +1 -0
- package/dist/generated/versions.js +11 -0
- package/dist/generated/versions.js.map +1 -0
- package/dist/index.js +48 -93
- package/dist/index.js.map +1 -1
- package/dist/render.d.ts +15 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +125 -0
- package/dist/render.js.map +1 -0
- package/package.json +8 -6
- package/template/README.md.tpl +12 -7
- package/template/__dot__claude/settings.json.tpl +22 -12
- package/template/__dot__env.example +10 -0
- package/template/__dot__gitignore +5 -0
- package/template/__dot__rulesync/commands/check.md +11 -10
- package/template/__dot__rulesync/commands/doctor.md.tpl +17 -0
- package/template/__dot__rulesync/commands/scaffold-module.md +3 -2
- package/template/__dot__rulesync/commands/scaffold-plugin.md +19 -3
- package/template/__dot__rulesync/commands/scaffold-route.md +1 -0
- package/template/__dot__rulesync/commands/start.md +2 -2
- package/template/__dot__rulesync/hooks/_shared.mjs +26 -13
- package/template/__dot__rulesync/hooks/guard-core.mjs.tpl +76 -28
- package/template/__dot__rulesync/hooks/guard-generated.mjs +15 -13
- package/template/__dot__rulesync/hooks/guard-mr.mjs +81 -0
- package/template/__dot__rulesync/hooks/guard-subagent.mjs +22 -6
- package/template/__dot__rulesync/hooks/post-edit.mjs.tpl +116 -0
- package/template/__dot__rulesync/hooks.json +27 -4
- package/template/__dot__rulesync/rules/conventions.md +99 -119
- package/template/__dot__rulesync/rules/db-conventions.md +6 -72
- package/template/__dot__rulesync/rules/e2e-conventions.md +33 -7
- package/template/__dot__rulesync/rules/frontend-conventions.md.tpl +35 -0
- package/template/__dot__rulesync/rules/oss-boundaries.md.tpl +78 -0
- package/template/__dot__rulesync/rules/overview.md +18 -82
- package/template/__dot__rulesync/skills/create-plugin/{SKILL.md → SKILL.md.tpl} +12 -23
- package/template/__dot__rulesync/skills/create-pr/SKILL.md.tpl +38 -0
- package/template/__dot__rulesync/skills/create-task/{SKILL.md → SKILL.md.tpl} +14 -22
- package/template/__dot__rulesync/skills/create-ui-module/{SKILL.md → SKILL.md.tpl} +12 -10
- package/template/__dot__rulesync/skills/enhance-prompt/{SKILL.md → SKILL.md.tpl} +4 -4
- package/template/__dot__rulesync/skills/handoff/SKILL.md +95 -0
- package/template/__dot__rulesync/skills/review/SKILL.md.tpl +277 -0
- package/template/__dot__rulesync/skills/ship/SKILL.md.tpl +157 -0
- package/template/__dot__rulesync/skills/ship/handoff.md.tpl +48 -0
- package/template/__dot__rulesync/skills/ship/watch-pr.mjs +172 -0
- package/template/__dot__rulesync/skills/ui-verify/SKILL.md.tpl +120 -0
- package/template/__dot__rulesync/skills/ui-verify/layout-check.check.sh +58 -0
- package/template/__dot__rulesync/skills/ui-verify/layout-check.js +163 -0
- package/template/__dot__rulesync/subagents/builder.md.tpl +115 -0
- package/template/__dot__rulesync/subagents/compliance-reviewer.md +78 -0
- package/template/__dot__rulesync/subagents/debugger.md.tpl +86 -0
- package/template/__dot__rulesync/subagents/deployer.md.tpl +54 -0
- package/template/__dot__rulesync/subagents/expert.md +28 -34
- package/template/__dot__rulesync/subagents/planner.md.tpl +39 -0
- package/template/__dot__rulesync/subagents/quality-reviewer.md.tpl +112 -0
- package/template/__dot__rulesync/subagents/security-reviewer.md.tpl +85 -0
- package/template/__dot__rulesync/subagents/tester.md.tpl +102 -0
- package/template/__dot__rulesync/sync.json.tpl +26 -0
- package/template/apps/api/package.json.tpl +2 -2
- package/template/apps/api/src/extensions.config.ts.tpl +12 -0
- package/template/apps/api/src/main.ts.tpl +1 -4
- package/template/apps/api/src/seed.ts.tpl +8 -1
- package/template/docker-compose.yml +13 -0
- package/template/docs/agents/cross-repo.md.tpl +37 -0
- package/template/docs/agents/forge.md.tpl +54 -0
- package/template/docs/agents/issue-tracker.md.tpl +42 -0
- package/template/docs/standards/comments.md +16 -0
- package/template/docs/standards/database.md +77 -0
- package/template/docs/standards/enforcement.md +12 -0
- package/template/docs/standards/errors.md +30 -0
- package/template/docs/standards/frontend.md +60 -0
- package/template/docs/standards/functions.md +38 -0
- package/template/docs/standards/git-delivery.md +20 -0
- package/template/docs/standards/module-structure.md +37 -0
- package/template/docs/standards/testing.md +88 -0
- package/template/docs/standards/types.md +31 -0
- package/template/package.json.tpl +15 -9
- package/template/rulesync.jsonc +3 -3
- package/template/tools/oss-worktree.mjs.tpl +105 -0
- package/template/tools/review-precheck.mjs +531 -0
- package/template/tools/sync-agents.mjs +257 -0
- package/dist/generated/core-version.d.ts +0 -2
- package/dist/generated/core-version.d.ts.map +0 -1
- package/dist/generated/core-version.js +0 -2
- package/dist/generated/core-version.js.map +0 -1
- package/template/__dot__rulesync/commands/doctor.md +0 -16
- package/template/__dot__rulesync/hooks/post-edit.mjs +0 -57
- package/template/__dot__rulesync/rules/oss-boundaries.md +0 -31
- package/template/__dot__rulesync/skills/add-feature/SKILL.md +0 -113
- package/template/__dot__rulesync/skills/add-feature/handoff.md +0 -58
- package/template/__dot__rulesync/skills/create-pr/SKILL.md +0 -55
- package/template/__dot__rulesync/skills/review/SKILL.md +0 -110
- package/template/__dot__rulesync/subagents/builder.md +0 -93
- package/template/__dot__rulesync/subagents/debugger.md +0 -82
- package/template/__dot__rulesync/subagents/deployer.md +0 -66
- package/template/__dot__rulesync/subagents/qa.md +0 -88
- package/template/__dot__rulesync/subagents/quality-reviewer.md +0 -67
- package/template/__dot__rulesync/subagents/security-reviewer.md +0 -54
|
@@ -3,132 +3,112 @@ root: false
|
|
|
3
3
|
targets:
|
|
4
4
|
- '*'
|
|
5
5
|
globs:
|
|
6
|
-
- '
|
|
7
|
-
|
|
6
|
+
- 'apps/**'
|
|
7
|
+
- 'packages/**'
|
|
8
|
+
- 'tools/**'
|
|
9
|
+
- '*.config.*'
|
|
10
|
+
- 'package.json'
|
|
11
|
+
description: Engineering code conventions (TS, DB, frontend, testing, git) - the always-on core, with a routing table to full detail in docs/standards/.
|
|
8
12
|
---
|
|
9
13
|
|
|
10
14
|
# Engineering conventions
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
The always-on core of the code standard: what you must obey while typing. Detail, examples, and
|
|
17
|
+
rationale live in `docs/standards/` - read the one file that matches the change instead of
|
|
18
|
+
carrying all of it. The enforced import graph lives in `oss-boundaries`; Playwright rules in
|
|
19
|
+
`e2e-conventions`; the always-on UI and SQL cores in `frontend-conventions` and `db-conventions`.
|
|
20
|
+
|
|
21
|
+
| Change you are making | Read first |
|
|
22
|
+
| ------------------------------------- | ------------------------------------ |
|
|
23
|
+
| schema, type, enum-like value set | `docs/standards/types.md` |
|
|
24
|
+
| function, service method, constructor | `docs/standards/functions.md` |
|
|
25
|
+
| new package, overlay, module layout | `docs/standards/module-structure.md` |
|
|
26
|
+
| a comment or JSDoc | `docs/standards/comments.md` |
|
|
27
|
+
| error class, catch, money path | `docs/standards/errors.md` |
|
|
28
|
+
| a test | `docs/standards/testing.md` |
|
|
29
|
+
| commit, PR | `docs/standards/git-delivery.md` |
|
|
30
|
+
| a failing gate, a new lint rule | `docs/standards/enforcement.md` |
|
|
31
|
+
| React/UI component, page, styling | `docs/standards/frontend.md` |
|
|
32
|
+
| SQL, Drizzle, migration, seed | `docs/standards/database.md` |
|
|
33
|
+
|
|
34
|
+
`docs/standards/frontend.md` doesn't apply to a headless/api-only repo - delete it (and its row
|
|
35
|
+
above) if this repo has no UI apps.
|
|
14
36
|
|
|
15
37
|
## Philosophy
|
|
16
38
|
|
|
17
|
-
- Functional and declarative: pure functions, immutable data, composition. No stateful classes, no
|
|
18
|
-
|
|
39
|
+
- Functional and declarative: pure functions, immutable data, composition. No stateful classes, no
|
|
40
|
+
imperative accumulation loops.
|
|
41
|
+
- Explicit over magic: no decorators, no auto-discovery, no reflection. Every wiring point is a
|
|
42
|
+
greppable typed call (`ctx.provide(TOKEN, factory)`).
|
|
19
43
|
- Self-documenting: clear names beat comments (`percentChange`, not `d` + a comment).
|
|
20
|
-
- YAGNI then DRY: don't build for imagined futures; abstract on the third occurrence, not the
|
|
44
|
+
- YAGNI then DRY: don't build for imagined futures; abstract on the third occurrence, not the
|
|
45
|
+
first.
|
|
21
46
|
- Boring and consistent: match the surrounding code's idiom, naming, and density.
|
|
22
47
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
-
|
|
63
|
-
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- One concept per file, exported name matches the kebab-case filename; split hooks and components into separate files.
|
|
91
|
-
|
|
92
|
-
## Package structure (`packages/*` and overlay packages)
|
|
93
|
-
|
|
94
|
-
- One package = one concern, named `@<scope>/<kebab>`, with an explicit `exports` map. The entrypoint IS the public API; everything else is internal and off-limits to consumers (`oss-boundaries`).
|
|
95
|
-
- Overlay/add-on packages mirror the platform module shape: `contract/`, `schema/`, `service/`, `router/`, `adapters/`, `__tests__/`, plus `plugin.ts` at the root as the single wiring point.
|
|
96
|
-
- Each overlay owns its `drizzle.config.ts` and its own migration history - never share one migration folder across packages.
|
|
97
|
-
- Pin exact dependency versions in every package; a package never depends on an app.
|
|
98
|
-
|
|
99
|
-
## Errors
|
|
100
|
-
|
|
101
|
-
- Fail fast at boundaries with typed errors (`CreateOrderSchema.parse(raw)` throws early).
|
|
102
|
-
- No silent catches - log with context and rethrow.
|
|
103
|
-
- Typed, named error classes via the shared factories (`makeNotFoundError`/`makeOwnershipError`/`makeConflictError`); the router's `mapErrors` keys off the exported class.
|
|
104
|
-
- `ORPCError.message` is an English fallback for logs, not player-facing copy - UI copy keys off `.code` plus typed `.data` fields.
|
|
105
|
-
- Money/critical paths are transactional AND idempotent: a DB guard inside the transaction, not just an `idempotencyKey` (delivery is at-least-once).
|
|
106
|
-
|
|
107
|
-
## Testing
|
|
108
|
-
|
|
109
|
-
- Co-locate as `src/__tests__/<name>.test.ts` (Vitest); service tests use a vi-mocked Drizzle.
|
|
110
|
-
- Test behavior, not implementation - tests must survive a safe refactor (assert outputs, not private caches).
|
|
111
|
-
- Cover new logic as part of the same change: unit tests for pure functions, authz negatives included.
|
|
112
|
-
- Deterministic and isolated: no shared mutable state, no real network, seedable data.
|
|
113
|
-
|
|
114
|
-
## Dependencies
|
|
115
|
-
|
|
116
|
-
- Pin exact versions - no `^`/`~`.
|
|
117
|
-
- No inline `fetch`/`axios` in module code - third-party integrations are ports + adapters bound at the composition root (`ctx.provide(KYC_ADAPTER, ...)`).
|
|
118
|
-
- Add a dependency deliberately - std lib or a few lines often beat a tree.
|
|
119
|
-
|
|
120
|
-
## Git and delivery
|
|
121
|
-
|
|
122
|
-
- Conventional commits, enforced by commitlint (husky + CI): `feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `ci`, `perf`. E.g. `feat(wallet): atomic debit command port`.
|
|
123
|
-
- Subject starts lowercase, acronyms included (`feat(pam): kyc status filter`). This applies to the PR title too - squash merges derive the commit message from it.
|
|
124
|
-
- One PR = one concern. Stage files explicitly; never `git add -A` when foreign changes are in the tree.
|
|
125
|
-
- Green before review: typecheck + lint + unit tests pass; `pnpm verify` is the full gate (adds format:check + boundaries + build).
|
|
126
|
-
- Branch off `dev`; never commit directly to a shared branch; never push without an explicit per-action "yes push".
|
|
127
|
-
- PR description carries intent: what / why / acceptance criteria / ticket link. No secrets, internal hostnames, or PII - it is the public record.
|
|
128
|
-
|
|
129
|
-
## Enforcement
|
|
130
|
-
|
|
131
|
-
- `pnpm check:types` + `pnpm check:lint` (oxlint) + `pnpm test:unit` is the fast gate; `pnpm check:boundaries` (dependency-cruiser) is the whole-graph boundary/cycle check, also run by the pre-commit hook and CI.
|
|
132
|
-
- oxlint extends the platform's shared config (`./node_modules/@openora/core/oxlint/oxlintrc.json`) - add local rules on top, never fork it.
|
|
133
|
-
- Don't work around a lint/boundary violation - fix the import.
|
|
134
|
-
- Agent rules are generated from `.rulesync/` via `pnpm gen:agents` - never hand-edit a generated file.
|
|
48
|
+
## Never (lint-enforced unless noted)
|
|
49
|
+
|
|
50
|
+
- `any` outside tests, `!` non-null assertions, `as` casts to silence the compiler (`as const` is
|
|
51
|
+
fine). Two sanctioned casts: test doubles through one `mock<T>()` helper (never inline in a
|
|
52
|
+
test), and a third-party inference boundary a library gives no honest way to satisfy (one cast,
|
|
53
|
+
one-line `// Library boundary:` note).
|
|
54
|
+
- `interface`, TS `enum`, decorators, inheritance for reuse, default exports (exceptions:
|
|
55
|
+
`*.config.*`, `plugin.ts`, Next.js App Router files).
|
|
56
|
+
- Hand-written duplicates of an inferrable type, re-inferring an imported schema, re-typing
|
|
57
|
+
derived schema fields, ad-hoc/duplicated Zod schemas outside a module's `contract/`.
|
|
58
|
+
- Re-exporting types "to be nice" - import from where defined.
|
|
59
|
+
- Inline `fetch`/`axios` in module code - third-party access is a port + adapter bound at the
|
|
60
|
+
composition root.
|
|
61
|
+
- Deep (`../../`+) relative imports that leave your module/zone, imports of another module's
|
|
62
|
+
internals, import cycles, deep `dist/`/`src/` paths into another package.
|
|
63
|
+
- SQL anti-patterns (bare `timestamp()`, CamelCase identifiers, hand-edited migrations) - detail
|
|
64
|
+
in `docs/standards/database.md`.
|
|
65
|
+
- Silent catches - log with context and rethrow. `ORPCError.message` rendered to a player - UI copy
|
|
66
|
+
keys off `.code` + typed `.data` through `t()`.
|
|
67
|
+
- Comments, unless one states a fact the code cannot contain (an external system's behaviour, a
|
|
68
|
+
spec constraint); a reason goes in the commit or PR, never inline. Never in tests. Detail:
|
|
69
|
+
`docs/standards/comments.md`.
|
|
70
|
+
- An in-process test that mocks the database, a repository, or a sibling service - found one in
|
|
71
|
+
the diff, delete it and replace it with the API E2E; never add to it.
|
|
72
|
+
|
|
73
|
+
## Always
|
|
74
|
+
|
|
75
|
+
- **Naming:** files `kebab-case.ts`, one concept per file, filename names the concept
|
|
76
|
+
(`wallet.service.ts`, never `helpers.ts`); types `PascalCase`; values/functions `camelCase`;
|
|
77
|
+
true global constants `SCREAMING_SNAKE_CASE`; Zod schemas `<Name>Schema` with inferred type
|
|
78
|
+
`<Name>`; booleans read as predicates (`isActive`, `canEdit`); money is a decimal string plus a
|
|
79
|
+
`currency` field alongside it, never `amountCents`; IO functions are verbs (`fetchInvoice`); names
|
|
80
|
+
carry units and intent (`delayMs`, `maxRetries`).
|
|
81
|
+
- **One source of truth per shape** - infer, never hand-write: `z.infer<typeof XSchema>`,
|
|
82
|
+
`typeof x.$inferSelect`. Full detail: `docs/standards/types.md`.
|
|
83
|
+
- **Literal config arrays/objects (option lists, key sets) use `as const`**, not an explicit union
|
|
84
|
+
type annotation - let TypeScript infer the literal types.
|
|
85
|
+
- **Schema-first at every boundary** (HTTP, config, env, events); validate once at the edge, trust
|
|
86
|
+
the type after.
|
|
87
|
+
- **Entity ids typed through their owning type** (`playerId: Player['id']`), never a bare
|
|
88
|
+
`string`.
|
|
89
|
+
- **Guard clauses first, main path last; more than 3 params -> one named object** (a leading
|
|
90
|
+
`tx` handle may stay positional). Always brace control statements, even one-liners (lint:
|
|
91
|
+
`curly`). Detail: `docs/standards/functions.md`.
|
|
92
|
+
- **Construct objects by spread + override**, never a hand-copied field list.
|
|
93
|
+
- **Side effects at the edges**; money paths are transactional AND idempotent (a DB guard inside
|
|
94
|
+
the transaction, not just an idempotency key). Detail: `docs/standards/errors.md`.
|
|
95
|
+
- **Typed, named error classes** from the shared factories, mapped to transport in the router's
|
|
96
|
+
`mapErrors`.
|
|
97
|
+
- **Pin exact dependency versions** (no `^`/`~`); add a dependency deliberately - std lib or a few
|
|
98
|
+
lines often beat a tree.
|
|
99
|
+
- **A feature or fix PR ships its own tests** - no separate test PR. It ships the change, the
|
|
100
|
+
manual verification pass that proves it (drive the running stack by hand, attach a short video
|
|
101
|
+
per acceptance criterion, or the request/response trace for an API-only change, to the PR
|
|
102
|
+
description), and the tests that pass exercised, listed in the description's "Tests" list.
|
|
103
|
+
Which tier each one belongs to, and how to run the manual pass: `docs/standards/testing.md`.
|
|
104
|
+
- **Green before review:** `/check` (typecheck + lint + unit tests) while iterating, `pnpm verify`
|
|
105
|
+
on the committed tree before the PR (the `guard-mr` hook refuses to open one until it passed on
|
|
106
|
+
HEAD). Conventional commits, lowercase subject (PR
|
|
107
|
+
title too - a squash merge turns it into the commit), one PR per concern. Never push without
|
|
108
|
+
explicit confirmation. The description carries what / why / acceptance criteria / bare ticket
|
|
109
|
+
key, the manual-verification evidence (a short video per acceptance criterion, before and after on
|
|
110
|
+
a fix) and the "Tests" list - but no CI checklist the pipeline already shows, no URLs,
|
|
111
|
+
hostnames, secrets, or PII. Detail: `docs/standards/git-delivery.md`.
|
|
112
|
+
- **Fix the import, never work around a lint or boundary violation.** Agent rules are generated
|
|
113
|
+
from `.rulesync/` via `pnpm gen:agents` - never hand-edit a generated file. Detail:
|
|
114
|
+
`docs/standards/enforcement.md`.
|
|
@@ -4,80 +4,14 @@ targets:
|
|
|
4
4
|
- '*'
|
|
5
5
|
globs:
|
|
6
6
|
- 'apps/api/**'
|
|
7
|
-
description: SQL / Drizzle conventions for tables an overlay or local add-on owns.
|
|
7
|
+
description: SQL / Drizzle conventions for tables an overlay or local add-on owns - the always-on core; full detail in docs/standards/database.md.
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Database conventions (SQL / Drizzle)
|
|
11
11
|
|
|
12
|
-
Applies to every table an overlay or local add-on owns (`apps/api/src/extensions/<name>/src/schema/`).
|
|
13
|
-
Tables live in `@openora/*` core for platform domains - never edit those; these rules govern the
|
|
14
|
-
tables you add. Boundary/import rules live in `oss-boundaries`; this file is SQL only.
|
|
12
|
+
Applies to every table an overlay or local add-on owns (`apps/api/src/extensions/<name>/src/schema/`). Platform-domain tables live in `@openora/*` core - never edit those. Identifiers, timestamps, keys, indexes, N+1, idempotency and migrations are specified in `docs/standards/database.md` - read it before touching a schema, query, migration, or seed. The rules below are the ones that file does not state.
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
// good - key derives the column; const camelCase; explicit snake_case only for table + index
|
|
23
|
-
export const playerNote = pgTable(
|
|
24
|
-
'player_note',
|
|
25
|
-
{
|
|
26
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
27
|
-
playerId: uuid().notNull(), // -> player_id
|
|
28
|
-
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
29
|
-
},
|
|
30
|
-
(t) => [index('player_note_player_id_idx').on(t.playerId)],
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
// bad - explicit/camelCase column names, PascalCase table
|
|
34
|
-
pgTable('PlayerNote', { player_id: uuid('playerId') });
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Timestamps - always timestamptz
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
createdAt: timestamp({ withTimezone: true }); // good - Postgres timestamptz, store UTC
|
|
41
|
-
createdAt: timestamp(); // bad - naive, drops the zone
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Every datetime column (`createdAt`, `updatedAt`, `expiresAt`, any `*At`) carries the zone.
|
|
45
|
-
|
|
46
|
-
## Keys, references, indexes
|
|
47
|
-
|
|
48
|
-
- UUID primary keys (`uuid().primaryKey().defaultRandom()`).
|
|
49
|
-
- **No foreign keys across a module/overlay boundary** - store a plain ID string and resolve via the
|
|
50
|
-
oRPC client or a schema subpath. FKs only within the same add-on.
|
|
51
|
-
- `NOT NULL` by default; push defaults to the DB (`.notNull().default(...)`), not app code.
|
|
52
|
-
- Index every column you filter or join on; name it `<table>_<cols>_idx`.
|
|
53
|
-
|
|
54
|
-
## Efficient operations
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
// bad - N+1
|
|
58
|
-
for (const id of ids) await db.select().from(wallet).where(eq(wallet.playerId, id));
|
|
59
|
-
// good - one batched query
|
|
60
|
-
await db.select().from(wallet).where(inArray(wallet.playerId, ids));
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
- Select only the columns you use; don't `select(*)` wide rows to read one field.
|
|
64
|
-
- **Build rows by spread + override, never hand-copy field-by-field.** When a row mostly mirrors a
|
|
65
|
-
validated input, spread it and set only the server-computed fields - `db.insert(x).values({ ...input,
|
|
66
|
-
id, createdAt })` - never re-list `field: input.field` per key (it silently drifts the moment a
|
|
67
|
-
column is added). Keep fields explicit only for: an order-sensitive hash/signature payload; a source
|
|
68
|
-
carrying columns the row must not receive (spread then omit them); or a null-vs-undefined boundary
|
|
69
|
-
that won't coerce (`actorId: input.actorId ?? null`).
|
|
70
|
-
- **Money / critical paths are transactional and idempotent** - a DB guard inside the transaction,
|
|
71
|
-
not just an `idempotencyKey` (delivery is at-least-once).
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
await db.transaction(async (t) => {
|
|
75
|
-
if (await ledgerExists(t, idempotencyKey)) return; // guard, not just a key
|
|
76
|
-
await insertLedger(t, { idempotencyKey, amountCents });
|
|
77
|
-
});
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Migrations
|
|
81
|
-
|
|
82
|
-
- Never hand-edit generated migrations. Change the `pgTable`, then `pnpm db:migrate`.
|
|
83
|
-
- One migration per schema change; review the generated SQL before committing.
|
|
14
|
+
- Money: exact decimal, never float, never a scaled integer. Every money column is `decimal()` (Postgres `NUMERIC`) - never `real`/`float`, never an `integer` "cents" column. Pair it with a `currency` column; on the wire use `MoneyAmountSchema` (decimal string) + `currency`, never `z.number()`. Balance math runs in SQL, never JS float arithmetic.
|
|
15
|
+
- `pgEnum` derives from a values + schema + type triple declared once (never an inline value array): `X_STATUSES = [...] as const` -> `XStatusSchema = z.enum(X_STATUSES)` -> `pgEnum('x_status', X_STATUSES)`.
|
|
16
|
+
- Bound the fan-out: never `Promise.all(rows.map(fn))` when `rows` is a query result and `fn` calls out per row (a vendor adapter, another service) - it opens unbounded concurrent connections/requests and starves everything else at scale. Use `mapConcurrent(items, limit, fn)` (`@openora/core/server`).
|
|
17
|
+
- Audit every mutation: each state-changing action emits a domain event the `audit` add-on subscribes to, or resolves `AUDIT_WRITER` and calls `record(...)`. A mutation with no audit trail is not done.
|
|
@@ -4,23 +4,49 @@ targets:
|
|
|
4
4
|
- '*'
|
|
5
5
|
globs:
|
|
6
6
|
- 'apps/e2e/**'
|
|
7
|
-
description: Playwright E2E conventions -
|
|
7
|
+
description: Playwright E2E conventions - specs run against the real stack; mocking is a narrow, justified exception.
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# E2E conventions (`apps/e2e`)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Applies once this repo has an `apps/e2e` Playwright suite; a repo without one is not covered by this rule, and adding the suite is what turns it on.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
This file governs **how** a spec is written. A feature or fix PR ships its specs in the same branch as the change, after the manual verification pass (`docs/standards/testing.md`).
|
|
15
|
+
|
|
16
|
+
Two kinds of spec live here:
|
|
17
|
+
|
|
18
|
+
- **Browser specs** (`tests/<app>/**`) - a player or admin journey through the UI.
|
|
19
|
+
- **API specs** (`tests/api/**`) - the `api` Playwright project: no browser, no `page`. Drives the real API over HTTP against real Postgres, with each external vendor replaced by a stub HTTP server the API is pointed at by env. This is the tier that proves an overlay end to end: request -> route -> service -> rows -> webhook -> resulting state. `USE_MOCKS` does not apply here at all.
|
|
20
|
+
|
|
21
|
+
## Real stack by default
|
|
22
|
+
|
|
23
|
+
Every spec runs against the real seeded stack - API, Postgres, and the apps under test - and that
|
|
24
|
+
run is what blocks merge. A test that never executes the backend cannot tell you the backend works.
|
|
25
|
+
|
|
26
|
+
`USE_MOCKS=true` intercepts responses in the browser and exists only for a state you cannot produce
|
|
27
|
+
for real: a third-party widget or redirect you are not allowed to run in CI. It is an exception you
|
|
28
|
+
justify per spec, never the mode a suite is written for. Anything reachable by seeding data or by
|
|
29
|
+
scripting a vendor stub is NOT such a state - script the stub instead, and prefer the API tier when
|
|
30
|
+
the behaviour under test is the backend's.
|
|
31
|
+
|
|
32
|
+
Existing mocked specs are converted as they are touched; delete the fixture with the last spec that
|
|
33
|
+
used it.
|
|
16
34
|
|
|
17
35
|
## Rules
|
|
18
36
|
|
|
19
37
|
- Import `test`/`expect` from `fixtures.ts`, NEVER from `@playwright/test` - fixtures add `cleanup()` and shared setup.
|
|
20
|
-
- `mockApi(page, routes)` (from `lib/mocks.ts`)
|
|
21
|
-
-
|
|
38
|
+
- Set up state through the product: seed data or drive the API, so the spec exercises the same path a player would. `mockApi(page, routes)` (from `lib/mocks.ts`) is the escape hatch above - reach for it only after the real setup is genuinely impossible, and never branch test logic on the mode.
|
|
39
|
+
- A mock that stays is typed: fixture shapes in `mocks/<domain>.ts` mirror the response contract - type platform-endpoint fixtures with the `z.infer` contract type from `@openora/*` so a contract change fails typecheck. No loose hand-written JSON.
|
|
22
40
|
- Selectors use `data-testid` - kebab-case, domain-prefixed (`chat-message`, `chat-input`). Never assert on CSS classes or component-library structure; shared UI components spread props so `data-testid` passes through.
|
|
23
41
|
- Page objects in `pages/<name>.page.ts` are functional factories - no classes, no `this`; locators + actions returned as a plain object.
|
|
24
42
|
- Test names are behavioural: "shows masked standings", not "table renders".
|
|
25
|
-
- `cleanup(fn)` reverses any
|
|
43
|
+
- `cleanup(fn)` reverses any data a test creates (runs in reverse order) - the suite shares one database, so a spec that leaks rows breaks the next one.
|
|
26
44
|
- Layout: `tests/<app>/<domain>/<scenario>.spec.ts` (one Playwright project per app, sharing fixtures/mocks/pages; run one with `--project=<app>`), `mocks/<domain>.ts`, `pages/<name>.page.ts`.
|
|
45
|
+
|
|
46
|
+
## API specs (`tests/api/**`)
|
|
47
|
+
|
|
48
|
+
- Use `request` (Playwright's APIRequestContext), never `page`. Assert status, body, and the state a follow-up request reports - not internals.
|
|
49
|
+
- A vendor stub is a plain `node:http` server under `apps/e2e/`, modelled on `mock-identity-server.mjs`: it answers only the vendor endpoints the flow reaches and records the calls a spec asserts on. Point the API at it with the vendor's base-URL env var; every vendor adapter must accept one.
|
|
50
|
+
- Drive the vendor's inbound side the way the vendor does - post the real webhook shape to the real route with a signature the stub's key material produces. Never call the adapter directly.
|
|
51
|
+
- Own your data: create the player the spec needs and reverse it with `cleanup(fn)`. The suite shares one database.
|
|
52
|
+
- Layout: `tests/api/<domain>/<scenario>.spec.ts`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
root: false
|
|
3
|
+
targets:
|
|
4
|
+
- '*'
|
|
5
|
+
globs:
|
|
6
|
+
- 'apps/web/**'
|
|
7
|
+
- 'apps/backoffice/**'
|
|
8
|
+
- 'packages/ui/**'
|
|
9
|
+
description: React/UI conventions (component library, theming, i18n, permission gating, modular architecture) for apps/web, apps/backoffice, packages/ui - applies once those apps exist; full detail in docs/standards/frontend.md.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Frontend conventions
|
|
13
|
+
|
|
14
|
+
Applies once this repo has UI apps (`apps/web`, `apps/backoffice`, `packages/ui`); an api-only repo has none of those paths and this rule is inert there.
|
|
15
|
+
|
|
16
|
+
UI-specific rules for `apps/web`, `apps/backoffice`, and `packages/ui`. Stack-agnostic rules (naming, types, functions, comments, errors, testing, git) live in `conventions`. Styling, i18n, and the modular architecture are specified in `docs/standards/frontend.md` - read it before touching a component, page, or module. The rules below are the ones that file does not state.
|
|
17
|
+
|
|
18
|
+
## Component library
|
|
19
|
+
|
|
20
|
+
- `{{scope}}/ui` is the component library: compose from its primitives (`Button`, `Dialog`, `Select`, `Switch`, `Tabs`, `Badge`, ...) plus utility classes on the theme tokens. Don't hand-roll what a primitive already provides; a genuinely missing one is added to `packages/ui/src/primitives/`.
|
|
21
|
+
- Theme tokens and CSS variables are declared once in `packages/ui/themes.css`; `cn()` and `registerTranslations()` come from the `{{scope}}/ui` barrel - never deep-import.
|
|
22
|
+
- `{{scope}}/ui` is consumed pre-built, so the React Compiler exception in `docs/standards/frontend.md` applies to it.
|
|
23
|
+
|
|
24
|
+
## Permission-gated UI
|
|
25
|
+
|
|
26
|
+
A page, nav item, tab, panel, or write action gated on a permission must check the exact resource/level the backend route actually asserts (`adminGuard.assert(resource, action)`) - never a looser, unrelated, or merely-plausible resource, and never a coarse "is admin" shortcut. Confirm the check against the route/service being called, not against what reads correctly at a glance.
|
|
27
|
+
|
|
28
|
+
- Gate at every layer the feature touches, and keep them in agreement: the nav entry, the route guard, the data-fetching hook's `enabled`, and the rendered branch. A mismatch between any two of these reopens the exact bug class this guards against - a request that fires (and 403s) before the UI hides itself, a control shown that fails on click, or a section rendering an empty/failed state instead of disappearing.
|
|
29
|
+
- Thread the permission into the query hook itself (`enabled: Boolean(id) && useHasPermission(resource, level)`), not only into the component that renders it - a hook with no grant check still fires for any caller that forgets the wrapper, including ones added later.
|
|
30
|
+
- A missing or denied permission hides the section outright - never a failed-to-load screen, an empty state standing in for "no access," or a disabled-but-visible control. See `conventions` > Testing: cover the authz-negative for every new gate as part of the same change.
|
|
31
|
+
|
|
32
|
+
## Modular architecture
|
|
33
|
+
|
|
34
|
+
- Outer composition code (`src/app/`, `src/routes/`) imports a module only as `@/modules/<name>`; files inside a module use relative paths to siblings.
|
|
35
|
+
- Cross-module communication is query invalidation, never a direct import.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
root: false
|
|
3
|
+
targets:
|
|
4
|
+
- '*'
|
|
5
|
+
globs:
|
|
6
|
+
- 'apps/**'
|
|
7
|
+
- 'packages/**'
|
|
8
|
+
- 'package.json'
|
|
9
|
+
- 'pnpm-workspace.yaml'
|
|
10
|
+
description: OSS core is read-only except in an OSS worktree (paired changes); enforced import/module boundaries.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# OSS core + import boundaries
|
|
14
|
+
|
|
15
|
+
## Never patch OSS core in place
|
|
16
|
+
|
|
17
|
+
`@openora/*` is a dependency - read it for reference, never patch it where it is installed.
|
|
18
|
+
|
|
19
|
+
- Do NOT edit `node_modules/**` or the main linked OSS checkout (`{{ossDir}}`). The `guard-core` hook denies those writes in every agent tool; don't route around it with `sed`, redirection, or scripts. A patched dependency is lost on reinstall and diverges from the published package, and other sessions build against the main checkout.
|
|
20
|
+
- Extend from the OUTSIDE first: overlay plugins, adapter rebindings, UI plugins, config.
|
|
21
|
+
|
|
22
|
+
## Changing OSS core (paired change)
|
|
23
|
+
|
|
24
|
+
When a fix can only live in core, change it in core - in a git worktree of `{{ossDir}}`, the one OSS path the hook lets you write. Works the same from any agent tool; `docs/agents/cross-repo.md` lists how each tool is granted access to `{{ossDir}}`.
|
|
25
|
+
|
|
26
|
+
**Scope.** Any core change can be made here, in the worktree: a bug, a missing export or field, a contract alignment, or a new feature or module. Use the `handoff` skill only when the work should run in a separate session rooted in the OSS repo.
|
|
27
|
+
|
|
28
|
+
**Genericity test - before the first edit.** Core serves every operator, and this session carries this operator's requirements. Write down, for the OSS PR's Why section: why it cannot be an overlay, adapter, or config here, and why another operator would want the same change. If only this operator needs it, core gets only the seam (an adapter token, an event, a config field, a hook) and the behavior stays in this repo. Jurisdiction rules, vendor specifics, and limits or flows only this operator uses never go into core.
|
|
29
|
+
|
|
30
|
+
1. `pnpm oss:worktree <branch>`, with this repo's branch name: the same name in both repos is how skills pair the two requests. It creates or reuses `{{ossDir}}/.worktrees/<branch, / as +>` and installs its dependencies. `--link` also builds it and points `pnpm link:oss` at it, so this repo runs against the change; rebuild with `pnpm -C <worktree> build` after each core edit.
|
|
31
|
+
2. Before the first edit there, read `<worktree>/AGENTS.md`, every `<worktree>/.rulesync/rules/*.md`, and the `<worktree>/docs/standards/` file for the kind of change. Files in the worktree follow the OSS rules; files in this repo follow this repo's rules. On conflict, the file's location decides.
|
|
32
|
+
3. Review before any push. The OSS repo's own edit hooks (format, lint, typecheck) and its reviewer agents do not run from this session, so do their work yourself: run `pnpm -C <worktree> verify`, then review the worktree diff by `<worktree>/docs/standards/skills/review.md`, applying the checklists in `<worktree>/.rulesync/subagents/contract-reviewer.md` and, for money, auth, KYC, or RG paths, `security-reviewer.md`. Fix every BLOCK, and re-check the genericity test against the final diff.
|
|
33
|
+
4. Commit and open the OSS pull request from the worktree, per `<worktree>/docs/standards/skills/delivery.md`, with its own explicit push confirmation. The OSS repo is public: its commits and PR carry the bare ticket key at most - no operator name, no internal URL, no link to this repo's request, and no ticket text or operator-specific domain detail. Describe the change in generic terms.
|
|
34
|
+
5. Do not link the two requests - the shared branch name pairs them. This repo's request may be read by people outside the team, so it never names or links the OSS repo.
|
|
35
|
+
6. Never `git checkout` a branch in the main `{{ossDir}}` checkout. Remove the worktree with `pnpm oss:worktree <branch> --remove` once both requests have merged.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Import boundaries (enforced)
|
|
39
|
+
|
|
40
|
+
Enforced by `pnpm check:lint` (oxlint, per-edit), the pre-commit hook, CI, and the agent PostToolUse hook - plus `pnpm check:boundaries` (dependency-cruiser, one graph per app/package via turbo) and `pnpm check:shape` in repos that ship them. Fix the import, never work around a violation.
|
|
41
|
+
|
|
42
|
+
### Across packages
|
|
43
|
+
|
|
44
|
+
- No deep OSS imports: `@openora/*/src/*` or `/dist/*` - import only the package entrypoint (oxlint owns this one; every published entrypoint resolves into `dist/`, so the boundary graph cannot tell the two apart).
|
|
45
|
+
- No deep `{{scope}}/ui` imports - only the barrel.
|
|
46
|
+
- No app-to-app imports (`apps/web` <-> `apps/backoffice` <-> `apps/api`) - extract shared code to `packages/*`. Never reach across with a relative `../../apps/` path either.
|
|
47
|
+
- No deep workspace-package imports - only the index entrypoint.
|
|
48
|
+
- No import cycles.
|
|
49
|
+
- A shared package (`packages/*`) must not import from an app.
|
|
50
|
+
- `{{scope}}/ui` is renderer- and router-agnostic: no `next`, no router package. Take the capability through an injected adapter (see `navigation.tsx`).
|
|
51
|
+
- No Node builtins in browser code (the Next instrumentation/proxy entries and `src/lib/api-server.*` are the declared exceptions).
|
|
52
|
+
|
|
53
|
+
### Inside an app (ADR-0001)
|
|
54
|
+
|
|
55
|
+
- No cross-module imports; a module is reached only through its barrel `index.ts`.
|
|
56
|
+
- Layering inside a module: `pages/` is the leaf - `components/`, `hooks/`, `utils/` must not import it; `utils/` must not import `components/`, `hooks/` or `pages/`.
|
|
57
|
+
- A file inside a module must not import its own barrel.
|
|
58
|
+
- The shared kernel (`src/lib`, `src/hooks`, `src/utils`, `src/components`) must not import a feature module - dependencies point inwards.
|
|
59
|
+
- Routes are leaves: nothing may import `src/app/**` (web) or `src/routes/**` / `routeTree.gen.ts` (backoffice, except `main.tsx`).
|
|
60
|
+
- Transport clients (`@orpc/client`, `@orpc/openapi-client`, `@orpc/tanstack-query`) are constructed in `src/lib/` only.
|
|
61
|
+
- A `'use client'` file must not import server-only code (`next/headers`, `src/lib/api-server.*`, `*.server.ts`).
|
|
62
|
+
|
|
63
|
+
### Dependency manifest hygiene
|
|
64
|
+
|
|
65
|
+
- Every import resolves; every npm package used is declared in that package's own `package.json`; no package sits in two dependency sections (a library's devDependency + peerDependency pair is the exception).
|
|
66
|
+
- Production code imports no devDependency and no test/mock/fixture file.
|
|
67
|
+
|
|
68
|
+
### Structure checks (`pnpm check:shape`, in repos that ship it)
|
|
69
|
+
|
|
70
|
+
Runs the checks a dependency graph cannot make, because they are about files that do not exist or edges that do not exist:
|
|
71
|
+
|
|
72
|
+
- Every module has `index.ts`. An `AGENTS.md` is optional - only for knowledge local to the module that the rules and the code do not show. A `check:shape` that still requires one drops that assertion with this rule, or the gate contradicts it.
|
|
73
|
+
- The barrel re-exports only; the single side effect it may carry is `import './locales'`.
|
|
74
|
+
- Every file under a module's `pages|components|hooks|utils` is reachable from the barrel, and every `packages/ui/src` file from the `{{scope}}/ui` barrel - otherwise it is dead code.
|
|
75
|
+
|
|
76
|
+
Each app/package owns its own `.dependency-cruiser.cjs` (its own tsconfig for `@/*` alias resolution) built on the shared rule/option helpers in `.dependency-cruiser.shared.cjs`. Where the repo ships `pnpm gen:boundaries-graph`, it renders each package's graph (needs Graphviz).
|
|
77
|
+
|
|
78
|
+
The shared options keep npm packages in the graph on purpose - a dependency-type rule can only judge a package that is a node - so build output is excluded per workspace path, never as a bare `/dist/`.
|