@starci/eslint-canon-fe 8.2.13 → 8.2.15

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 (48) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +1 -0
  3. package/client-boundary.mjs +66 -23
  4. package/docs/client-boundary.md +9 -9
  5. package/docs/hooks-folder.md +16 -4
  6. package/docs/hygiene.md +2 -2
  7. package/docs/transport.md +26 -26
  8. package/docs/type-safety.md +6 -6
  9. package/fe-stack.mjs +86 -0
  10. package/formatting.mjs +12 -6
  11. package/hooks-folder.mjs +73 -31
  12. package/hygiene.mjs +4 -4
  13. package/icon.mjs +25 -17
  14. package/index.mjs +2 -0
  15. package/lib/fe-stack.mjs +73 -0
  16. package/lib/scope.mjs +23 -2
  17. package/lib/why.mjs +23 -13
  18. package/next-conventions.mjs +4 -3
  19. package/package.json +1 -1
  20. package/runtime/knowledge/hfs/canon-pins.yaml +207 -15
  21. package/runtime/knowledge/hfs/fe-stack.yaml +218 -0
  22. package/runtime/knowledge/hfs/slots.yaml +319 -179
  23. package/runtime/knowledge/patterns/fe/folder.yaml +24 -18
  24. package/runtime/knowledge/sonar-gate.yaml +10 -2
  25. package/runtime/modules/kernel/removed-vocabulary.yaml +48 -0
  26. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +0 -2
  27. package/runtime/scripts/hfs/architecture/config.mjs +3 -2
  28. package/runtime/scripts/hfs/architecture/dead-exports.mjs +6 -2
  29. package/runtime/scripts/hfs/architecture/error-masked.mjs +10 -1
  30. package/runtime/scripts/hfs/architecture/frontend.mjs +10 -3
  31. package/runtime/scripts/hfs/architecture/hfs.mjs +6 -3
  32. package/runtime/scripts/hfs/architecture/index.mjs +2 -0
  33. package/runtime/scripts/hfs/architecture/owners.mjs +19 -16
  34. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +4 -1
  35. package/runtime/scripts/hfs/architecture/server-action.mjs +75 -20
  36. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +2 -2
  37. package/runtime/scripts/hfs/architecture/supabase.mjs +36 -210
  38. package/runtime/scripts/hfs/architecture/tiers.mjs +1 -33
  39. package/runtime/scripts/hfs/architecture/transport-owner.mjs +110 -54
  40. package/runtime/scripts/hfs/architecture/type-context.mjs +4 -1
  41. package/runtime/scripts/hfs/edition-slots.mjs +13 -4
  42. package/runtime/scripts/hfs/manifest-shape.mjs +3 -3
  43. package/runtime/scripts/hfs/rule-catalog.mjs +21 -1
  44. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +4 -5
  45. package/served-locale.mjs +10 -13
  46. package/the-split.mjs +5 -2
  47. package/transport.mjs +81 -67
  48. package/runtime/scripts/hfs/architecture/supabase-results.mjs +0 -182
package/lib/why.mjs CHANGED
@@ -11,7 +11,7 @@
11
11
  * `fix` strings are the keys of `modules/i18n/messages/canon.yaml`, so `translator('vi')(entry.en)` and
12
12
  * `translator('vi')(entry.fix)` hand back the Vietnamese of the same sentence.
13
13
  *
14
- * Every rule that carries a catalogue id (R18, R22, R49-R59, R60-R62, R65) has an entry;
14
+ * Every rule that carries a catalogue id (R18, R22, R49-R59, R60-R62, R65, the fe-stack catalog) has an entry;
15
15
  * the twin test refuses a rule of those laws with no entry and an entry for a rule that does not exist.
16
16
  */
17
17
 
@@ -31,12 +31,12 @@ export const why = {
31
31
  },
32
32
  "fetch-only-in-api-client": {
33
33
  code: "FE_TRANSPORT_OWNER",
34
- en: "`fetch` (or `Request`, `EventSource`, `sendBeacon`, another HTTP library) in `<file>` sits outside the one client (`modules/api/client.ts`, or `src/client.ts` of the shared api package). A repo has exactly one transport.",
35
- fix: "Call the repo's client and receive `Outcome<T>`; do not call `fetch` yourself.",
34
+ en: "`<file>` reaches the back end outside the API layer: `fetch` (or `Request`, `EventSource`, `sendBeacon`, another HTTP library) outside `modules/api`, or Apollo, axios or socket.io-client outside its home (`modules/api`, `modules/api/rest`, `hooks/socketio`).",
35
+ fix: "Call an operation of `modules/api` (Apollo GraphQL, or axios in `modules/api/rest`) through its SWR hook in `hooks/swr`; listen to realtime through `hooks/socketio`.",
36
36
  },
