@akanjs/cli 3.0.0-alpha.6 → 3.0.0-alpha.60

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 (111) hide show
  1. package/.build-stamp +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/{abstractCompactor-e5yq60qz.js → abstractCompactor-20e0cwbs.js} +2 -2
  5. package/{agent.command-b0fcx3r2.js → agent.command-tsx1nsdj.js} +9 -7
  6. package/{application.command-cezdnvtq.js → application.command-n8ex8g9h.js} +8 -6
  7. package/{applicationBuildRunner-ha3y8ew8.js → applicationBuildRunner-x8h12rp0.js} +7 -7
  8. package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-93nzdfsy.js} +1 -1
  9. package/buildBatch.proc.js +7 -5
  10. package/{capacitorApp-jaksw4cy.js → capacitorApp-8mv9e537.js} +29 -27
  11. package/{cloud.command-6xvsd487.js → cloud.command-f0rhw014.js} +12 -10
  12. package/{context.command-ajn1b683.js → context.command-s7a41qjs.js} +17 -15
  13. package/{dependencyScanner-f6grrep3.js → dependencyScanner-rbs2407b.js} +1 -1
  14. package/getDirname-ka11hm7e.js +8 -0
  15. package/{guideline.command-9ew6aqhm.js → guideline.command-qrw84ttb.js} +8 -6
  16. package/guidelines/agentRule/agentRule.generate.json +31 -0
  17. package/guidelines/agentRule/agentRule.instruction.md +525 -0
  18. package/guidelines/conventions/conventions.generate.json +25 -0
  19. package/guidelines/conventions/conventions.instruction.md +811 -0
  20. package/guidelines/cssRule/cssRule.instruction.md +7 -0
  21. package/guidelines/framework/framework.instruction.md +1 -0
  22. package/guidelines/mcpRule/mcpRule.generate.json +25 -0
  23. package/guidelines/mcpRule/mcpRule.instruction.md +176 -0
  24. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +24 -0
  25. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +638 -0
  26. package/incrementalBuilder.proc.js +8 -6
  27. package/{index-zr2wvx69.js → index-0g9ajdz8.js} +6 -6
  28. package/index-1577bej2.js +7 -0
  29. package/{index-wrrb8rxk.js → index-4mtyaqj2.js} +4 -4
  30. package/{index-bjpxzr6s.js → index-5vy2jzar.js} +1 -1
  31. package/{index-47qxw1tz.js → index-681sx6nt.js} +176 -66
  32. package/{index-zbx0jheb.js → index-8md8zszv.js} +14 -13
  33. package/{index-pfrt7qwh.js → index-cc1mgv1f.js} +4 -4
  34. package/{index-hk9rbcsz.js → index-ce9755av.js} +1 -1
  35. package/{index-jkynkyfk.js → index-ckpwq052.js} +5 -3
  36. package/{index-c94nq3c1.js → index-dvg362vq.js} +3 -3
  37. package/{index-vkqxw0ek.js → index-ek4mkwxp.js} +13 -13
  38. package/{index-exhpp31y.js → index-f0t3wjyr.js} +15 -7
  39. package/index-f39512t6.js +255 -0
  40. package/{index-m4kpmmtj.js → index-g44fykfw.js} +1 -1
  41. package/{index-nfxq39kj.js → index-h0mnt0b0.js} +37 -6
  42. package/{index-qp089jp0.js → index-j37qq1f2.js} +1 -1
  43. package/{index-hrwzz2wf.js → index-kkya2ped.js} +1 -1
  44. package/{index-j9cxndc0.js → index-qnv0aqmm.js} +9 -6
  45. package/{index-r7nqv8qj.js → index-r8z20vnj.js} +14 -14
  46. package/{index-6npsfpxr.js → index-ryns915y.js} +3 -3
  47. package/{index-0cj2zxbm.js → index-sh01r3pm.js} +1 -1
  48. package/{index-60a3jfcz.js → index-tn46rr6w.js} +8 -8
  49. package/{index-jn9vfpwz.js → index-wc19q8mb.js} +54 -15
  50. package/{index-meh7mmz9.js → index-x1dbhd80.js} +8 -4
  51. package/{index-qb61h0k7.js → index-xj5b8c0w.js} +193 -565
  52. package/index.js +21 -19
  53. package/{library.command-aa1g8nsa.js → library.command-p3jgf5h2.js} +6 -4
  54. package/{localRegistry.command-jha3sn50.js → localRegistry.command-bms0qn6t.js} +11 -9
  55. package/{module.command-b1zzgqde.js → module.command-hb3xm07r.js} +10 -8
  56. package/{package.command-9qv6vr9s.js → package.command-fbpa6r4w.js} +6 -4
  57. package/package.json +3 -4
  58. package/{page.command-k16pw244.js → page.command-92vrmw96.js} +6 -4
  59. package/{primitive.command-s60dn1ae.js → primitive.command-k6b9jfxr.js} +11 -9
  60. package/{quality.command-p3vaze52.js → quality.command-89vwmjvf.js} +194 -112
  61. package/{recipeScanner-tghkzrrt.js → recipeScanner-zrzev25x.js} +3 -3
  62. package/{repair.command-mfv749ha.js → repair.command-nh9jn8rh.js} +8 -6
  63. package/{routeSourceValidator-cncd92fv.js → routeSourceValidator-7fycdqyv.js} +3 -39
  64. package/{scalar.command-hs3nwzv1.js → scalar.command-zcsjgvax.js} +9 -7
  65. package/templates/app/env/env.server.debug.ts.template +1 -5
  66. package/templates/app/env/env.server.develop.ts.template +1 -5
  67. package/templates/app/env/env.server.local.ts.template +1 -5
  68. package/templates/app/env/env.server.main.ts.template +1 -5
  69. package/templates/app/page/styles.css.template +3 -3
  70. package/templates/appSample/srvkit/AuthGuard.ts +9 -5
  71. package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
  72. package/templates/crudPages/[__model__Id]/edit/page.tsx +1 -1
  73. package/templates/crudPages/new/page.tsx +1 -1
  74. package/templates/crudSinglePage/page.tsx +1 -1
  75. package/templates/env/_env.server.type.ts +7 -5
  76. package/templates/lib/db.ts +1 -1
  77. package/templates/lib/dict.ts +1 -1
  78. package/templates/lib/sig.ts +1 -1
  79. package/templates/lib/srv.ts +6 -2
  80. package/templates/module/__Model__.Util.tsx +1 -1
  81. package/templates/module/__Model__.Zone.tsx +3 -5
  82. package/templates/workspaceRoot/AGENTS.md.template +0 -971
  83. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  84. package/templates/workspaceRoot/biome.json.template +4 -239
  85. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
  86. package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
  87. package/{typeChecker-kravn7ns.js → typeChecker-g6bh0xfv.js} +1 -1
  88. package/typecheck.proc.js +1 -1
  89. package/{workflow.command-z0e0fk0z.js → workflow.command-jj0cphfm.js} +16 -14
  90. package/{workspace.command-3ydyy991.js → workspace.command-ma3g994j.js} +71 -52
  91. package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +0 -43
  92. package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +0 -34
  93. package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +0 -16
  94. package/templates/workspaceRoot/.cursor/rules/change-scope.mdc.template +0 -12
  95. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -18
  96. package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +0 -25
  97. package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +0 -44
  98. package/templates/workspaceRoot/.cursor/rules/domain-module-conventions.mdc.template +0 -17
  99. package/templates/workspaceRoot/.cursor/rules/format-lint.mdc.template +0 -12
  100. package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +0 -18
  101. package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +0 -61
  102. package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +0 -15
  103. package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +0 -21
  104. package/templates/workspaceRoot/.cursor/rules/repo-overview.mdc.template +0 -17
  105. package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +0 -58
  106. package/templates/workspaceRoot/.cursor/rules/secrets-and-env.mdc.template +0 -13
  107. package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +0 -83
  108. package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +0 -50
  109. package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +0 -59
  110. package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +0 -16
  111. /package/{index-r24hmh0q.js → index-yb2wsc7f.js} +0 -0
