@starci/hfs 4.2.12 → 4.2.14

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 (203) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +7 -3
  3. package/emit/db-types.mjs +2 -1
  4. package/package.json +1 -1
  5. package/runtime/knowledge/hfs/canon-pins.yaml +207 -15
  6. package/runtime/knowledge/hfs/fe-stack.yaml +218 -0
  7. package/runtime/knowledge/hfs/rules.yaml +51 -48
  8. package/runtime/knowledge/hfs/slots.yaml +319 -179
  9. package/runtime/knowledge/patterns/be/event-bus.yaml +15 -0
  10. package/runtime/knowledge/patterns/be/jobs.yaml +10 -0
  11. package/runtime/knowledge/patterns/be/projections.yaml +10 -0
  12. package/runtime/knowledge/patterns/be/queues.yaml +15 -0
  13. package/runtime/knowledge/patterns/be/reactors.yaml +8 -0
  14. package/runtime/knowledge/patterns/be/realtime.yaml +17 -0
  15. package/runtime/knowledge/patterns/be/saga.yaml +13 -0
  16. package/runtime/knowledge/patterns/fe/folder.yaml +24 -18
  17. package/runtime/knowledge/sonar-gate.yaml +10 -2
  18. package/runtime/modules/kernel/failure-codes.yaml +40 -60
  19. package/runtime/modules/kernel/removed-vocabulary.yaml +48 -0
  20. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +0 -2
  21. package/runtime/scripts/hfs/architecture/config.mjs +3 -2
  22. package/runtime/scripts/hfs/architecture/dead-exports.mjs +6 -2
  23. package/runtime/scripts/hfs/architecture/error-masked.mjs +10 -1
  24. package/runtime/scripts/hfs/architecture/frontend.mjs +10 -3
  25. package/runtime/scripts/hfs/architecture/hfs.mjs +6 -3
  26. package/runtime/scripts/hfs/architecture/index.mjs +2 -0
  27. package/runtime/scripts/hfs/architecture/owners.mjs +19 -16
  28. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +4 -1
  29. package/runtime/scripts/hfs/architecture/server-action.mjs +75 -20
  30. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +2 -2
  31. package/runtime/scripts/hfs/architecture/supabase.mjs +36 -210
  32. package/runtime/scripts/hfs/architecture/tiers.mjs +1 -33
  33. package/runtime/scripts/hfs/architecture/transport-owner.mjs +110 -54
  34. package/runtime/scripts/hfs/architecture/type-context.mjs +4 -1
  35. package/runtime/scripts/hfs/check.mjs +10 -6
  36. package/runtime/scripts/hfs/edition-slots.mjs +13 -4
  37. package/runtime/scripts/hfs/manifest-shape.mjs +3 -3
  38. package/runtime/scripts/hfs/rule-catalog.mjs +21 -1
  39. package/runtime/scripts/hfs/rules/database-constants.mjs +4 -0
  40. package/runtime/scripts/hfs/rules/database-migrations.mjs +12 -7
  41. package/runtime/scripts/hfs/rules/database.mjs +4 -3
  42. package/runtime/scripts/hfs/rules/edition.mjs +49 -37
  43. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +4 -5
  44. package/runtime/scripts/hfs/rules/fe-stack.mjs +71 -0
  45. package/scaffold/add-app.mjs +24 -18
  46. package/scaffold/add-cli-lite.mjs +33 -3
  47. package/scaffold/add-inbox-lite.mjs +24 -8
  48. package/scaffold/add-table.mjs +222 -74
  49. package/scaffold/add.mjs +85 -35
  50. package/scaffold/app.mjs +146 -38
  51. package/scaffold/edition-gate.mjs +21 -2
  52. package/scaffold/lite-exports.mjs +30 -1
  53. package/scaffold/service.mjs +5 -8
  54. package/src/main.mjs +2 -2
  55. package/sync/index.mjs +19 -15
  56. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +14 -0
  57. package/templates/app/codegen-graphql.mjs.partial +34 -0
  58. package/templates/app/codegen-lite/scripts/codegen.mjs +18 -13
  59. package/templates/app/package-scripts/core.json +1 -0
  60. package/templates/app/package-scripts-lite/package.json +4 -0
  61. package/templates/app/skeleton/README.md +1 -1
  62. package/templates/app/skeleton/scripts/codegen.mjs +4 -1
  63. package/templates/app/skeleton-lite/README.md +7 -4
  64. package/templates/app/skeleton-lite/supabase/config.toml +1 -1
  65. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +13 -14
  66. package/templates/be/inbox/database-client.spec.ts.tpl +47 -0
  67. package/templates/be/inbox/service.spec.ts.tpl +46 -0
  68. package/templates/be/inbox/webhook-door.spec.ts.tpl +76 -0
  69. package/templates/be/patterns/api/graphql.module.ts.tpl +2 -2
  70. package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +6 -3
  71. package/templates/be/platform/errors-graphql/graphql-error.mapper.spec.ts +52 -0
  72. package/templates/be/platform/errors-graphql/graphql-error.mapper.ts +31 -0
  73. package/templates/be/platform/graphql/graphql-depth.policy.spec.ts +43 -0
  74. package/templates/be/platform/graphql/graphql-depth.policy.ts +21 -0
  75. package/templates/be/platform/graphql/graphql.contracts.ts +12 -0
  76. package/templates/be/platform/graphql/graphql.module-definition.ts +7 -0
  77. package/templates/be/platform/graphql/graphql.module.ts +49 -0
  78. package/templates/be/platform/graphql/graphql.options.ts +2 -0
  79. package/templates/be/platform/graphql/index.ts +1 -0
  80. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +114 -0
  81. package/templates/be/platform/graphql/localize-errors.mapper.ts +51 -0
  82. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +11 -0
  83. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +8 -3
  84. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +13 -1
  85. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +11 -2
  86. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +21 -0
  87. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +94 -0
  88. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +2 -4
  89. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +10 -0
  90. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +2 -2
  91. package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +2 -6
  92. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +15 -0
  93. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  94. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +1 -1
  95. package/templates/be/table/api/create.command.ts.tpl +10 -0
  96. package/templates/be/table/api/create.contracts.ts.tpl +8 -0
  97. package/templates/be/table/api/create.handler.ts.tpl +22 -0
  98. package/templates/be/table/api/create.mapper.ts.tpl +5 -0
  99. package/templates/be/table/api/create.resolver.ts.tpl +21 -0
  100. package/templates/be/table/api/create.type.ts.tpl +9 -0
  101. package/templates/be/table/api/feature.index.ts.tpl +1 -0
  102. package/templates/be/table/api/feature.module.ts.tpl +7 -0
  103. package/templates/be/table/api/get.contracts.ts.tpl +11 -0
  104. package/templates/be/table/api/get.handler.ts.tpl +22 -0
  105. package/templates/be/table/api/get.input.ts.tpl +12 -0
  106. package/templates/be/table/api/get.mapper.ts.tpl +9 -0
  107. package/templates/be/table/api/get.query.ts.tpl +10 -0
  108. package/templates/be/table/api/get.resolver.ts.tpl +27 -0
  109. package/templates/be/table/api/get.type.ts.tpl +9 -0
  110. package/templates/be/table/api/graphql.module.ts.tpl +8 -0
  111. package/templates/be/table/error.api.ts.tpl +19 -0
  112. package/templates/be/table/migration.sql.tpl +13 -21
  113. package/templates/be/table/service.api.ts.tpl +30 -0
  114. package/templates/be/table/sql.api.ts.tpl +7 -0
  115. package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignInForm/index.tsx +16 -18
  116. package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignOutButton/component.tsx +7 -3
  117. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignOutButton/index.tsx +23 -0
  118. package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +49 -0
  119. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +4 -0
  120. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -0
  121. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +29 -0
  122. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +27 -0
  123. package/templates/fe/skeleton/apps/__app__/src/hooks/effects/index.ts +1 -0
  124. package/templates/fe/skeleton/apps/__app__/src/hooks/effects/useAuthSessionEffect.ts +14 -0
  125. package/templates/fe/skeleton/apps/__app__/src/hooks/swr/SwrSideEffects.tsx +8 -0
  126. package/templates/fe/skeleton/apps/__app__/src/hooks/swr/index.ts +1 -0
  127. package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/index.ts +1 -0
  128. package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/types.ts +26 -0
  129. package/templates/fe/skeleton/apps/__app__/src/modules/api/index.ts +1 -0
  130. package/templates/fe/skeleton/apps/__app__/src/modules/auth/client.ts +21 -0
  131. package/templates/fe/skeleton/apps/__app__/src/modules/auth/contracts.ts +27 -0
  132. package/templates/fe/skeleton/apps/__app__/src/modules/auth/index.ts +2 -0
  133. package/templates/fe/skeleton/apps/__app__/src/modules/toast/api.ts +52 -0
  134. package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +33 -0
  135. package/templates/fe/skeleton/apps/__app__/src/modules/toast/index.ts +1 -0
  136. package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +27 -0
  137. package/templates/fe/skeleton/apps/__app__/src/modules/types/ambient/styles.d.ts +2 -0
  138. package/templates/fe/skeleton/apps/__app__/src/redux/ReduxProvider.tsx +11 -0
  139. package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +8 -0
  140. package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +3 -0
  141. package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +26 -0
  142. package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +14 -0
  143. package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +5 -2
  144. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -2
  145. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +1 -1
  146. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +23 -0
  147. package/templates/fe/skeleton/apps/landing/src/modules/types/ambient/styles.d.ts +2 -0
  148. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +51 -0
  149. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +10 -13
  150. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +8 -5
  151. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -17
  152. package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +3 -10
  153. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +0 -6
  154. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +11 -8
  155. package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +0 -3
  156. package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +9 -25
  157. package/templates/fe/table/graphql/mutation.ts.tpl +25 -0
  158. package/templates/fe/table/graphql/mutation.types.ts.tpl +7 -0
  159. package/templates/fe/table/graphql/query.ts.tpl +25 -0
  160. package/templates/fe/table/graphql/query.types.ts.tpl +7 -0
  161. package/templates/fe/table/graphql/useMutateSwr.ts.tpl +21 -0
  162. package/templates/fe/table/graphql/useQuerySwr.ts.tpl +17 -0
  163. package/templates/fe/transport/graphql/clients/clients/auth.ts +39 -0
  164. package/templates/fe/transport/graphql/clients/clients/index.ts +2 -0
  165. package/templates/fe/transport/graphql/clients/clients/no-auth.ts +33 -0
  166. package/templates/fe/transport/graphql/clients/index.ts +2 -0
  167. package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +9 -0
  168. package/templates/fe/transport/graphql/clients/links/error.ts +30 -0
  169. package/templates/fe/transport/graphql/clients/links/http.ts +16 -0
  170. package/templates/fe/transport/graphql/clients/links/index.ts +8 -0
  171. package/templates/fe/transport/graphql/clients/links/locale.ts +13 -0
  172. package/templates/fe/transport/graphql/clients/links/options.ts +17 -0
  173. package/templates/fe/transport/graphql/clients/links/refresh-token.ts +36 -0
  174. package/templates/fe/transport/graphql/clients/links/retry.ts +16 -0
  175. package/templates/fe/transport/graphql/clients/links/timeout.ts +28 -0
  176. package/templates/fe/transport/graphql/clients/response.ts +43 -0
  177. package/upgrade/check.mjs +1 -1
  178. package/upgrade/index.mjs +3 -2
  179. package/runtime/scripts/hfs/architecture/supabase-results.mjs +0 -182
  180. package/templates/app/quality-config-lite/sonar-project.properties +0 -5
  181. package/templates/be/patterns/api/feature.index.http.ts.tpl +0 -1
  182. package/templates/be/patterns/api/http.controller.ts.tpl +0 -27
  183. package/templates/be/patterns/api/http.mapper.ts.tpl +0 -9
  184. package/templates/be/patterns/api/http.module.ts.tpl +0 -7
  185. package/templates/be/patterns/api/http.request.ts.tpl +0 -10
  186. package/templates/be/patterns/api/http.response.ts.tpl +0 -5
  187. package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +0 -24
  188. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +0 -38
  189. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +0 -25
  190. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +0 -25
  191. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +0 -17
  192. package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +0 -14
  193. package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +0 -3
  194. package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +0 -59
  195. package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +0 -29
  196. package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +0 -70
  197. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +0 -58
  198. package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +0 -26
  199. package/templates/fe/table/read.ts.tpl +0 -16
  200. package/templates/fe/table/write.ts.tpl +0 -16
  201. /package/templates/be/patterns/api/{application.command.http.ts.tpl → application.command.principal.ts.tpl} +0 -0
  202. /package/templates/be/patterns/api/{application.handler.http.ts.tpl → application.handler.principal.ts.tpl} +0 -0
  203. /package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignInForm/component.tsx +0 -0