37
37
  "client-fetch-has-signal": {
38
38
  code: "FE_TRANSPORT_OWNER",
39
- en: "`fetch` in `<file>` has no `signal`. The client must carry a timeout and an `AbortSignal`.",
39
+ en: "`fetch` in `<file>` has no `signal`. Every request of the API layer must carry a timeout and an `AbortSignal`.",
40
40
  fix: "Pass `signal` (`AbortSignal.timeout(...)` combined with the caller's signal).",
41
41
  },
42
42
  "no-shared-transport-state": {
@@ -56,8 +56,8 @@ export const why = {
56
56
  },
57
57
  "no-hand-typed-wire": {
58
58
  code: "FE_WIRE_GENERATED",
59
- en: "`<file>` hand-types a wire type or casts a response. Use the types generated from `be/contracts/`.",
60
- fix: "Run codegen from the be contract snapshot (`be/contracts/`) and import the generated types; put GraphQL documents in `.graphql` files.",
59
+ en: "`<file>` hand-types a wire type, casts a response, or writes a GraphQL document outside the API layer. Use the types generated from `be/contracts/`.",
60
+ fix: "Run codegen from the be contract snapshot (`be/contracts/`) and import the generated types; write each GraphQL document in its operation file under `modules/api/graphql/queries` or `modules/api/graphql/mutations`.",
61
61
  },
62
62
  "use-client-only-at-boundary": {
63
63
  code: "FE_CLIENT_BOUNDARY",
@@ -66,8 +66,8 @@ export const why = {
66
66
  },
67
67
  "hooks-folder-holds-hooks-only": {
68
68
  code: "FE_HOOKS_ARE_HOOKS",
69
- en: "`<file>` inside `hooks/` is not a React hook. A server reader goes to `modules/api`, a helper to `<domain>.shared.ts`.",
70
- fix: "One hook per `use*.ts` file; a shared helper goes in `<domain>.shared.ts`; a server reader goes in `modules/api/<domain>/read-*.ts`.",
69
+ en: "`<file>` inside `hooks/` is not a React hook, or is not a file its hooks family names. A call to the back end goes to `modules/api`, a helper to `<domain>.shared.ts`.",
70
+ fix: "One hook per `use*.ts` file under the name its family gives it (`hooks/swr/api/graphql/queries/useQuery<X>Swr.ts`, `hooks/rhf/use<X>Form.ts`, ...); a shared helper goes in `<domain>.shared.ts`; a back-end call is an operation of `modules/api`.",
71
71
  },
72
72
  "no-middleware-file": {
73
73
  code: "FE_NEXT_CONVENTIONS",
@@ -172,7 +172,7 @@ export const why = {
172
172
  "no-data-fetch-in-effect": {
173
173
  code: "FE_EFFECT_FETCH",
174
174
  en: "`useEffect` in `<file>` starts a promise (`await`, `fetch`, `.then`, `void load()`, `mutate()`/`refresh()` called directly): no cache, no request merging, no loading/error state, no abort. Freshness of data comes from the SWR key, not from an effect.",
175
- fix: "Read on the client with SWR calling the app's client (`hooks/`) keyed by what changes; call `mutate()` from the event that changes the data (handler, socket message); on the route use the server reader `modules/api/<domain>/read-*.ts`.",
175
+ fix: "Read with an SWR hook of `hooks/swr` over an operation of `modules/api`, keyed by what changes; call `mutate()` from the event that changes the data (handler, socket message).",
176
176
  },
177
177
  "no-empty-catch": {
178
178
  code: "FE_SWALLOWED_ERROR",
@@ -216,14 +216,19 @@ export const why = {
216
216
  },
217
217
  "client-no-server-import": {
218
218
  code: "FE_CLIENT_SERVER_IMPORT",
219
- en: "A client component in `<file>` imports code that exists only on the server (`server-only`, `next/headers`, a Node module, a server reader).",
220
- fix: "Read the data in a server component or a server reader and pass it down as props; or use SWR calling the app's client.",
219
+ en: "A client component in `<file>` imports code that exists only on the server (`server-only`, `next/headers`, a Node module, a server-only module).",
220
+ fix: "Read the data through an SWR hook of `hooks/swr` over an operation of `modules/api`; keep request-scoped server APIs in server components.",
221
221
  },
222
222
  "server-module-marks-server-only": {
223
223
  code: "FE_SERVER_ONLY_MARK",
224
224
  en: "Module `<file>` imports a server-only API (`next/headers`, `next/server`, `next-intl/server`, a Node module or an already server-only module) but does not open with `import \"server-only\"`.",
225
225
  fix: "Make `import \"server-only\"` the file's first statement; route files (`page`, `layout`, `route`, `proxy`) are server components already and need none.",
226
226
  },
227
+ "no-server-action": {
228
+ code: "FE_SERVER_ACTION_FORBIDDEN",
229
+ en: "`<file>` carries a `\"use server\"` directive: a Server Action, an endpoint the front end serves. The front end serves no API.",
230
+ fix: "Write it as a GraphQL mutation of the back end and call it through its operation in `modules/api` and its SWR hook in `hooks/swr`.",
231
+ },
227
232
  "web-storage-only-in-modules": {
228
233
  code: "FE_STORAGE_OUTSIDE_MODULES",
229
234
  en: "`<file>` uses `localStorage`/`sessionStorage` outside `modules/`; storage does not exist on the server and throws when over quota.",
@@ -261,8 +266,8 @@ export const why = {
261
266
  },
262
267
  "one-outcome-union": {
263
268
  code: "FE_HTTP_STATUS_COLLAPSE",
264
- en: "`<file>` declares another result union (`ok` or `kind`) outside `outcome.ts`. A repo has only one `Outcome<T>`.",
265
- fix: "Use `Outcome<T>` of `modules/api/outcome.ts` (or the shared api package); add business detail through the second parameter instead of declaring a new union.",
269
+ en: "`<file>` declares another result union (`success`, `ok` or `kind`) outside `modules/api/graphql/types.ts`. A repo has only one result vocabulary, `GraphQLResponse<T>`.",
270
+ fix: "Use `GraphQLResponse<T>` of `modules/api/graphql/types.ts` (or the shared api package); put the business detail in its `data` instead of declaring a new union.",
266
271
  },
267
272
  "i18n-stack-in-one-module": {
268
273
  code: "FE_I18N_PLACEMENT",
@@ -274,6 +279,11 @@ export const why = {
274
279
  en: "Raw `<tag>` in `<file>`. Page structure and text are composed from grammar components, not hand-written HTML.",
275
280
  fix: "Replace it with the matching grammar component (`Heading`, `Text`, `SurfaceCard`, ...); if grammar lacks it, add it to grammar or the ui package instead of drawing it in place.",
276
281
  },
282
+ "fe-stack-import": {
283
+ code: "FE_STACK_FORBIDDEN",
284
+ en: "`<file>` imports `<what>`, a library the front-end stack forbids (a second library for a concern that has one), or a Supabase package outside `modules/auth`.",
285
+ fix: "Use the concern's canonical library from `knowledge/hfs/fe-stack.yaml` (the message names it); Supabase on the front end is the auth client in `modules/auth` only, data goes through `modules/api`.",
286
+ },
277
287
  "file-size-growth": {
278
288
  code: "HFS_SIZE_GROWTH",
279
289
  en: "`<file>` exceeds the line budget: a new file already too long, or an existing file longer than at the parent commit. Every file must be small enough for one person to read through and one test to cover.",
@@ -12,11 +12,12 @@
12
12
  */
13
13
 
14
14
  import { attribute, attributeValue, calleeName, leadingTextOf, stringOf } from "./lib/ast.mjs"
15
+ import { forbiddingConcern } from "./lib/fe-stack.mjs"
15
16
  import { hfsOf } from "./lib/hfs.mjs"
16
17
  import { baseName, classOf, fileOf, inSlot, roleOfFile, slotOfFile, stem } from "./lib/scope.mjs"
17
18
 
18
- /** Another i18n stack: a second place that decides which language a reader sees. */
19
- const OTHER_I18N = /^(?:react-i18next|i18next|next-i18next|react-intl|next-translate|@lingui\/.+|typesafe-i18n|rosetta)$/
19
+ /** Another i18n stack: a second place that decides which language a reader sees (one the fe-stack catalog forbids by name is fe-stack-import's finding). */
20
+ const OTHER_I18N = /^(?:react-i18next|i18next|next-i18next|react-intl|next-translate|@lingui\/.+|typesafe-i18n|rosetta)(?:\/.*)?$/
20
21
 
21
22
  // -- FE-NEXT-1 -------------------------------------------------------------------------------------
22
23
 
@@ -96,7 +97,7 @@ export const noSecondI18nStack = {
96
97
  return {
97
98
  ImportDeclaration(node) {
98
99
  const source = String(node.source.value)
99
- if (OTHER_I18N.test(source)) context.report({ node, messageId: "stack", data: { name: source } })
100
+ if (OTHER_I18N.test(source) && !forbiddingConcern(source)) context.report({ node, messageId: "stack", data: { name: source } })
100
101
  },
101
102
  }
102
103
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "8.2.13",
3
+ "version": "8.2.15",
4
4
  "description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,43 +14,38 @@ purpose: >-
14
14
  pins:
15
15
  # --- @starci packages. Every @starci package is a PUBLISHED npm package (install: registry): a product repository
16
16
  # installs the exact pinned version from the npm registry in the root and every workspace package.json, never `file:`.
17
- '@starci/heroicons':
18
- version: 0.3.3
19
- group: starci
20
- install: registry
21
- side: fe
22
- source: packages/heroicons/package.json
23
- why: 'StarCi custom Heroicons-compatible cuts; eslint-canon-fe (icon.mjs) admits @starci/heroicons/24/outline and /16/solid as icon sources, so an app that uses them gets this exact version.'
17
+ # @starci/heroicons has no pin since 1.0.0-alpha.13: the application icon set is @phosphor-icons/react (fe-stack.yaml
18
+ # icons) and eslint-canon-fe one-icon-set refuses every other glyph catalogue, so no product repository installs it.
24
19
  '@starci/grammar':
25
- version: 0.9.12
20
+ version: 0.9.14
26
21
  group: starci
27
22
  install: registry
28
23
  side: fe
29
24
  source: packages/grammar/package.json
30
25
  why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
31
26
  '@starci/eslint-canon-be':
32
- version: 3.2.13
27
+ version: 3.2.16
33
28
  group: starci
34
29
  install: registry
35
30
  side: be
36
31
  source: packages/eslint/be/package.json
37
32
  why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
38
33
  '@starci/eslint-canon-fe':
39
- version: 8.2.13
34
+ version: 8.2.15
40
35
  group: starci
41
36
  install: registry
42
37
  side: fe
43
38
  source: packages/eslint/fe/package.json
44
39
  why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
45
40
  '@starci/stylelint-canon':
46
- version: 2.0.7
41
+ version: 2.0.9
47
42
  group: starci
48
43
  install: registry
49
44
  side: fe
50
45
  source: packages/stylelint/package.json
51
46
  why: 'the front-end stylelint canon: no-class-selector refuses a compound class selector (`div.card`); the brand layer accepts `--font-sans` and `--font-mono`; status-contrast measures the HeroUI soft pairs per theme; brand-layer-shape reads the shared :root,.light,.dark block and the info soft pair; appTokens come from loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
52
47
  '@starci/tsconfig':
53
- version: 2.0.3
48
+ version: 2.0.4
54
49
  group: starci
55
50
  install: registry
56
51
  side: both
@@ -62,7 +57,7 @@ pins:
62
57
  side: both
63
58
  source: packages/prettier-config/package.json
64
59
  '@starci/jest-preset':
65
- version: 2.2.6
60
+ version: 2.2.7
66
61
  group: starci
67
62
  install: registry
68
63
  side: be
@@ -76,14 +71,14 @@ pins:
76
71
  source: packages/test-world/package.json
77
72
  why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
78
73
  '@starci/cli':
79
- version: 1.2.12
74
+ version: 1.2.14
80
75
  group: starci
81
76
  install: registry
82
77
  side: both
83
78
  source: packages/cli/package.json
84
79
  why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
85
80
  '@starci/hfs':
86
- version: 4.2.12
81
+ version: 4.2.14
87
82
  group: starci
88
83
  install: registry
89
84
  side: both
@@ -274,6 +269,11 @@ pins:
274
269
  side: be
275
270
  why: the e2e world of a back end calls the app over HTTP (/health/live in the skeleton); two product back ends resolve 7.3.0, one 7.2.2.
276
271
  # --- ui
272
+ '@xyflow/react':
273
+ version: 12.11.6
274
+ group: ui
275
+ side: fe
276
+ why: 'Grammar FlowGraph imports React Flow from @xyflow/react; every Grammar consumer declares this required peer at the version used by the Grammar package.'
277
277
  '@heroui/react':
278
278
  version: 3.2.6
279
279
  group: ui
@@ -294,6 +294,198 @@ pins:
294
294
  group: ui
295
295
  side: fe
296
296
  why: 'the PostCSS plugin tailwindcss 4 runs through; the reference app declares ^4 and resolves 4.3.3.'
297
+ # --- front-end stack (knowledge/hfs/fe-stack.yaml; owner ruling 2026-10-13: lite = full)
298
+ '@apollo/client':
299
+ version: 4.1.6
300
+ group: framework
301
+ side: fe
302
+ why: 'the GraphQL client of every front end (fe-stack graphql): auth and no-auth clients with the link chain in modules/api/graphql; version read from the starci-academy package-lock on 2026-10-13.'
303
+ '@graphql-codegen/cli':
304
+ version: 7.4.6
305
+ group: tooling
306
+ side: fe
307
+ why: 'generates the front-end wire types from be/contracts/<app>/schema.graphql into modules/api/graphql/__generated__ (fe-stack codegen); the latest registry version on 2026-10-13, academy has none.'
308
+ '@graphql-codegen/client-preset':
309
+ version: 6.2.3
310
+ group: tooling
311
+ side: fe
312
+ why: 'the codegen preset of the front-end wire types (fe-stack codegen); the latest registry version on 2026-10-13.'
313
+ 'axios':
314
+ version: 1.15.2
315
+ group: framework
316
+ side: fe
317
+ why: 'the REST client of the front end for uploads, presigned URLs and auth endpoints (fe-stack rest); version read from the starci-academy package-lock on 2026-10-13.'
318
+ 'swr':
319
+ version: 2.4.2
320
+ group: framework
321
+ side: fe
322
+ why: 'the data hooks and cache of every front end, hooks/swr (fe-stack data); version read from the starci-academy package-lock on 2026-10-13.'
323
+ 'react-hook-form':
324
+ version: 7.77.0
325
+ group: framework
326
+ side: fe
327
+ why: 'the form library, hooks/rhf (fe-stack forms); version read from the starci-academy package-lock on 2026-10-13.'
328
+ '@hookform/resolvers':
329
+ version: 5.4.0
330
+ group: framework
331
+ side: fe
332
+ why: 'the zod resolver of react-hook-form (fe-stack forms); version read from the starci-academy package-lock on 2026-10-13.'
333
+ 'zod':
334
+ version: 4.4.3
335
+ group: framework
336
+ side: fe
337
+ why: 'the schema library of front-end forms and boundary parsing (fe-stack forms); version read from the starci-academy package-lock on 2026-10-13.'
338
+ '@reduxjs/toolkit':
339
+ version: 2.11.2
340
+ group: framework
341
+ side: fe
342
+ why: 'app-wide shared state, redux/ (fe-stack state-global); version read from the starci-academy package-lock on 2026-10-13.'
343
+ 'react-redux':
344
+ version: 9.2.0
345
+ group: framework
346
+ side: fe
347
+ why: 'the React binding of the app-wide store (fe-stack state-global); version read from the starci-academy package-lock on 2026-10-13.'
348
+ 'zustand':
349
+ version: 5.0.12
350
+ group: framework
351
+ side: fe
352
+ why: 'feature-local client state, hooks/zustand (fe-stack state-local); version read from the starci-academy package-lock on 2026-10-13.'
353
+ 'socket.io-client':
354
+ version: 4.8.3
355
+ group: framework
356
+ side: fe
357
+ why: 'the realtime client, hooks/socketio (fe-stack realtime); version read from the starci-academy package-lock on 2026-10-13.'
358
+ 'eventemitter2':
359
+ version: 6.4.9
360
+ group: framework
361
+ side: fe
362
+ why: 'the fan-out emitter behind the socket side-effect component (fe-stack realtime); version read from the starci-academy package-lock on 2026-10-13.'
363
+ '@phosphor-icons/react':
364
+ version: 2.1.10
365
+ group: ui
366
+ side: fe
367
+ why: 'the one application icon set (fe-stack icons); version read from the starci-academy package-lock on 2026-10-13.'
368
+ 'next-themes':
369
+ version: 0.4.6
370
+ group: ui
371
+ side: fe
372
+ why: 'light and dark theme switching in components/providers (fe-stack theme); version read from the starci-academy package-lock on 2026-10-13.'
373
+ 'framer-motion':
374
+ version: 12.42.2
375
+ group: ui
376
+ side: fe
377
+ why: 'motion (fe-stack motion); version read from the starci-academy package-lock on 2026-10-13.'
378
+ 'dayjs':
379
+ version: 1.11.21
380
+ group: framework
381
+ side: fe
382
+ why: 'date math and parsing, modules/dayjs (fe-stack dates); display goes through the next-intl formatter; version read from the starci-academy package-lock on 2026-10-13.'
383
+ 'decimal.js':
384
+ version: 10.6.0
385
+ group: framework
386
+ side: fe
387
+ why: 'money and decimal arithmetic, modules/utils (fe-stack numbers); version read from the starci-academy package-lock on 2026-10-13.'
388
+ 'superjson':
389
+ version: 2.2.6
390
+ group: framework
391
+ side: fe
392
+ why: 'serialization of stored values, modules/superjson and modules/storage (fe-stack storage); version read from the starci-academy package-lock on 2026-10-13.'
393
+ 'lodash':
394
+ version: 4.17.23
395
+ group: framework
396
+ side: fe
397
+ why: 'per-method utilities, modules/utils (fe-stack utils); version read from the starci-academy package-lock on 2026-10-13.'
398
+ 'usehooks-ts':
399
+ version: 3.1.1
400
+ group: framework
401
+ side: fe
402
+ why: 'reusable React hooks, hooks/reuseables (fe-stack utils); version read from the starci-academy package-lock on 2026-10-13.'
403
+ 'recharts':
404
+ version: 3.8.1
405
+ group: ui
406
+ side: fe
407
+ why: 'charts (fe-stack charts); version read from the starci-academy package-lock on 2026-10-13.'
408
+ 'react-markdown':
409
+ version: 10.1.0
410
+ group: ui
411
+ side: fe
412
+ why: 'markdown rendering (fe-stack markdown); version read from the starci-academy package-lock on 2026-10-13.'
413
+ 'remark-gfm':
414
+ version: 4.0.1
415
+ group: ui
416
+ side: fe
417
+ why: 'GitHub-flavoured markdown for react-markdown (fe-stack markdown); version read from the starci-academy package-lock on 2026-10-13.'
418
+ 'remark-directive':
419
+ version: 4.0.0
420
+ group: ui
421
+ side: fe
422
+ why: 'markdown directives for react-markdown (fe-stack markdown); version read from the starci-academy package-lock on 2026-10-13.'
423
+ 'react-shiki':
424
+ version: 0.9.2
425
+ group: ui
426
+ side: fe
427
+ why: 'code highlighting (fe-stack code); version read from the starci-academy package-lock on 2026-10-13.'
428
+ '@monaco-editor/react':
429
+ version: 4.7.0
430
+ group: ui
431
+ side: fe
432
+ why: 'the code editor (fe-stack code); version read from the starci-academy package-lock on 2026-10-13.'
433
+ 'mermaid':
434
+ version: 11.16.0
435
+ group: ui
436
+ side: fe
437
+ why: 'diagrams (fe-stack diagrams); version read from the starci-academy package-lock on 2026-10-13.'
438
+ 'react-dropzone':
439
+ version: 15.0.0
440
+ group: ui
441
+ side: fe
442
+ why: 'file drop zones of uploads (fe-stack upload); version read from the starci-academy package-lock on 2026-10-13.'
443
+ 'react-pdf':
444
+ version: 10.4.1
445
+ group: ui
446
+ side: fe
447
+ why: 'PDF viewing (fe-stack pdf); version read from the starci-academy package-lock on 2026-10-13.'
448
+ '@fingerprintjs/fingerprintjs':
449
+ version: 5.2.0
450
+ group: framework
451
+ side: fe
452
+ why: 'the device id of the GraphQL fingerprint link (fe-stack device-id); version read from the starci-academy package-lock on 2026-10-13.'
453
+ '@sentry/nextjs':
454
+ version: 11.7.0
455
+ group: framework
456
+ side: fe
457
+ why: 'error reporting through sentry.*.config.ts and instrumentation.ts (fe-stack observability); the latest registry version on 2026-10-13, academy has none.'
458
+ # --- back-end transports the front end calls, and the realtime gateway
459
+ '@nestjs/graphql':
460
+ version: 13.4.5
461
+ group: framework
462
+ side: be
463
+ why: 'the GraphQL transport of every back end (be.transport.graphql) that the front end calls; the ecommerce-app resolution on 2026-10-13.'
464
+ '@nestjs/apollo':
465
+ version: 13.4.5
466
+ group: framework
467
+ side: be
468
+ why: 'the Apollo driver of @nestjs/graphql; the ecommerce-app resolution on 2026-10-13.'
469
+ '@apollo/server':
470
+ version: 5.5.1
471
+ group: framework
472
+ side: be
473
+ why: 'the Apollo server behind @nestjs/apollo; the ecommerce-app resolution on 2026-10-13.'
474
+ '@nestjs/websockets':
475
+ version: 11.2.5
476
+ group: framework
477
+ side: be
478
+ why: 'the socket.io gateway of a realtime back end (lite runs it in the api process, owner ruling 2026-10-13); the @nestjs/core pin line.'
479
+ '@nestjs/platform-socket.io':
480
+ version: 11.2.5
481
+ group: framework
482
+ side: be
483
+ why: 'the socket.io platform of @nestjs/websockets; the @nestjs/core pin line.'
484
+ '@as-integrations/express5':
485
+ version: 1.1.2
486
+ group: framework
487
+ side: be
488
+ why: 'the Express 5 integration @nestjs/apollo loads at boot (R111 nestjs-apollo-express5, knowledge/hfs/peer-integrations.yaml); every api app that serves GraphQL installs it; the latest registry version on 2026-10-13.'
297
489
  # --- Supabase lite
298
490
  '@supabase/ssr':
299
491
  version: 0.12.7
@@ -0,0 +1,218 @@
1
+ schema: starci/fe-stack@1
2
+ purpose: >-
3
+ The front-end stack of every app, lite and full alike (owner ruling 2026-10-13, R212 case-2): for each common front-end concern the
4
+ ONE canonical library, its HFS home (slot ids of knowledge/hfs/slots.yaml), the pattern id that teaches it
5
+ (knowledge/patterns/fe/*.yaml) and the alternatives a front end never depends on or imports. Versions live in
6
+ knowledge/hfs/canon-pins.yaml only. `tier: core` packages are installed by every scaffold; `opt` packages are added when a feature
7
+ needs the concern. Source example: starci-lab/starci-academy (origin/main, 2026-10-13); it illustrates, this file decides.
8
+ concerns:
9
+ - id: framework
10
+ tier: core
11
+ packages: [next, react, react-dom, typescript]
12
+ home: [fe.route, fe.source-root-pinned]
13
+ pattern: FE-ROUTE
14
+ forbids: []
15
+ why: App Router with thin routes; route handlers (except health/live and the auth callback slot) and Server Actions are refused by FE_ROUTE_HANDLER_FORBIDDEN and FE_SERVER_ACTION_FORBIDDEN.
16
+ - id: graphql
17
+ tier: core
18
+ packages: ['@apollo/client', graphql]
19
+ home: [fe.modules.api, fe.transport.client, fe.transport.outcome]
20
+ pattern: FE-API-GRAPHQL
21
+ forbids: [urql, graphql-request, '@urql/core', graphql-hooks, apollo-boost]
22
+ why: one file per operation under modules/api/graphql/{queries,mutations}, run by the auth or no-auth Apollo client with the link chain.
23
+ - id: codegen
24
+ tier: core
25
+ packages: ['@graphql-codegen/cli', '@graphql-codegen/client-preset']
26
+ home: [fe.modules.api]
27
+ pattern: FE-WIRE-GENERATED
28
+ forbids: []
29
+ why: the wire types are generated from be/contracts/<app>/schema.graphql into modules/api/graphql/__generated__, never typed by hand.
30
+ - id: rest
31
+ tier: core
32
+ packages: [axios]
33
+ home: [fe.modules.api]
34
+ pattern: FE-API-REST
35
+ forbids: [ky, superagent, node-fetch, cross-fetch, isomorphic-fetch]
36
+ why: modules/api/rest/<area>/*.ts for uploads, presigned URLs and auth endpoints only; GraphQL carries every other call.
37
+ - id: data
38
+ tier: core
39
+ packages: [swr]
40
+ home: [fe.hooks.swr]
41
+ pattern: FE-DATA-SWR
42
+ forbids: ['@tanstack/react-query', react-query]
43
+ why: hooks/swr/api/{graphql,rest}/{queries,mutations}/use*Swr.ts wrap one operation each; components call hooks only.
44
+ - id: toast
45
+ tier: core
46
+ packages: ['@heroui/react']
47
+ home: [fe.modules.toast]
48
+ pattern: FE-TOAST
49
+ forbids: [sonner, react-toastify, react-hot-toast, notistack]
50
+ why: mutation outcomes are reported by modules/toast (showGraphQLToast, use*WithToast) with next-intl labels.
51
+ - id: forms
52
+ tier: core
53
+ packages: [react-hook-form, '@hookform/resolvers', zod]
54
+ home: [fe.hooks.rhf]
55
+ pattern: FE-FORM
56
+ forbids: [formik, yup, final-form, react-final-form]
57
+ why: hooks/rhf/use<X>Form.ts builds a zod schema with next-intl messages and submits through an SWR mutation and the toast.
58
+ - id: state-global
59
+ tier: core
60
+ packages: ['@reduxjs/toolkit', react-redux]
61
+ home: [fe.redux]
62
+ pattern: FE-STATE-REDUX
63
+ forbids: [mobx, mobx-react, mobx-react-lite, recoil, jotai, valtio, redux-saga]
64
+ why: app-wide shared state (selected ids synced from the route, server-pushed entities) lives in redux/slices.
65
+ - id: state-local
66
+ tier: core
67
+ packages: [zustand]
68
+ home: [fe.hooks.zustand]
69
+ pattern: FE-STATE-ZUSTAND
70
+ forbids: []
71
+ why: feature-local UI, wizard and editor state lives in hooks/zustand/<store>.
72
+ - id: realtime
73
+ tier: opt
74
+ packages: [socket.io-client, eventemitter2]
75
+ home: [fe.hooks.socketio]
76
+ pattern: FE-REALTIME
77
+ forbids: [graphql-ws, subscriptions-transport-ws, '@supabase/realtime-js', pusher-js, ably]
78
+ why: one socket.io-client singleton per namespace and one side-effect component that dispatches to redux and SWR; lite runs the gateway in the be api process.
79
+ - id: auth
80
+ tier: core
81
+ packages: []
82
+ home: [fe.modules.auth]
83
+ pattern: FE-AUTH
84
+ forbids: []
85
+ why: modules/auth is the identity-provider client (lite - the @supabase/supabase-js auth client; full - the app's IdP client); it yields the session and the access token for the Apollo link and never reads data.
86
+ - id: i18n
87
+ tier: core
88
+ packages: [next-intl]
89
+ home: [fe.modules.i18n]
90
+ pattern: FE-I18N
91
+ forbids: [i18next, react-i18next, next-i18next, react-intl, '@lingui/react']
92
+ why: useTranslations for copy and the next-intl formatter for numbers, dates and currency.
93
+ - id: ui
94
+ tier: core
95
+ packages: ['@heroui/react', '@heroui/styles', tailwindcss, '@starci/grammar']
96
+ home: [fe.components, fe.feature]
97
+ pattern: FE-UI
98
+ forbids: ['@mantine/core', '@mui/material', '@chakra-ui/react', antd, styled-components, '@emotion/react', '@emotion/styled']
99
+ why: the grammar renderers over HeroUI and Tailwind; no second UI kit and no CSS-in-JS.
100
+ - id: icons
101
+ tier: core
102
+ packages: ['@phosphor-icons/react']
103
+ home: [fe.components]
104
+ pattern: FE-ICON
105
+ forbids: [react-icons, lucide-react, '@iconify/react', '@gravity-ui/icons', '@tabler/icons-react', '@fortawesome/react-fontawesome']
106
+ why: one application icon set.
107
+ - id: theme
108
+ tier: core
109
+ packages: [next-themes]
110
+ home: [fe.components.providers]
111
+ pattern: FE-THEME
112
+ forbids: []
113
+ why: the theme provider lives in components/providers.
114
+ - id: motion
115
+ tier: opt
116
+ packages: [framer-motion]
117
+ home: [fe.components]
118
+ pattern: FE-MOTION
119
+ forbids: [react-spring, '@react-spring/web', gsap]
120
+ why: one motion library.
121
+ - id: dates
122
+ tier: core
123
+ packages: [dayjs]
124
+ home: [fe.modules]
125
+ pattern: FE-DATE
126
+ forbids: [moment, moment-timezone, date-fns, luxon]
127
+ why: dayjs (modules/dayjs) for date math and parsing; display through the next-intl formatter.
128
+ - id: numbers
129
+ tier: opt
130
+ packages: [decimal.js]
131
+ home: [fe.modules]
132
+ pattern: FE-NUMBER
133
+ forbids: [big.js, bignumber.js, numeral]
134
+ why: decimal.js for money arithmetic in modules/utils; display through the next-intl formatter.
135
+ - id: storage
136
+ tier: core
137
+ packages: [superjson]
138
+ home: [fe.modules]
139
+ pattern: FE-STORAGE
140
+ forbids: [localforage, store2]
141
+ why: modules/storage (local and session storage) serializes through modules/superjson; components never touch storage directly.
142
+ - id: utils
143
+ tier: opt
144
+ packages: [lodash, usehooks-ts]
145
+ home: [fe.modules, fe.hooks.reuseables]
146
+ pattern: FE-UTILS
147
+ forbids: [underscore, ramda, lodash-es]
148
+ why: per-method lodash imports in modules/utils and reusable hooks in hooks/reuseables.
149
+ - id: charts
150
+ tier: opt
151
+ packages: [recharts]
152
+ home: [fe.components]
153
+ pattern: FE-CHART
154
+ forbids: [chart.js, react-chartjs-2, '@nivo/core', d3, echarts, echarts-for-react]
155
+ why: one chart library, styled by the dataviz grammar.
156
+ - id: flow
157
+ tier: opt
158
+ packages: ['@xyflow/react']
159
+ home: [fe.components]
160
+ pattern: FE-FLOW
161
+ forbids: [reactflow, cytoscape]
162
+ why: graph canvases go through the grammar FlowGraph over React Flow.
163
+ - id: markdown
164
+ tier: opt
165
+ packages: [react-markdown, remark-gfm, remark-directive]
166
+ home: [fe.components]
167
+ pattern: FE-MARKDOWN
168
+ forbids: [marked, markdown-it, showdown]
169
+ why: markdown renders through react-markdown, never dangerouslySetInnerHTML.
170
+ - id: code
171
+ tier: opt
172
+ packages: [react-shiki, '@monaco-editor/react']
173
+ home: [fe.components]
174
+ pattern: FE-CODE
175
+ forbids: [prismjs, react-syntax-highlighter, highlight.js, '@uiw/react-codemirror', codemirror]
176
+ why: react-shiki highlights and Monaco edits.
177
+ - id: diagrams
178
+ tier: opt
179
+ packages: [mermaid]
180
+ home: [fe.components]
181
+ pattern: FE-DIAGRAM
182
+ forbids: []
183
+ why: diagrams render through mermaid in a block.
184
+ - id: upload
185
+ tier: opt
186
+ packages: [react-dropzone]
187
+ home: [fe.components, fe.modules.api]
188
+ pattern: FE-UPLOAD
189
+ forbids: [react-dropzone-uploader, filepond, react-filepond, uppy]
190
+ why: react-dropzone in a block and a presigned PUT through modules/api/rest.
191
+ - id: pdf
192
+ tier: opt
193
+ packages: [react-pdf]
194
+ home: [fe.components]
195
+ pattern: FE-PDF
196
+ forbids: ['@react-pdf-viewer/core', pdfjs-dist]
197
+ why: PDF viewing through react-pdf in a block.
198
+ - id: device-id
199
+ tier: opt
200
+ packages: ['@fingerprintjs/fingerprintjs']
201
+ home: [fe.transport.client]
202
+ pattern: FE-API-GRAPHQL
203
+ forbids: []
204
+ why: the fingerprint link of the Apollo client attaches the device id.
205
+ - id: observability
206
+ tier: opt
207
+ packages: ['@sentry/nextjs']
208
+ home: [fe.source-root-pinned]
209
+ pattern: FE-OBSERVE
210
+ forbids: [logrocket, bugsnag-js, '@bugsnag/js', '@datadog/browser-rum']
211
+ why: errors are reported through @sentry/nextjs (sentry.*.config.ts, instrumentation.ts), never console-only.
212
+ - id: lint
213
+ tier: core
214
+ packages: [eslint, prettier, '@starci/eslint-canon-fe', '@starci/prettier-config']
215
+ home: [fe.tool-config]
216
+ pattern: FE-LINT
217
+ forbids: []
218
+ why: the canon lint and format configuration.