@@ -1,17 +0,0 @@
1
- ---
2
- description: Akan.js monorepo structure and ownership conventions
3
- alwaysApply: true
4
- ---
5
-
6
- # Repo Overview
7
-
8
- - Akan.js is a full-stack TypeScript framework for building all-stack applications at once.
9
- - Write one line and deploy across web, app, server, database, and infrastructure.
10
- - Akan prioritizes actual business code by abstracting technical implementation details as much as possible.
11
- - The goal is minimal code, high performance, and type-safe services that can deploy to web, mobile, server, and DB infrastructure together.
12
- - This is a Bun-first Akan.js monorepo.
13
- - Main top-level areas are `apps/`, `libs/`, `pkgs/`, and `infra/`.
14
- - `apps/<app>` contains app-level pages, domain code, UI, env files, and `akan.config.ts`.
15
- - `libs/*` contains shared domain and utility libraries.
16
- - `pkgs/akanjs/*` contains framework, runtime, and tooling facets published through the single `akanjs` package. Prefer extending existing Akan facets before adding new framework-level patterns.
17
- - `infra/` contains Helm, deployment templates, edge configs, and Jenkins env/secret scripts.
@@ -1,58 +0,0 @@
1
- ---
2
- description: Akan scalar and constant model conventions
3
- globs: **/*.constant.ts
4
- alwaysApply: false
5
- ---
6
-
7
- # Scalar Modeling
8
-
9
- - Define Akan models in `.constant.ts` files with `via` from `akanjs/constant`.
10
- - Use `Int` for whole-number counts and quantities; use `Float` only for values that need decimals.
11
- - Use `ID` for document references and prefer explicit structured fields over `Any` unless the content is genuinely flexible.
12
- - For date defaults, prefer a function such as `default: () => dayjs()` so the value is created at runtime.
13
- - Follow the established model layering pattern in this order: `Input`, `Object`, `Light<Model>`, full `<Model>`, and `<Model>Insight`. Write all five, and write `<Model>Insight` even when it is empty.
14
- - Put display and predicate logic on the `Light<Model>` class (`isNew()`, `canWrite(user?)`, `formatTimes()`) rather than in a util module — the Light class is the one both server and client hold. Collection-level helpers go `static` on the full model.
15
- - Defaults are a literal for scalars and a thunk for anything constructed. Arrays are `field([T])`; optional is the postfix `.optional()`.
16
- - Give any field whose business meaning is not obvious a short trailing comment.
17
-
18
- ## Scalar & Field Type Reference
19
-
20
- - **Import from `akanjs/base`** (real classes/helpers, not globals): `Int`, `Float`, `ID`, `Any`, `Upload`, `enumOf`, and the `dayjs` factory. There is **no `JSON` scalar** — use `Any` for open/flexible payloads.
21
- - **Use the JS globals directly (no import needed)**: `String`, `Boolean`, `Date`. They are monkey-patched to behave like scalars, so `field(String)` typechecks.
22
- - **`Number` is not a valid field/body type.** `NumberConstructor` is intentionally not augmented, so `field(Number)` / `.body("x", Number)` fails to typecheck. Use `Int` or `Float` instead.
23
- - Runtime resolution of every scalar (globals included) goes through `PrimitiveRegistry` by `refName` (`pkgs/akanjs/base/primitiveRegistry.ts`).
24
-
25
- ## Text Search Fields — the `text` role
26
-
27
- - A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and likewise
28
- `"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model switch.
29
- - Pick the role by what the value *is*, because `bm25` weights them positionally (`title` 10, `tag` 3, `desc` 1,
30
- `filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword list, `filter` is a
31
- scoping value (status, owner, role) that must be matchable but must never outrank a real title hit.
32
- - `thumb` is mirrored for rendering a hit and is **not** indexed — never expect it to match.
33
- - **A `secret`, `hidden`, or `resolve()` field with `text` throws at class-build time**, not at query time. That is
34
- deliberate: the mirror is plaintext, so an indexed secret would leak through search. Do not work around it. The
35
- same throw covers a `text` field *underneath* one of those — a scalar's own field is reachable through its parent,
36
- so `f.secret(Noti)` where `Noti.label` carries a role is rejected at the parent, not silently indexed.
37
- - The role works on a relation too (`image: field(File, { text: "thumb" })`) and on an array (`playing: field([String],
38
- { text: "tag" })`); an array of objects indexes by leaf key, including an array leaf (`works[*].tags`). A field
39
- inside a `Map` indexes nothing: there is no fixed path to extract it from.
40
- - Declaring roles is all the wiring there is. Mirror rows are maintained by SQL triggers — not document hooks —
41
- because `updateOneByQuery` and friends fire no hooks, and most searchable-field mutations go through exactly that
42
- path.
43
- - Search runs on sqlite/libsql only. `q.search()` against Postgres throws, loudly, rather than returning every row.
44
- - `AKAN_SEARCH_ENABLED=0` switches the index off process-wide; unset means on. It never deletes mirror data, and
45
- re-enabling reconciles every ref. **Give every process the same value** — a process cannot drop triggers for models
46
- it does not mount, so a mixed fleet leaves stale triggers behind.
47
- - The tokenizer is `AKAN_SEARCH_TOKENIZER` (or `database.search.tokenizer`, which wins), defaulting to
48
- `unicode61 remove_diacritics 2`. Changing it rebuilds the index from the mirror on the next boot — the model
49
- tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The
50
- rebuild takes no cross-process claim, so a fleet restarted at once repeats it in every process; stagger the
51
- restart when the mirror is large.
52
-
53
- ## Image & File Fields
54
-
55
- - **Do not declare `Upload` as a model field.** `Upload` is a signal-body-only primitive. Models reference the `File` model instead.
56
- - Declare an image/file field as a relation to `File`: `image: field(File).optional()` for one, `images: field([File])` for many (see `libs/shared/lib/user/user.constant.ts`, `libs/shared/lib/banner/banner.constant.ts`).
57
- - The store then auto-generates an `upload<Field>On<Model>(fileList)` action that calls the framework upload mutation and polls file status until it leaves `"uploading"` (`pkgs/akanjs/store/action.ts`).
58
- - Storage is wired through the `StorageAdaptor` DI role (default `BlobStorage`, `pkgs/akanjs/service/predefinedAdaptor/storage.adaptor.ts`); the reference implementation is the `file` lib (`libs/shared/lib/file/*`). Do not hand-roll data-URL fallbacks.
@@ -1,13 +0,0 @@
1
- ---
2
- description: Secret and environment file safety conventions
3
- globs: .env,**/.env,**/*.env,infra/**/*,**/*secret*,**/*Secret*,**/*credential*,**/*Credential*
4
- alwaysApply: false
5
- ---
6
-
7
- # Secrets And Env Safety
8
-
9
- - Never print, summarize, commit, or expose real secret values, credentials, tokens, private keys, or `.env` contents.
10
- - If env keys are needed for documentation, list only key names and example placeholders, not live values.
11
- - Preserve the existing env/secret flow through root scripts such as `bun run downloadEnv`, `bun run uploadEnv`, `bun run downloadSecret`, and `bun run uploadSecret`.
12
- - When editing infra env or secret scripts, keep Jenkins and deployment assumptions intact unless the task explicitly asks to change them.
13
- - Treat generated env/secret artifacts as sensitive even when they are not named `.env`.
@@ -1,83 +0,0 @@
1
- ---
2
- description: Akan service, signal, and adapter conventions
3
- globs: apps/**/lib/**/*.{service,signal}.ts,libs/**/lib/**/*.{service,signal}.ts,apps/**/server/**/*.ts,libs/**/server/**/*.ts
4
- alwaysApply: false
5
- ---
6
-
7
- # Service And Signal Conventions
8
-
9
- - Keep domain business operations in `.service.ts` classes built with `serve(...)`.
10
- - Keep execution contracts and triggers in `.signal.ts` classes built with `internal(...)`, `slice(...)`, and `endpoint(...)`.
11
- - Use `Internal` for internal triggers such as init, interval, cron, or queue jobs.
12
- - Use `Slice` for typed data views that feed client stores and zones; keep each slice focused on one purpose.
13
- - Use `Endpoint` for query and mutation contracts exposed to callers.
14
- - Connect external APIs or infrastructure through adapters, usually under `srvkit/`, and inject them into services instead of importing vendor clients directly into domain logic.
15
-
16
- ## Guards And Transports
17
-
18
- - Guards run on both HTTP and websocket calls. Read the caller with `context.get<T>("account")` (`pkgs/akanjs/signal/signalContext.ts`) instead of branching on `getHttpContext()` / `getWebSocketContext()`.
19
- - Slice-level `guards` only reach the generated query/mutation endpoints. A `pubsub`/`message` endpoint is unguarded unless it declares its own `guards` in its signal option.
20
- - A pubsub room is authorized once, at subscribe. When a socket's credential changes the framework re-runs each room's guards and unsubscribes the ones that now fail (`SignalResolver.revalidateWsRooms`), so guards must stay side-effect free and safe to re-run.
21
- - A websocket carries its credential in the handshake snapshot on `ws.data` (`AppWsData`); clients that hold the token in memory send it with `fetch.setJwt(...)`, which forwards an auth frame over the socket.
22
-
23
- ## Authorization Defaults
24
-
25
- - **Every `slice()` takes an explicit `{ guards: {…} }` second argument, and `root:` is always `Admin`.**
26
- - **Every custom `mutation` / `query` / `message` names its own `guards: [...]` array.** Never rely on the slice default. `Public` belongs on a slice `get:`, never on a mutation.
27
- - The acting user arrives via `.with(Self)` / `.with(CurrentUserId)` / `.with(Me)`. Never trust a client-supplied id.
28
- - Services re-check ownership even when a guard already gated the call — two independent gates.
29
- - Guard class shape, fail-closed rules, and the `static name = "User"` requirement live in the srvkit adapters and guards rule.
30
-
31
- ## Service Method Shape
32
-
33
- - Keep methods to a few lines: load → chain → `return await ….save()`. Write `return await` explicitly in tail position; do not "optimize" it away.
34
- - Side effects belong in `override async _preUpdate` / `_postCreate`, not inline. Fire-and-forget is explicitly `void`-ed.
35
- - Order deliberately: load every referenced document, then save, then notify.
36
- - Return `null` / `false` for "not allowed" or "not found" and let the signal decide whether that is an error.
37
- - `XInternal` → `XSlice` → `XEndpoint` are all declared even when empty, and `exec` is a one-liner delegating to the service.
38
-
39
- ## Signal Body Types
40
-
41
- - `.body(...)` / `.param(...)` args accept `ConstantFieldTypeInput` only: scalars, model refs, or `enumOf(...)`.
42
- - Numbers must use `Int` or `Float` — `Number` is rejected (`pkgs/akanjs/signal/endpointInfo.ts`).
43
- - `Upload` is valid only inside a mutation flagged for file upload: `mutation([cnst.File], { fileUpload: true }).body("files", [Upload])` (see `libs/shared/lib/file/file.signal.ts`). It is not a model field type.
44
-
45
- ## Reserved Endpoint Names
46
-
47
- - Auto-generated CRUD endpoints (e.g. `create<Model>`, `update<Model>`, `remove<Model>`) already exist for every model. Do not declare an `Endpoint`/`Slice` with a name that collides with them.
48
- - The service layer surfaces such a collision as a typecheck error, but the signal layer can pass sync/typecheck/build and fail only at runtime — so treat name collisions as errors regardless of whether the build is green.
49
-
50
- ## Slices, Queries, and Hydration
51
-
52
- - A slice's `exec` returns a `QueryOf` (an opaque query descriptor, `pkgs/akanjs/constant/types.ts`); you **cannot** chain `.sort()`/`.limit()` on it.
53
- - Apply ordering/paging via the store `init` fetch option instead: `initX(..., { sort, page, limit })` (`pkgs/akanjs/fetch/fetchType/sliceFetch.type.ts`).
54
- - Generated list accessors like `listBy(...)` return `Promise<Doc[]>`. For a chainable builder (`.sort().skip().limit().select()`) use the model facade's `findMany`/`findOne` (`FindManyChain`, `pkgs/akanjs/document/into.ts`).
55
- - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`); client fetch results are raw `GetStateObject` plain data (functions stripped, `pkgs/akanjs/base/types.ts`).
56
-
57
- ## Text Search In A Filter — `q.search()`
58
-
59
- - Text search is a filter query node like any other: `bySearch: filter().arg("text", String).query((text, q) =>
60
- q.search(text, { prefix: true }))`. The generated `listBySearch` / `countBySearch` / `queryBySearch` /
61
- `insightBySearch` come for free — you do **not** need a slice to make search usable.
62
- - **Only add a search slice when the model's data is safe to enumerate.** A filter is server-side; a slice is a
63
- client-callable endpoint, so on a model whose slice `get:` is `Public` a search slice hands anyone a way to walk the
64
- table. Leave that decision to the mounting app.
65
- - `q.search()` compiles to a JOIN, not a WHERE fragment, so it **must sit at an AND position**. Nesting it under
66
- `q.any()` or `q.not()` throws, and it is rejected in `updateOneByQuery` / `updateManyByQuery` — a query-level write
67
- takes no join, so ignoring the node would silently widen the write to every other matching row.
68
- - Blank or whitespace-only input matches **nothing**. Never "fix" that into a passthrough: a passthrough turns a
69
- search endpoint into a full listing.
70
- - Order by relevance with the built-in `relevance` sort key. It is an empty sort map, which the store reads as
71
- "unspecified": score order when a search join is present, `createdAt` descending otherwise. That fallback is the
72
- compiler's own, not a model-defined default — redefining `latest` on the model does not change it.
73
- - **A slice endpoint never reaches "unspecified".** The resolver fills `latest` before the query is built, so a
74
- client asking for the score order has to name `relevance`; leaving `sort` off gets `latest`, not relevance.
75
- - Scope a search with `columns` (`q.search(text, { columns: ["title"] })`) and re-weight with `weights`, a tuple of
76
- finite numbers positional over `["title", "desc", "tag", "filter"]`.
77
-
78
- ## Service / Signal Injection
79
-
80
- - Injected dependencies resolve by field-name convention: a field named `<refName>Service` resolves to the service registered under `<refName>`, and `<refName>Signal` likewise (`pkgs/akanjs/service/injectInfo.ts`).
81
- - The `Service`/`Signal` suffix is required — the injector strips it to derive the registry lookup key. Name the field after the target refName plus the suffix, not arbitrarily.
82
- - Preference order inside a service: `service<srv.XService>()` for another module's service · `plug(AdapterClass)` or `plug(StorageAdaptorRole)` for an adapter · `use<T>()` only to reach an `option.ts`-registered legacy singleton · `env(...)` for config.
83
- - New adapters are `adapt("name" as const, ({ use, env, plug, memory }) => ({…}))` classes in `srvkit/` that self-register — do not add them to `lib/option.ts`. See the srvkit adapters and guards rule.
@@ -1,50 +0,0 @@
1
- ---
2
- description: srvkit adapter (adapt/plug), guard, and error-handling conventions
3
- globs: apps/**/srvkit/**/*.ts,libs/**/srvkit/**/*.ts
4
- alwaysApply: false
5
- ---
6
-
7
- # Adapters — `adapt()` And `plug()`
8
-
9
- An injected singleton is an `adapt()` class in `srvkit/`. Write new adapters this way.
10
-
11
- ```ts
12
- export class AdminNoti extends adapt("adminNoti" as const, ({ use, env, plug, memory }) => ({
13
- discordApi: use<DiscordApi>(),
14
- workspaceRoot: env(() => `~/build/${getEnv().environment}`),
15
- masterHost: plug(MasterHost),
16
- tokenMap: memory(Map, { of: String }),
17
- })) {}
18
- ```
19
-
20
- - Inject it with `plug(TheClass)` from a service or from another adapter. Destructure only the injectors you use, and write the registration key `as const`.
21
- - **Do not register an `adapt()` class in `lib/option.ts`.** It self-registers, and `plug(Class)` uses the class itself as the token. `option.ts` is now only for legacy constructor-style adapters and for widening the options type.
22
- - `this.logger` is provided; never construct a `Logger` inside an `adapt()` class. Lifecycle work goes in `override async onInit()`.
23
- - `#private` is the house style here — the `#private` lint ban covers only `*.constant.ts`, `*.document.ts`, `*.service.ts`, and `*.store.ts`.
24
- - **`adapt()` is for singletons only.** A per-use value object stays a plain class you `new` at the call site. Ask whether there is exactly one per process that a service needs injected; if not, it is a plain class.
25
- - **Legacy shape — recognise it, do not copy it.** Plain classes with `constructor(options: XOptions)`, registered in `lib/option.ts` as `options.x ? new XApi(options.x) : null` and injected with `use<T>()`, still work. Migrate one to `adapt()` only when you are already changing it.
26
- - Preference order inside a service: `service<srv.XService>()` for another module's service · `plug(AdapterClass)` or `plug(StorageAdaptorRole)` for an adapter · `use<T>()` only to reach an `option.ts`-registered legacy singleton · `env(...)` for config.
27
-
28
- Conventions that hold for both shapes:
29
-
30
- - Route every remote call through one private `#api<T>(path, init?)` with `signal: AbortSignal.timeout(20_000)`.
31
- - Paginate with `for (let page = 1; ; page += 1)` broken by `if (pageItems.length < 100) break;`.
32
- - Resolve secrets as `process.env.X ?? options.x ?? deterministicGenerator(...)` **inside a function**, never at module scope.
33
- - Extend a function by appending an optional trailing parameter with a default, never by changing arity.
34
- - Parameters: up to three required primitives positional; optional flags in a trailing `{ … } = {}`; four or more parameters, or any two same-typed strings, in one named destructured object.
35
- - Release locks in `finally`. Load heavy optional dependencies through a module-level memoized promise (`sharpLoad ??= import("sharp")`).
36
-
37
- # Guards (`srvkit/guards.ts`)
38
-
39
- - Resource guards are `Can<Verb><Model>` classes that `implements Guard` with an `async canPass(context)`.
40
- - They **fail closed**: no resource named ⇒ `false`; a load that throws ⇒ `logger.warn` then `false`. Admin bypass goes first.
41
- - Keep `static name = "User";` on guard classes. `fetch` serializes guard names and the API explorer filters on them; it looks like dead code, and deleting it breaks the UI. Comment it so the next reader knows.
42
- - Guards ship with the library that owns the model and are imported by its own signals through the package path, so a mounting app inherits authorization and cannot forget it.
43
- - Services re-check ownership even when a guard already gated the call — two independent gates.
44
- - This file earns real comments: explain what would leak without each guard.
45
-
46
- # Error Placement
47
-
48
- - State-machine preconditions throw in `document.ts`; cross-document rules throw in `service.ts`; request-level policy belongs in signal guards.
49
- - Best-effort code returns a sentinel (`null`, `undefined`, `[0, 0]`, `{}`). There are no Result/Either wrappers.
50
- - `try/catch` is rare and always converts an exception into a decision, never swallows one. Guards catch → `logger.warn` → `return false`; adapters catch → `logger.error` → `return null`; UI uses `try/finally` to reset a spinner. A bodyless `catch {}` is acceptable only with a one-line reason.
@@ -1,59 +0,0 @@
1
- ---
2
- description: SSR-first rendering rules — keep JSX on the server and justify every "use client"
3
- globs: apps/**/*.tsx,libs/**/*.tsx
4
- alwaysApply: false
5
- ---
6
-
7
- # SSR First — Server Rendering Is The Default
8
-
9
- Akan is SSR-first. A JSX element rendered on the server ships as HTML and costs nothing to hydrate; an element
10
- behind `"use client"` ships twice — as markup and as bundled JS that re-runs in the browser. The boundary question
11
- is not which file *may* be client, it is **how little** ends up on the client side.
12
-
13
- - **The default is server. `"use client"` is a cost you justify per component, not a habit.** A component earns the
14
- directive only by using a client-only capability: a React hook, a JSX event handler, the store (`st.use.*` /
15
- `st.do.*`), a browser global, or a client-only third-party package. Rendering markup, mapping over data, reading a
16
- route param, and calling `l()` are server work — `usePage()` and `l()` never force a boundary.
17
- - The file role decides *where* the boundary sits (`Template`/`Zone`/`Util` are client, `Unit`/`View` are server);
18
- it does not decide how much markup sits behind it. A `Zone` that hoards markup is still an SSR regression.
19
- - Measure with `akan quality ssr` (`--format json` for tooling). It reports the server render share per app and lib.
20
- Treat **50% server share as the floor** and a falling share as a regression to justify or revert.
21
-
22
- ## What `akan quality ssr` Flags
23
-
24
- - `akan.ssr.unnecessary-use-client` — the directive is there but nothing in the file needs it. Delete it.
25
- - `akan.ssr.client-static-component` — a component in a client file renders real markup with zero client-only
26
- capability: pure server work sitting in the bundle.
27
- - `akan.ssr.client-static-markup` — a large subtree wraps one or two interactive touches. Split it.
28
- - `akan.ssr.client-mount-load` — a `useEffect(…, [])` loads server data the page could fetch first.
29
- - `akan.ssr.module-missing-server-view` — a module renders only from `Template`/`Zone`/`Util`, with no `Unit`/`View`.
30
- - `akan.ssr.template-client-state` — a `Template` holds form state in `useState` instead of the store.
31
-
32
- A third-party client package and the `ui/<Folder>/index_.tsx` + `lazy()` boundary both justify the directive and are
33
- not flagged. Interaction-driven `fetch.*` inside an `onClick` is not flagged either — only mount-time loads are.
34
-
35
- ## Server-Side Implementation Playbook
36
-
37
- 1. **Wrap the interaction, not the UI.** The smallest useful client component adds one behaviour and renders
38
- `children` untouched, so the markup inside never reaches the bundle:
39
- `export const ClickWrapper = ({ children, onPick }: ClickWrapperProps) => <div onClick={onPick}>{children}</div>;`
40
- 2. **Split compound components so panels stay on the server.** A tab or accordion needs client state only for
41
- *which* part is visible. `Tab` / `Tab.Menus` / `Tab.Menu` / `Tab.Panel` from `akanjs/ui` is the shape: only the
42
- provider and menu hold state, and `<Tab.Panel>` renders children as-is, so a server `Unit`/`View` passed in stays
43
- server-rendered. Never one client file with a mode `useState` and every panel body inlined.
44
- 3. **Sync state instead of fetching it.** The page calls `fetch.initTaskInTodo()` / `fetch.viewTask(id)` and passes
45
- the serializable result into a `Zone`; `Load.Units` / `Load.View` hydrate the store from it. Never a
46
- `useEffect(…, [])` that fetches on mount.
47
- 4. **Push the boundary down to the leaf that needs it.** A store-reading `Zone` holds zero markup and delegates to a
48
- server `View`: `export const Self = () => <Task.View.General task={st.use.task()} />;`
49
- 5. **Hand the promise across, not the awaited value.** `ClientInit` / `ClientView` are `PromiseOrObject<T>`, so a
50
- page may pass an unawaited `fetch.initTaskInTodo()` and `Load.*` resolves it behind a skeleton.
51
- 6. **Use named `ReactNode` slots, not just `children`.** `Layout.Navbar` takes `title`, `back`, `left`, `right`, and
52
- `children`, so a client shell composes server content in five places.
53
- 7. **Let the server do the derived work.** Display and predicate logic lives on `LightTask` (`isNew()`,
54
- `canWrite(user?)`); enum→class lookups live in a module-scope `as const` map.
55
- 8. **Gate auth on the server.** Check the session in `_layout.tsx` and redirect there, before any HTML is sent.
56
- 9. **Prefer CSS over client state for pure visibility.** A `data-*` attribute plus `group-data-[…]` variants, or
57
- `<details>`/`<summary>`, keeps both branches server-rendered.
58
- 10. **Keep the heavy island out of the first load.** Put a large client-only widget behind the
59
- `ui/<Folder>/index_.tsx` + `lazy()` pair.
@@ -1,16 +0,0 @@
1
- ---
2
- description: TypeScript and import conventions
3
- globs: **/*.{ts,tsx}
4
- alwaysApply: false
5
- ---
6
-
7
- # TypeScript And Imports
8
-
9
- - Use Bun and ESM assumptions from the root `tsconfig.json`.
10
- - Prefer path aliases over deep relative imports when crossing package boundaries.
11
- - Use `akanjs/*` for framework facets, `@apps/*` for apps, `@libs/*` for shared libs, and `@contract/*` for contract code.
12
- - Respect existing client/server entrypoints such as `@libs/shared/client`, `@libs/shared/server`, `@apps/myapp/client`, and `@apps/myapp/server`.
13
- - Let Biome organize imports instead of manually reshuffling unrelated imports.
14
- - Namespace the generated barrels in backend `.ts` files: `import * as cnst from "../cnst"`, `* as db`, `* as srv`. Use `import type * as srv` in services so the runtime graph stays lazy, and a value import in signals.
15
- - In `.tsx` files use one flat named import from the package client path (`import { cnst, fetch, st, Task, usePage } from "@apps/myapp/client"`) — never a relative `../` import.
16
- - Never import a third-party package from a page, a barrel, or a module file; re-export it through a one-line shim in `base/`, `webkit/`, or `ui/` first.
File without changes