@@ -283,3 +283,18 @@ files:
283
283
  role: "The consumed event's e2e flow; titles event-bus/<scenario>: ... prove the pattern."
284
284
  spec: event-bus/atomic-with-transaction, event-bus/relay-only-after-commit, event-bus/redelivery-then-dead-letter, event-bus/duplicate-delivery-is-a-noop
285
285
  rules: [BE_PATTERN_SPEC_MISSING]
286
+ # Edition lite (owner ruling 2026-10-12, R212 case-1): the same ports and rules, a Supabase adapter behind them.
287
+ liteAdapter:
288
+ summary: >-
289
+ Under lite the event bus keeps typed events, the vendored contract, publish(event, tx) in the domain transaction, the outbox,
290
+ the inbox and thin consumers; Kafka is replaced by pgmq-backed topics, or by in-process delivery after commit for a back end
291
+ with a single api app.
292
+ ports:
293
+ - {port: EventPublisher (platform/event-bus), adapter: the same outbox row in the domain transaction}
294
+ - {port: relay, adapter: "fans each committed event out with pgmq.send to one queue per consumer group of its topic, marking the row in the same transaction"}
295
+ - {port: EventConsumer (platform/event-bus), adapter: "pgmq.read with a visibility timeout, the inbox claim on the event id, pgmq.delete on success, pgmq.archive as the dead letter"}
296
+ forbids: [kafkajs]
297
+ proof: >-
298
+ Unit specs as full; the event-bus/* scenarios are optional integration specs on `starci supabase start`, and there is no e2e
299
+ async spec (R155 and R167 are full's).
300
+ see: [knowledge/patterns/be/supabase-queues.yaml]
@@ -259,3 +259,13 @@ files:
259
259
  role: "Titles fenced-job/<scenario>: ... prove the pattern."
260
260
  spec: fenced-job/claim-bumps-token, fenced-job/zombie-fenced-out, fenced-job/redispatch-isolated
261
261
  rules: [BE_PATTERN_SPEC_MISSING]
262
+ # Edition lite (owner ruling 2026-10-12, R212 case-1): the same ports and rules, a Supabase adapter behind them.
263
+ liteAdapter:
264
+ summary: >-
265
+ Under lite a fenced job is unchanged: the job row with its fencing token, the atomic claim, guarded writes and run keys. The
266
+ row's table is a supabase migration, and the processor is fed by the lite queue (pgmq).
267
+ ports:
268
+ - {port: job claim (platform/jobs), adapter: "the same claim SQL through the shared EntityManager; the table comes from supabase/migrations"}
269
+ - {port: processor delivery, adapter: "the lite queue worker (pgmq.read, visibility timeout, archive on dead letter)"}
270
+ proof: Unit specs as full (job-claim.service.spec.ts); the fenced-job/* scenarios are optional integration specs on `starci supabase start`.
271
+ see: [knowledge/patterns/be/supabase-queues.yaml]
@@ -176,3 +176,13 @@ files:
176
176
  role: "Titles projection/<scenario>: ... prove the pattern."
177
177
  spec: projection/recompute-is-idempotent, projection/replay-rebuilds
178
178
  rules: [BE_PATTERN_SPEC_MISSING]
179
+ # Edition lite (owner ruling 2026-10-12, R212 case-1): the same ports and rules, a Supabase adapter behind them.
180
+ liteAdapter:
181
+ summary: >-
182
+ Under lite a projection is unchanged in behaviour (only it writes its table, recompute is idempotent and replayable, readers
183
+ use get*); its table is a supabase migration with a rows type, so the slot carries no projection entity (be.projections lite
184
+ overlay), and it is kept current by a reactor or a job on the lite transport.
185
+ ports:
186
+ - {port: read model table, adapter: "a supabase migration plus <name>.sql.ts and <name>.rows.ts under persistence/"}
187
+ proof: Unit specs as full (the projection is a logic role at 100 per file); the projection/* scenarios are optional integration specs.
188
+ see: [knowledge/patterns/be/supabase-queues.yaml]
@@ -234,3 +234,18 @@ files:
234
234
  role: "Titles queue/<scenario>: ... prove the pattern."
235
235
  spec: queue/enqueue-atomic-with-transaction, queue/relay-job-id-is-outbox-id, queue/scheduler-fires-once
236
236
  rules: [BE_PATTERN_SPEC_MISSING]
237
+ # Edition lite (owner ruling 2026-10-12, R212 case-1): the same ports and rules, a Supabase adapter behind them.
238
+ liteAdapter:
239
+ summary: >-
240
+ Under lite the queue pattern is unchanged above the port (producers, the outbox written in the caller's transaction, processors,
241
+ schedulers declared with their queue) and its transport is Supabase Queues (pgmq) instead of BullMQ and Redis.
242
+ ports:
243
+ - {port: QueueOutbox (platform/queue), adapter: the same outbox row written through the caller's tx; nothing changes}
244
+ - {port: queue transport (queue-transport.port.ts), adapter: "pgmq-queue-transport.client.ts: pgmq.send through the shared EntityManager, the outbox id inside the message"}
245
+ - {port: QueueWorkerRegistry (platform/queue), adapter: "a poll loop per queue: pgmq.read with the policy's visibility timeout, pgmq.delete on success, pgmq.set_vt for backoff, pgmq.archive when read_ct exceeds the attempts"}
246
+ - {port: schedulers, adapter: "a pg_cron job declared in a supabase migration that calls pgmq.send; no BullMQ job scheduler"}
247
+ forbids: [bullmq, "@nestjs/bullmq", redis, ioredis, "@nestjs/schedule"]
248
+ proof: >-
249
+ Unit specs as full (queue-outbox, relay and worker services, the pgmq client against mockEntityManager); the queue/* scenarios are
250
+ optional integration specs on `starci supabase start` (R155 is full's).
251
+ see: [knowledge/patterns/be/supabase-queues.yaml]
@@ -138,3 +138,11 @@ files:
138
138
  - path: src/tests/e2e/<area>/<event>.e2e-spec.ts
139
139
  slot: be.tests.e2e
140
140
  role: The reactor's event flow through the bus, redelivery included.
141
+ # Edition lite (owner ruling 2026-10-12, R212 case-1): the same ports and rules, a Supabase adapter behind them.
142
+ liteAdapter:
143
+ summary: >-
144
+ Under lite a reactor is unchanged: a thin consumer of another context's events that dispatches one command. Its deliveries
145
+ come from the lite event bus (its own pgmq queue per consumer group).
146
+ ports:
147
+ - {port: EventConsumer delivery, adapter: "the reactor's pgmq queue (see event-bus.yaml liteAdapter)"}
148
+ see: [knowledge/patterns/be/supabase-queues.yaml]
@@ -207,3 +207,20 @@ files:
207
207
  spec: realtime/subscriber-receives-push, realtime/foreign-topic-is-isolated, realtime/disconnect-releases-subscription
208
208
  rules: [BE_PATTERN_SPEC_MISSING]
209
209
  optional: true
210
+ # Edition lite (owner rulings 2026-10-12 and 2026-10-13, R212 cases 1-2): the same ports, door and rules; one instance behind them.
211
+ liteAdapter:
212
+ summary: >-
213
+ Under lite the realtime pattern is the full one: the socket.io gateway of be.feature.realtime.websocket
214
+ (transport/websocket/<channel>.gateway.ts) runs inside the be api process, one instance with the default in-memory adapter
215
+ and no Redis adapter. A reactor publishes to the RealtimeHub of platform/realtime, which emits to the principal-scoped room
216
+ of the namespace; the front end listens through hooks/socketio (knowledge/patterns/fe/realtime.yaml). graphql-ws
217
+ subscriptions are not used in lite.
218
+ ports:
219
+ - {port: RealtimeHub.publish (platform/realtime), adapter: "the socket.io server of the api process emits to the namespace room of the topic (no Redis adapter: one instance)"}
220
+ - {port: RealtimeHub.subscribe, adapter: "the gateway's @SubscribeMessage handler joins the room built from the principal, as full"}
221
+ - {port: topic scope, adapter: "the room name built from @CurrentPrincipal (BE_REALTIME_TOPIC_SCOPE), as full"}
222
+ forbids: [redis, ioredis, "@socket.io/redis-adapter", graphql-ws, graphql-subscriptions]
223
+ proof: >-
224
+ The gateway's own unit spec (<channel>.gateway.spec.ts) and the hub adapter's spec, as full; the realtime/* scenarios are
225
+ optional integration specs on `starci supabase start` (lite has no e2e).
226
+ see: [knowledge/patterns/be/supabase-queues.yaml, knowledge/patterns/fe/realtime.yaml]
@@ -246,3 +246,16 @@ rules:
246
246
  manual:
247
247
  - Check the injected fault hits the compensation, not only the step before it.
248
248
  relatedExamples: [saga]
249
+ # Edition lite (owner ruling 2026-10-12, R212 case-1): the same ports and rules, a Supabase adapter behind them.
250
+ liteAdapter:
251
+ summary: >-
252
+ Under lite a saga is unchanged: the orchestrator service, the versioned state behind the fenced store, steps and their
253
+ compensations, and consumers advancing it through the inbox. Its events travel on the lite event bus (pgmq queues per
254
+ consumer group).
255
+ ports:
256
+ - {port: saga events, adapter: "the lite event-bus adapter (pgmq) - see event-bus.yaml liteAdapter"}
257
+ - {port: saga state store, adapter: "the same table and version fence, created by a supabase migration (no TypeORM entity under lite)"}
258
+ proof: >-
259
+ The orchestrator's unit spec (<saga>.saga.service.spec.ts) as full; the e2e compensation specs of R173 are full's, so a lite
260
+ saga proves each compensation in the orchestrator's unit spec and, optionally, an integration spec on `starci supabase start`.
261
+ see: [knowledge/patterns/be/supabase-queues.yaml]
@@ -29,8 +29,11 @@ rules:
29
29
  `hfs.json` `sides.fe.apps` with kind `next` (slot `fe.app.next`; the scaffold writes `landing` and `app`) — and all
30
30
  product source lives under fe/apps/<app>/src. An fe/src/ does not exist. Each Next source root
31
31
  (fe/apps/<app>/src) uses app/ for framework adapters; features/{pages,layouts,overlays} for route-facing product
32
- composition; components/{blocks,composites,branches,leaves} for product visuals; hooks/<domain> for React hooks;
33
- and modules/<capability> for cohesive reusable technical and domain capabilities. Every app carries the required
32
+ composition; components/{blocks,composites,branches,leaves} for product visuals and components/providers for the
33
+ client providers the root layout mounts; hooks/ for React hooks (swr, rhf, zustand, socketio, effects, reuseables and
34
+ <domain>); redux/ for the app-wide store; and modules/<capability> for cohesive reusable technical and domain
35
+ capabilities (api, auth, toast, i18n, config, routes, brand, types, storage, superjson, dayjs, utils). Each concern's
36
+ library and home is knowledge/hfs/fe-stack.yaml. Every app carries the required
34
37
  module instances of slot `fe.modules` and one brand.css under modules/brand when it sets brand tokens
35
38
  (`fe.modules.brand`). No other empty role
36
39
  folder is required. Grammar remains a separate product-agnostic package. The only files that may sit directly in the
@@ -78,9 +81,10 @@ rules:
78
81
  when: "A routed authentication scenario"
79
82
  write: "fe/apps/<app>/src/features/pages/AuthenticationPage/index.tsx"
80
83
  - id: case-2
81
- when: "A reusable API client, reader or session capability"
84
+ when: "A back-end call or the session"
82
85
  write: >
83
- fe/apps/<app>/src/modules/api/ or fe/apps/<app>/src/modules/session/ with one explicit public index.ts entry
86
+ slot fe.modules.api (one file per query or mutation) or slot fe.modules.auth, each with one explicit public index.ts
87
+ entry
84
88
  - id: case-3
85
89
  when: "A reusable product visual without scenario ownership"
86
90
  write: "an owner folder of slot `fe.components` under the layer it belongs to — for example fe/apps/<app>/src/components/leaves/<Name>/index.tsx"
@@ -257,7 +261,7 @@ rules:
257
261
  when: "Global app files"
258
262
  write: >
259
263
  app/globals.css, app/global-error.tsx, app/[locale]/error.tsx, app/[locale]/not-found.tsx, app/sitemap.ts,
260
- app/robots.ts, app/health/{live,ready}/route.ts
264
+ app/robots.ts, app/health/live/route.ts (the one route handler, FE-ROUTE)
261
265
  verification:
262
266
  automated:
263
267
  - FE_ERROR_BOUNDARY_MISSING
@@ -287,33 +291,35 @@ rules:
287
291
  relatedExamples: []
288
292
 
289
293
  - id: FE-FOLDER-4
290
- title: Hooks are hooks; readers and transport live in modules
294
+ title: Hooks are hooks; operations and transport live in modules
291
295
  kind: mandatory
292
296
  hfsRules: [R56, R50, R49, R21, R26]
293
297
  requirement: |
294
- hooks/<domain>/ holds React hooks (use*.ts, one hook per file) and exactly one <domain>.shared.ts for shared
295
- non-hook helpers of that domain (key builders, token readers). A file in hooks/ that is not a hook and is not the shared
296
- file is a finding. Server-side readers (fetch and map, no React) live in modules/api/<domain>/read-*.ts and are
297
- wrapped in React cache() when several blocks call them in one request. Generic client plumbing, the Outcome type,
298
- the generated wire types (from be/contracts/, through the root codegen) live in modules/api; environment reading lives in modules/config; every
299
- href builder lives in modules/routes; locale routing lives in modules/i18n; colour values live in modules/brand.
298
+ hooks/ holds React hooks (use*.ts, one hook per file): hooks/swr (one SWR wrapper per modules/api operation, plus
299
+ SwrSideEffects.tsx), hooks/rhf (forms), hooks/zustand/<store> (a store and its hooks), hooks/socketio (sockets.ts,
300
+ SocketIoSideEffects.tsx, enums/, types/), hooks/effects (app-wide side effects), hooks/reuseables (generic hooks) and
301
+ hooks/<domain>/ with exactly one <domain>.shared.ts for shared non-hook helpers of that domain. A file in hooks/ that is
302
+ none of these is a finding. The Apollo clients and their links, one file per query and mutation, the GraphQLResponse
303
+ envelope, the generated wire types (from be/contracts/, through the root codegen) and the axios calls live in
304
+ modules/api; the session lives in modules/auth; mutation feedback in modules/toast; environment reading in
305
+ modules/config; every href builder in modules/routes; locale routing in modules/i18n; colour values in modules/brand.
300
306
  A helper name defined twice across hook files is a finding.
301
307
  rationale: |
302
308
  A rule that says what a hooks folder is also says where everything else goes, so nobody copies a helper sixteen
303
- times or fills hooks/ with server fetch functions.
309
+ times or fills hooks/ with transport calls.
304
310
  cases:
305
311
  - id: case-1
306
- when: "Authentication request lifecycle"
307
- write: "fe/apps/<app>/src/hooks/authentication/useAuthentication.ts"
312
+ when: "A query the sales page reads"
313
+ write: "fe/apps/<app>/src/hooks/swr/api/graphql/queries/useQuerySalesSummarySwr.ts over modules/api/graphql/queries/query-sales-summary.ts"
308
314
  - id: case-2
309
315
  when: "A helper shared by the hooks of one domain"
310
316
  write: "fe/apps/<app>/src/hooks/sales/sales.shared.ts, for example useSalesAccessToken's token reader"
311
317
  - id: case-3
312
- when: "A server read used by several blocks"
313
- write: "fe/apps/<app>/src/modules/api/sales/read-sales-summary.ts, exported through modules/api/index.ts"
318
+ when: "A form"
319
+ write: "fe/apps/<app>/src/hooks/rhf/useSendHandoffForm.ts"
314
320
  - id: case-4
315
321
  when: "The API client"
316
- write: "fe/apps/<app>/src/modules/api/client.ts and fe/apps/<app>/src/modules/api/outcome.ts"
322
+ write: "fe/apps/<app>/src/modules/api/graphql/clients/ (auth and no-auth Apollo clients, links/) and fe/apps/<app>/src/modules/api/graphql/types.ts"
317
323
  - id: case-5
318
324
  when: "Intrinsic custom helper"
319
325
  write: "fe/apps/<app>/src/hooks/ui/useAutoScroll.ts; direct useRef and useState calls may remain in their visual owner"
@@ -81,13 +81,17 @@ overall:
81
81
  cognitiveComplexity:
82
82
  rule: typescript:S3776
83
83
  via: overall.issues
84
- # Lite is the same quality policy without a test or coverage world. Readers select this block from hfs.json edition;
85
- # keeping both gates in this document makes every threshold data, never a branch-local restatement.
84
+ # Lite is the same quality policy, coverage included: a lite back end keeps full's unit run and its 100 per file on the measured
85
+ # logic files (owner ruling 2026-10-12, R212 case-1). Readers select this block from hfs.json edition; the gate keeps its own
86
+ # name so a lite project's server gate stays a separate object, and every threshold below equals full's.
86
87
  lite:
87
88
  gate:
88
89
  name: starci-quality-lite
89
90
  newCodePeriod: {type: NUMBER_OF_DAYS, value: 30}
90
91
  newCode:
92
+ coverage:
93
+ metric: new_coverage
94
+ minPercent: 100
91
95
  ignoreBelowChangedLines: 20
92
96
  duplication:
93
97
  metric: new_duplicated_lines_density
@@ -109,6 +113,10 @@ lite:
109
113
  hotspots:
110
114
  metric: security_hotspots_reviewed
111
115
  minReviewedPercent: 100
116
+ coverage:
117
+ metric: coverage
118
+ minPercent: 100
119
+ perFile: true
112
120
  duplication:
113
121
  metric: duplicated_lines_density
114
122
  maxPercent: 3
@@ -862,16 +862,6 @@ FE_APP_ISOLATION:
862
862
  owner: op-retry
863
863
  kind: check-finding
864
864
 
865
- FE_AUTH_SESSION_TRUST:
866
- title: "Server code trusts `getSession`, or a Server Action does not establish and refuse an anonymous principal"
867
- title_vi: "Mã server tin getSession hoặc Server Action không xác thực principal"
868
- meaning_vi: "`<path>` gọi `auth.getSession()` trên server, write action không bắt đầu bằng `await getPrincipal()`, không trả Outcome `refused` cho anonymous (trừ `fe.modules.db/auth/write-sign-in.ts` đang xác thực chính caller anonymous), hoặc mutation nhạy cảm không gọi `auth.getUser()`."
869
- causes_vi:
870
- - "Vi phạm L14: server dùng claims đã kiểm tra; write action xác thực principal trước mọi việc và mutation nhạy cảm kiểm tra user từ Auth server."
871
- nextStep_vi: "Dùng `getPrincipal()`/`getClaims()`, từ chối anonymous bằng Outcome có kiểu, và gọi `auth.getUser()` cho tiền, bảo mật tài khoản hay đổi role."
872
- owner: op-retry
873
- kind: check-finding
874
-
875
865
  FE_BLOCK_PRODUCT_HOOK_DEFINITION:
876
866
  title: "Block defines a product hook"
877
867
  title_vi: "Khối giao diện tự định nghĩa hook sản phẩm"
@@ -889,8 +879,8 @@ FE_CLIENT_REACHES_SERVER:
889
879
  meaning_vi: "Một mô-đun có chỉ thị `\"use client\"` với tới, qua chuỗi import chạy thật (nhiều bước, không tính import chỉ-kiểu), một mô-đun import `server-only`, `next/headers`, `next/server`, `next-intl/server`, `node:*` hoặc `fs`. Lời nhắn nêu client đầu vào và cả chuỗi import."
890
880
  causes_vi:
891
881
  - "Hook hoặc module dùng chung được client component import gián tiếp lại đọc request ở máy chủ"
892
- - "Reader máy chủ bị import từ đường đi của một client component thay vì chỉ từ server component"
893
- nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để đọc dữ liệu ở server component và truyền xuống bằng props, hoặc đưa import ra khỏi đồ thị client theo lời nhắn; không thêm ngoại lệ."
882
+ - "Module chỉ chạy ở máy chủ bị import từ đường đi của một client component"
883
+ nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để đọc dữ liệu qua hook SWR gọi `modules/api`, hoặc đưa import ra khỏi đồ thị client theo lời nhắn; không thêm ngoại lệ."
894
884
  owner: op-retry
895
885
  kind: check-finding
896
886
 
@@ -949,36 +939,6 @@ FE_CUSTOM_HOOK_LOCATION:
949
939
  owner: other-op:interface.implement
950
940
  kind: check-finding
951
941
 
952
- FE_DB_ERROR_HANDLED:
953
- title: "An awaited Supabase result drops `error` or collapses failure to empty data"
954
- title_vi: "Kết quả Supabase bỏ qua error hoặc gộp lỗi thành dữ liệu rỗng"
955
- meaning_vi: "Kết quả await ở `<path>` không đọc `error`, không được truyền nguyên vẹn vào `toOutcome`, hoặc dùng `data ?? []` nên `refused`/`unavailable` biến thành dữ liệu rỗng."
956
- causes_vi:
957
- - "Vi phạm L12: mọi `{data,error}` đi qua một Outcome có kiểu; không destructure chỉ data và không nuốt lỗi."
958
- nextStep_vi: "Đọc và ánh xạ `error`, hoặc truyền toàn bộ result vào `toOutcome(result)` rồi xử lý đủ các nhánh Outcome."
959
- owner: op-retry
960
- kind: check-finding
961
-
962
- FE_DB_RESULT_TYPED:
963
- title: "A Supabase result is cast, explicitly generic, or an unbounded list read"
964
- title_vi: "Kết quả Supabase bị ép kiểu hoặc truy vấn danh sách không giới hạn"
965
- meaning_vi: "Truy vấn ở `<path>` dùng generic trên `single/maybeSingle/returns`, ép kiểu/non-null assertion, hoặc đọc danh sách không có `limit` hay biên keyset."
966
- causes_vi:
967
- - "Vi phạm L11: kiểu hàng phải suy ra từ `Database`; mọi đọc nhiều dòng phải có giới hạn tĩnh."
968
- nextStep_vi: "Bỏ generic/cast/assertion, sửa type nguồn trong `database.types.ts`, và thêm `.limit()` hoặc biên keyset."
969
- owner: op-retry
970
- kind: check-finding
971
-
972
- FE_DB_WRITE_SHAPE:
973
- title: "A Supabase Server Action uses input before schema parsing, or a write module exports no action"
974
- title_vi: "Server Action dùng input trước khi schema parse"
975
- meaning_vi: "Write module ở `<path>` không export Server Action, hoặc tham số được dùng trước lần gọi `schema.parse`/`schema.safeParse` đầu tiên."
976
- causes_vi:
977
- - "Vi phạm phần write-shape của L14: input không được tin trước validation."
978
- nextStep_vi: "Export action async trong `write-*.ts` và để lần dùng input đầu tiên là `schema.parse(input)` hoặc `schema.safeParse(input)`."
979
- owner: op-retry
980
- kind: check-finding
981
-
982
942
  FE_ERROR_BOUNDARY_MISSING:
983
943
  title: "Next app lacks a required error or loading boundary"
984
944
  title_vi: "Ứng dụng Next thiếu trang lỗi hoặc trang đang tải bắt buộc"
@@ -1004,9 +964,9 @@ FE_GRAPHQL_CONTRACT:
1004
964
  FE_HOOKS_ARE_HOOKS:
1005
965
  title: "`hooks/` hold hooks and one shared file per domain"
1006
966
  title_vi: "hooks/ chứa thứ không phải hook"
1007
- meaning_vi: "`<file>` trong `hooks/` không phải React hook, hoặc một domain có nhiều hơn một tệp `.shared.ts` (hay tệp shared không mang tên domain), hoặc một helper được khai báo ở nhiều tệp của domain. Reader server về `modules/api`, helper về `<domain>.shared.ts`."
967
+ meaning_vi: "`<file>` trong `hooks/` không phải React hook, hoặc một domain có nhiều hơn một tệp `.shared.ts` (hay tệp shared không mang tên domain), hoặc một helper được khai báo ở nhiều tệp của domain. Hàm gọi mạng về `modules/api`, helper về `<domain>.shared.ts`."
1008
968
  causes_vi:
1009
- - "Vi phạm luật R56: `hooks/<domain>/` chỉ chứa `use*.ts` và một `<domain>.shared.ts` cho helper dùng chung; reader server nằm ở `modules/api/<domain>/read-*.ts` (bọc `cache()`)."
969
+ - "Vi phạm luật R56: `hooks/` chỉ chứa hook: `hooks/swr` (mỗi operation của `modules/api` một hook SWR), `hooks/rhf`, `hooks/zustand/<store>`, `hooks/socketio`, `hooks/effects`, `hooks/reuseables` và `hooks/<domain>/` với một `<domain>.shared.ts` cho helper dùng chung."
1010
970
  nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra (nợ hàng loạt có codemod của HFS); không cần ai can thiệp thêm."
1011
971
  owner: op-retry
1012
972
  kind: check-finding
@@ -1175,12 +1135,12 @@ FE_ROUTE_FILES_THIN:
1175
1135
  kind: check-finding
1176
1136
 
1177
1137
  FE_ROUTE_HANDLER_FORBIDDEN:
1178
- title: "A front-end route handler is not health/live or the Supabase auth callback slot"
1138
+ title: "A front-end route handler is not health/live or the auth callback slot"
1179
1139
  title_vi: "Route handler phía front end không thuộc hai cửa được phép"
1180
- meaning_vi: "`<path>` là `route.ts` phía fe nhưng không phải `health/live` và không thuộc ô `fe.route.callback`."
1140
+ meaning_vi: "`<path>` là `route.ts` phía fe nhưng không phải `app/health/live/route.ts` và không thuộc ô `fe.route.callback`; front end của mọi app không phục vụ API."
1181
1141
  causes_vi:
1182
- - "Vi phạm L15: webhook, cron, public API và callback bên thứ ba là feature của back end."
1183
- nextStep_vi: "Chuyển handler sang feature back end; chỉ giữ health/live và callback PKCE trong ô `fe.route.callback`."
1142
+ - "Vi phạm luật R243: front end không có route handler nào ngoài health/live và callback auth; webhook, cron, public API và callback bên thứ ba là feature của back end, dữ liệu đi qua `modules/api` và hook SWR."
1143
+ nextStep_vi: "Chuyển handler sang feature back end (GraphQL cho front end, REST cho webhook/public/OAuth) và gọi nó từ `modules/api`; chỉ giữ health/live và callback trong ô `fe.route.callback`."
1184
1144
  owner: op-retry
1185
1145
  kind: check-finding
1186
1146
 
@@ -1195,6 +1155,16 @@ FE_ROUTE_ONE_PAGE:
1195
1155
  owner: other-op:interface.implement
1196
1156
  kind: check-finding
1197
1157
 
1158
+ FE_SERVER_ACTION_FORBIDDEN:
1159
+ title: "A front-end file carries a `\"use server\"` directive"
1160
+ title_vi: "Front end có Server Action (\"use server\")"
1161
+ meaning_vi: "`<path>` mang chỉ thị `\"use server\"` (ở đầu module hoặc trong một hàm); front end của mọi app không có Server Action."
1162
+ causes_vi:
1163
+ - "Vi phạm luật R243: front end gọi back end qua mutation GraphQL trong `modules/api/graphql/mutations` bọc bởi hook `useMutate*Swr`, kết quả báo bằng `modules/toast` (R212 case-2)."
1164
+ nextStep_vi: "Xoá Server Action, thêm mutation GraphQL ở back end và `mutation-<x>.ts` trong `modules/api`, gọi qua `useMutate<X>Swr` và `useGraphQLWithToast`; việc nặng là mutation đưa job vào hàng đợi rồi theo dõi bằng status query hoặc socket."
1165
+ owner: op-retry
1166
+ kind: check-finding
1167
+
1198
1168
  FE_SERVICE_ROLE_FORBIDDEN:
1199
1169
  title: "A service-role or credential-shaped environment value enters the front end"
1200
1170
  title_vi: "Service role hoặc biến môi trường bí mật đi vào front end"
@@ -1226,13 +1196,23 @@ FE_SOURCE_LAYOUT_INVALID:
1226
1196
  owner: other-op:interface.implement
1227
1197
  kind: check-finding
1228
1198
 
1199
+ FE_STACK_FORBIDDEN:
1200
+ title: "A front end depends on or imports a package that knowledge/hfs/fe-stack.yaml forbids"
1201
+ title_vi: "Front end dùng thư viện bị fe-stack cấm"
1202
+ meaning_vi: "`<path>` khai báo phụ thuộc hoặc import `<package>`, thư viện nằm trong `forbids` của concern `<concern>` trong knowledge/hfs/fe-stack.yaml; mỗi concern chỉ có một thư viện chuẩn."
1203
+ causes_vi:
1204
+ - "Vi phạm luật R244: front end dùng đúng thư viện chuẩn của từng concern trong fe-stack.yaml (phiên bản ở canon-pins.yaml), không cài thư viện thay thế bên cạnh."
1205
+ nextStep_vi: "Gỡ `<package>` khỏi package.json và thay import bằng thư viện chuẩn của concern đó; nếu cần một concern mới, thêm nó vào fe-stack.yaml và canon-pins.yaml trước."
1206
+ owner: op-retry
1207
+ kind: check-finding
1208
+
1229
1209
  FE_SUPABASE_CLIENT_OWNER:
1230
- title: "A front-end Supabase client, import or call is outside the db owner, or is not typed with `Database`"
1231
- title_vi: "Supabase client phía front end sai chủ sở hữu hoặc thiếu kiểu Database"
1232
- meaning_vi: "Import/call Supabase ở `<path>` không thuộc ô `fe.modules.db`/`fe.package.db`, hoặc client được tạo mà không mang kiểu `Database` đã sinh."
1210
+ title: "A front-end Supabase import or call is outside `fe.modules.auth`, or reaches past the auth API"
1211
+ title_vi: "Supabase phía front end ngoài modules/auth hoặc dùng ngoài API auth"
1212
+ meaning_vi: "Import/call Supabase ở `<path>` không thuộc ô `fe.modules.auth`, hoặc gọi API ngoài auth (`.from`, `.rpc`, `.storage`, `.channel`): front end không đọc hay ghi dữ liệu qua Supabase."
1233
1213
  causes_vi:
1234
- - "Vi phạm L10: chỉ các ô db được import/call `@supabase/ssr` hay `@supabase/supabase-js`; mọi client là `SupabaseClient<Database>`/`create*Client<Database>`."
1235
- nextStep_vi: "Chuyển factory và truy vấn vào ô db đúng của app/gói, import API công khai của ô đó, và thêm kiểu `Database`; không thêm ngoại lệ."
1214
+ - "Vi phạm luật R217: Supabase trên front end chỉ là xác thực (đăng nhập, phiên, access token) trong `modules/auth`; dữ liệu đi qua `modules/api` (Apollo GraphQL) và hook SWR tới back end (R212 case-2)."
1215
+ nextStep_vi: "Chuyển client Supabase vào `modules/auth` và chỉ giữ API auth; đọc/ghi dữ liệu bằng query/mutation GraphQL trong `modules/api` và hook `hooks/swr`; không thêm ngoại lệ."
1236
1216
  owner: op-retry
1237
1217
  kind: check-finding
1238
1218
 
@@ -1271,11 +1251,11 @@ FE_TIER_DIRECTION:
1271
1251
  kind: check-finding
1272
1252
 
1273
1253
  FE_TRANSPORT_OWNER:
1274
- title: "One `fetch`, in the repository's one transport client, with timeout and abort"
1275
- title_vi: "fetch ngoài client duy nhất"
1276
- meaning_vi: "`fetch` ở `<file>` ngoài client truyền tải duy nhất của repository (gọi, gán lại hoặc truyền như giá trị), thư viện HTTP khác được import, reader `read-*.ts` không đi qua client, repository có nhiều hơn một client hoặc nhiều hơn một union `Outcome` (mỗi app giữ một bản, hoặc gói api cộng thêm một bản của app), hoặc state dùng chung dạng `let`."
1254
+ title: "One transport: the Apollo clients and the one axios instance of modules/api"
1255
+ title_vi: "Gọi back end ngoài transport duy nhất"
1256
+ meaning_vi: "`fetch` hoặc thư viện HTTP/GraphQL khác ở `<file>` ngoài transport duy nhất của repository (gọi, gán lại hoặc truyền như giá trị), repository có nhiều hơn một thư mục client hoặc nhiều hơn một envelope `GraphQLResponse` (mỗi app giữ một bản, hoặc gói api cộng thêm một bản của app), hoặc state dùng chung dạng `let` giữ token/locale."
1277
1257
  causes_vi:
1278
- - "Vi phạm luật R50: Mỗi repository đúng một client `fetch`, có timeout + `AbortSignal`: `packages/<family>-api/src/client.ts` khi các app dùng chung, hoặc `modules/api/client.ts` của app duy nhất; không singleton `let` giữ token/locale; credential truyền bằng tham số/context."
1258
+ - "Vi phạm luật R50: Mỗi repository đúng một transport: client Apollo auth/no-auth với chuỗi link (retry, error, timeout, refresh, access token, http) trong `modules/api/graphql/clients/` của app duy nhất hoặc `packages/<family>-api/src/graphql/clients/` khi các app dùng chung, và một instance axios trong `modules/api/rest` cho upload, presigned URL và endpoint auth; component đọc qua hook `hooks/swr`."
1279
1259
  nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra và mục của luật trong tiêu chuẩn HFS; nợ cũ cần quyết định thiết kế nên op không thêm ngoại lệ, chỉ tách hoặc chuyển về đúng chủ."
1280
1260
  owner: op-retry
1281
1261
  kind: check-finding
@@ -1283,9 +1263,9 @@ FE_TRANSPORT_OWNER:
1283
1263
  FE_WIRE_GENERATED:
1284
1264
  title: "Wire types generated from the be contract snapshots the fe side reads"
1285
1265
  title_vi: "Kiểu wire gõ tay"
1286
- meaning_vi: "`<file>` tự gõ kiểu wire/ép kiểu phản hồi, hoặc kiểu sinh ra cũ hơn ảnh chụp hợp đồng. Dùng kiểu sinh từ `be/contracts/`."
1266
+ meaning_vi: "`<file>` tự gõ kiểu wire/ép kiểu phản hồi, hoặc kiểu sinh ra cũ hơn ảnh chụp hợp đồng. Dùng kiểu do graphql-codegen sinh từ `be/contracts/<app>/schema.graphql`."
1287
1267
  causes_vi:
1288
- - "Vi phạm luật R52: Kiểu wire sinh từ ảnh chụp hợp đồng của be (`be/contracts/`, chạy `starci gate run --root <app>` ở gốc app); tài liệu GraphQL trong `.graphql`; cấm `as GraphqlResult<T>`; enum wire không phải `string`."
1268
+ - "Vi phạm luật R52: Kiểu wire do graphql-codegen sinh từ `be/contracts/<app>/schema.graphql` (đọc tại chỗ, không chép) vào `modules/api/graphql/__generated__`; kiểu request/response của mỗi operation re-export kiểu sinh; cấm ép kiểu phản hồi; enum wire không phải `string`."
1289
1269
  nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra và mục của luật trong tiêu chuẩn HFS; nợ cũ cần quyết định thiết kế nên op không thêm ngoại lệ, chỉ tách hoặc chuyển về đúng chủ."
1290
1270
  owner: op-retry
1291
1271
  kind: check-finding
@@ -1892,7 +1872,7 @@ HFS_REQUIRED_FILE_MISSING:
1892
1872
  title_vi: "Thiếu tệp, thư mục hoặc số lượng tối thiểu bắt buộc theo bảng kê"
1893
1873
  meaning_vi: "Bảng kê HFS đòi một tệp hoặc thư mục phải có trong mỗi phần của kho (gói dùng chung, module frontend, api, i18n, tuyến, layout...), hoặc đòi kho có ít nhất một ứng dụng hay tính năng thuộc loại bắt buộc. Mục đó không tồn tại; khi là số lượng tối thiểu thì báo tại hfs.json. Thiếu nó thì phần liên quan chưa hoạt động đầy đủ hoặc kho chưa phải một sản phẩm hoàn chỉnh."
1894
1874
  causes_vi:
1895
- - "Tạo thư mục mới nhưng chưa thêm tệp bắt buộc như index.ts, client.ts, outcome.ts hoặc package.json"
1875
+ - "Tạo thư mục mới nhưng chưa thêm tệp bắt buộc như index.ts, store.ts, toast.tsx hoặc package.json"
1896
1876
  - "Tệp có nhưng tên hoặc chỗ đặt khác với bảng kê"
1897
1877
  - "Kho chưa có tính năng hoặc ứng dụng nào của loại bắt buộc"
1898
1878
  nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để tạo tệp còn thiếu đúng chỗ bảng kê chỉ định; nếu cấu trúc hiện có là chủ ý thì đó là việc sửa bảng kê, do người giám sát làm, không phải kho tự đổi."
@@ -266,3 +266,51 @@ removed:
266
266
  use: 'probeDraft reads only and returns verdict, draft and sends; a foreign draft receives no keys'
267
267
  since: 1.0.0-alpha.9
268
268
  match: '\b(?:draftProbe|probeDraft)\.(?:restore|restored|removed|after)\b'
269
+ - id: code-fe-db-result-typed
270
+ kind: code
271
+ name: FE_DB_RESULT_TYPED
272
+ use: 'the front end reads no data through Supabase (R217, auth only in fe.modules.auth); reads are GraphQL queries of modules/api wrapped by hooks/swr, typed by graphql-codegen (R52 FE_WIRE_GENERATED)'
273
+ since: 1.0.0-alpha.13
274
+ - id: code-fe-db-error-handled
275
+ kind: code
276
+ name: FE_DB_ERROR_HANDLED
277
+ use: 'a failure arrives as the GraphQLResponse envelope of modules/api/graphql/types.ts and is never collapsed into empty data (R51 FE_HTTP_STATUS_COLLAPSE)'
278
+ since: 1.0.0-alpha.13
279
+ - id: code-fe-auth-session-trust
280
+ kind: code
281
+ name: FE_AUTH_SESSION_TRUST
282
+ use: 'the front end runs no server code that trusts a session (R243 FE_SERVER_ACTION_FORBIDDEN); the back end verifies the access token against the JWKS (R219 BE_SUPABASE_JWT_VERIFIED)'
283
+ since: 1.0.0-alpha.13
284
+ - id: code-fe-db-write-shape
285
+ kind: code
286
+ name: FE_DB_WRITE_SHAPE
287
+ use: 'a write is a GraphQL mutation of modules/api wrapped by a useMutate<X>Swr hook and reported by modules/toast; Server Actions are refused (R243 FE_SERVER_ACTION_FORBIDDEN)'
288
+ since: 1.0.0-alpha.13
289
+ - id: slot-fe-modules-db
290
+ kind: vocabulary
291
+ name: fe.modules.db
292
+ use: 'the front end reads and writes through the back end GraphQL API in fe.modules.api, wrapped by hooks/swr; Supabase on the front end is fe.modules.auth only (owner ruling 2026-10-13)'
293
+ since: 1.0.0-alpha.13
294
+ match: '\bfe\.modules\.db\b'
295
+ - id: slot-fe-package-db
296
+ kind: vocabulary
297
+ name: fe.package.db
298
+ use: 'a shared front-end data client is fe.package.api (Apollo clients and the GraphQLResponse union); Supabase on the front end is fe.modules.auth only'
299
+ since: 1.0.0-alpha.13
300
+ match: '\bfe\.package\.db\b'
301
+ - id: slot-be-feature-realtime-supabase
302
+ kind: vocabulary
303
+ name: be.feature.realtime.supabase
304
+ use: 'both editions push through the socket.io gateway be.feature.realtime.websocket; lite runs it in the api process'
305
+ since: 1.0.0-alpha.13
306
+ match: '\bbe\.feature\.realtime\.supabase\b'
307
+ - id: slot-key-anonymous-actions
308
+ kind: vocabulary
309
+ name: anonymousActions
310
+ use: 'the front end holds no Server Action (R243 FE_SERVER_ACTION_FORBIDDEN), so no slot lists anonymous ones'
311
+ since: 1.0.0-alpha.13
312
+ - id: lint-heroicons-is-the-glyph-vendor
313
+ kind: vocabulary
314
+ name: heroicons-is-the-glyph-vendor
315
+ use: 'the eslint-fe rule is one-icon-set: the application icon set is @phosphor-icons/react (knowledge/hfs/fe-stack.yaml icons), imported only by the Icon leaf; the grammar Icon takes whatever glyph the app supplies'
316
+ since: 1.0.0-alpha.13
@@ -1,6 +1,5 @@
1
1
  import { builtinModules } from 'node:module';
2
2
  import { sourceLocation } from '../../lib/ts-ast.mjs';
3
- import { isServerActionModule } from './server-action.mjs';
4
3
 
5
4
  /**
6
5
  * R55 `client-reaches-server` (FE_CLIENT_REACHES_SERVER), the repository half of the eslint rule `client-no-server-import`,
@@ -58,7 +57,6 @@ const scanClientEntry = (entry, { ts, graph, runtime, violations }) => {
58
57
  const seen = new Map([[entry.rel, { chain: [entry.rel], first: null }]]);
59
58
  const queue = [entry.rel];
60
59
  for (const rel of queue) {
61
- if (isServerActionModule(ts, graph.files.get(rel).sourceFile)) continue;
62
60
  const state = seen.get(rel);
63
61
  for (const hit of runtime.serverOf(rel)) {
64
62
  const at = state.first ?? sourceLocation(graph.files.get(rel).sourceFile, hit.node);
@@ -199,7 +199,8 @@ function inferredLayout(root, workspaces) {
199
199
  features: collect('src/features'),
200
200
  components: [...new Set(components)],
201
201
  hooks: collect('src/hooks'),
202
- modules: collect('src/modules'),
202
+ // src/redux/ (fe.redux) is a modules-tier owner beside src/modules/ (owner ruling 2026-10-13, fe-stack state-global).
203
+ modules: [...collect('src/modules'), ...collect('src/redux')],
203
204
  transport: collect('src/modules/api'),
204
205
  };
205
206
  }
@@ -291,7 +292,7 @@ function derivedGrammar(root, packageRoot, workspaces, apps = []) {
291
292
  if (declaresGrammar(pkg)) consumerManifests.push(manifest);
292
293
  }
293
294
  if (!consumerManifests.length) return null;
294
- return { package: GRAMMAR_PACKAGE, entry: `${GRAMMAR_PACKAGE}/common`, styleEntry: `${GRAMMAR_PACKAGE}/common.css`, styleSources, consumerManifests, peers: ['react', '@heroui/react'] };
295
+ return { package: GRAMMAR_PACKAGE, entry: `${GRAMMAR_PACKAGE}/common`, styleEntry: `${GRAMMAR_PACKAGE}/common.css`, styleSources, consumerManifests, peers: ['react', '@heroui/react', '@xyflow/react'] };
295
296
  }
296
297
 
297
298
  /**
@@ -8,7 +8,7 @@
8
8
  * so a file only a spec imports is dead, on purpose. Type-only imports reach a file.
9
9
  *
10
10
  * Owner = every graph.ownerRoots unit whose tier is not `app` and whose public entry file is in the graph (index.ts or
11
- * index.tsx, a package's src/index.ts). The `entries` its slot declares (slot data, e.g. fe.modules.db browser.ts) are further
11
+ * index.tsx, a package's src/index.ts). The `entries` its slot declares (slot data: a second public entry file of the owner) are further
12
12
  * public entries of the same owner, judged exactly like the entry. The exported names of an entry are its named exports, `export { a as b }`,
13
13
  * `export { x } from`, declarations with `export`, and `default` for `export default`. `export *` is refused by owners.mjs
14
14
  * and contributes no name here.
@@ -34,6 +34,7 @@ import fs from 'node:fs';
34
34
  import { canonical } from './config.mjs';
35
35
  import path from 'node:path';
36
36
  import { treeOf } from './required-files.mjs';
37
+ import { globExpression } from '../../lib/glob.mjs';
37
38
 
38
39
  export const DEAD_EXPORT_RULE_IDS = ['HFS_UNUSED_EXPORT', 'HFS_UNUSED_FILE'];
39
40
 
@@ -59,7 +60,10 @@ export function entryOf(graph, config, key, owner) {
59
60
  /** The additional public entry files (repository-relative, in the graph) the slot manifest declares for the owner (slot field `entries`, on the slot that holds its entry file). */
60
61
  function extraEntries(graph, entry) {
61
62
  const found = graph.resolver.classifyPath(entry);
62
- return (found.slot ? graph.resolver.slot(found.slot)?.entries ?? [] : []).map(name => `${found.root}/${name}`).filter(rel => graph.files.has(rel));
63
+ const names = found.slot ? graph.resolver.slot(found.slot)?.entries ?? [] : [];
64
+ // an entry holding `*` (one file per operation) names every graph file it matches
65
+ const matches = name => [...graph.files.keys()].filter(rel => globExpression(`${found.root}/${name}`).test(rel));
66
+ return names.flatMap(name => (name.includes('*') ? matches(name) : [`${found.root}/${name}`].filter(rel => graph.files.has(rel))));
63
67
  }
64
68
 
65
69
  const importBindings = (ts, declaration) => {
@@ -33,13 +33,22 @@ const isGraphqlRegistration = (kit, checker, node) => kit.ts.isCallExpression(no
33
33
  && ['forRoot', 'forRootAsync'].includes(node.expression.name.text)
34
34
  && kit.isImportOf(checker, node.expression.expression, 'GraphQLModule', '@nestjs/graphql');
35
35
 
36
+ /**
37
+ * The formatter a `formatError` value names: the value itself, or the callee of an arrow whose body only calls it (an arrow that
38
+ * binds the errors service, `(formatted, error) => formatGraphqlError(errors, formatted, error)`).
39
+ */
40
+ function formatterOf(ts, value) {
41
+ if (value && ts.isArrowFunction(value) && ts.isCallExpression(value.body)) return value.body.expression;
42
+ return value;
43
+ }
44
+
36
45
  function passesMaskedFormatError(kit, inErrors, checker, call) {
37
46
  const { ts } = kit;
38
47
  let masked = false;
39
48
  for (const argument of call.arguments) {
40
49
  kit.walk(argument, inner => {
41
50
  if ((ts.isPropertyAssignment(inner) || ts.isShorthandPropertyAssignment(inner)) && kit.propertyNameText(inner.name) === 'formatError'
42
- && inErrors(checker, kit.valueOfProperty(inner))) masked = true;
51
+ && inErrors(checker, formatterOf(ts, kit.valueOfProperty(inner)))) masked = true;
43
52
  });
44
53
  }
45
54
  return masked;
@@ -8,7 +8,7 @@ import { checkWorldRenderBoundaries } from './frontend-world.mjs';
8
8
  import { checkGrammar } from './frontend-grammar.mjs';
9
9
  import { checkRoute } from './frontend-routing.mjs';
10
10
  const FEATURE_TIERS = new Set(['pages', 'layouts', 'overlays']);
11
- const COMPONENT_TIERS = new Set(['blocks', 'composites', 'branches', 'leaves']);
11
+ const COMPONENT_TIERS = new Set(['blocks', 'composites', 'branches', 'leaves', 'providers']);
12
12
 
13
13
  function absolute(root, relative) {
14
14
  return path.resolve(root, ...relative.split('/'));
@@ -90,7 +90,7 @@ function checkFrontendSourceLayout(config, sourceFile, roots) {
90
90
  if (located.role === 'hooks') valid = located.relative.includes('/') || roleEntry(located.relative);
91
91
  return valid ? [] : [{ ruleId: 'FE_SOURCE_LAYOUT_INVALID', path: relativePath(config.root, sourceFile.fileName), line: 1, column: 1,
92
92
  message: ({ features: 'Frontend features contain only pages, layouts and overlays plus an optional root public entry.',
93
- components: 'Frontend components contain only blocks, composites, branches and leaves plus an optional root public entry.',
93
+ components: 'Frontend components contain only blocks, composites, branches, leaves and providers plus an optional root public entry.',
94
94
  hooks: 'Every authored custom hook is grouped below a domain folder under hooks; only the root public entry may sit directly under hooks.' })[located.role] }];
95
95
  }
96
96
 
@@ -139,8 +139,15 @@ function reportExportedHooks(state) {
139
139
  }
140
140
  }
141
141
 
142
+ /** True when the slot that owns this file names it as its `hooks` role (redux/hooks.ts, modules/toast/hooks.ts): the owner's own typed hooks. */
143
+ function slotHooksRole(config, fileName) {
144
+ const found = config.hfs?.classifyPath?.(relativePath(config.root, fileName));
145
+ const role = found?.slot ? config.hfs.slot(found.slot)?.roles?.hooks : undefined;
146
+ return typeof role === 'string' && path.basename(fileName) === role;
147
+ }
148
+
142
149
  function checkCustomHookLocations(config, context, sourceFile, roots) {
143
- if (insideAny(roots.hooks, sourceFile.fileName)) return [];
150
+ if (insideAny(roots.hooks, sourceFile.fileName) || slotHooksRole(config, sourceFile.fileName)) return [];
144
151
  const { ts } = context;
145
152
  const state = { ts, config, sourceFile, checker: context.checkerFor(sourceFile.fileName), violations: [], seen: new Set() };
146
153
  visitHookDeclarations(state, sourceFile);