@starci/hfs 4.2.13 → 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.
- package/CHANGELOG.md +14 -0
- package/README.md +7 -3
- package/emit/db-types.mjs +2 -1
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +201 -14
- package/runtime/knowledge/hfs/fe-stack.yaml +218 -0
- package/runtime/knowledge/hfs/rules.yaml +51 -48
- package/runtime/knowledge/hfs/slots.yaml +307 -179
- package/runtime/knowledge/patterns/be/event-bus.yaml +15 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +10 -0
- package/runtime/knowledge/patterns/be/projections.yaml +10 -0
- package/runtime/knowledge/patterns/be/queues.yaml +15 -0
- package/runtime/knowledge/patterns/be/reactors.yaml +8 -0
- package/runtime/knowledge/patterns/be/realtime.yaml +17 -0
- package/runtime/knowledge/patterns/be/saga.yaml +13 -0
- package/runtime/knowledge/patterns/fe/folder.yaml +24 -18
- package/runtime/knowledge/sonar-gate.yaml +10 -2
- package/runtime/modules/kernel/failure-codes.yaml +40 -60
- package/runtime/modules/kernel/removed-vocabulary.yaml +48 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +0 -2
- package/runtime/scripts/hfs/architecture/config.mjs +2 -1
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +6 -2
- package/runtime/scripts/hfs/architecture/error-masked.mjs +10 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +10 -3
- package/runtime/scripts/hfs/architecture/hfs.mjs +6 -3
- package/runtime/scripts/hfs/architecture/index.mjs +2 -0
- package/runtime/scripts/hfs/architecture/owners.mjs +19 -16
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +4 -1
- package/runtime/scripts/hfs/architecture/server-action.mjs +75 -20
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +2 -2
- package/runtime/scripts/hfs/architecture/supabase.mjs +36 -210
- package/runtime/scripts/hfs/architecture/tiers.mjs +1 -33
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +110 -54
- package/runtime/scripts/hfs/architecture/type-context.mjs +4 -1
- package/runtime/scripts/hfs/check.mjs +10 -6
- package/runtime/scripts/hfs/edition-slots.mjs +13 -4
- package/runtime/scripts/hfs/manifest-shape.mjs +3 -3
- package/runtime/scripts/hfs/rule-catalog.mjs +21 -1
- package/runtime/scripts/hfs/rules/database-constants.mjs +4 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +12 -7
- package/runtime/scripts/hfs/rules/database.mjs +4 -3
- package/runtime/scripts/hfs/rules/edition.mjs +49 -37
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +4 -5
- package/runtime/scripts/hfs/rules/fe-stack.mjs +71 -0
- package/scaffold/add-app.mjs +24 -18
- package/scaffold/add-cli-lite.mjs +33 -3
- package/scaffold/add-inbox-lite.mjs +24 -8
- package/scaffold/add-table.mjs +222 -74
- package/scaffold/add.mjs +85 -35
- package/scaffold/app.mjs +140 -36
- package/scaffold/edition-gate.mjs +21 -2
- package/scaffold/lite-exports.mjs +30 -1
- package/scaffold/service.mjs +5 -8
- package/src/main.mjs +2 -2
- package/sync/index.mjs +19 -15
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +14 -0
- package/templates/app/codegen-graphql.mjs.partial +34 -0
- package/templates/app/codegen-lite/scripts/codegen.mjs +18 -13
- package/templates/app/package-scripts/core.json +1 -0
- package/templates/app/package-scripts-lite/package.json +4 -0
- package/templates/app/skeleton/README.md +1 -1
- package/templates/app/skeleton/scripts/codegen.mjs +4 -1
- package/templates/app/skeleton-lite/README.md +7 -4
- package/templates/app/skeleton-lite/supabase/config.toml +1 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +13 -14
- package/templates/be/inbox/database-client.spec.ts.tpl +47 -0
- package/templates/be/inbox/service.spec.ts.tpl +46 -0
- package/templates/be/inbox/webhook-door.spec.ts.tpl +76 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +2 -2
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +6 -3
- package/templates/be/platform/errors-graphql/graphql-error.mapper.spec.ts +52 -0
- package/templates/be/platform/errors-graphql/graphql-error.mapper.ts +31 -0
- package/templates/be/platform/graphql/graphql-depth.policy.spec.ts +43 -0
- package/templates/be/platform/graphql/graphql-depth.policy.ts +21 -0
- package/templates/be/platform/graphql/graphql.contracts.ts +12 -0
- package/templates/be/platform/graphql/graphql.module-definition.ts +7 -0
- package/templates/be/platform/graphql/graphql.module.ts +49 -0
- package/templates/be/platform/graphql/graphql.options.ts +2 -0
- package/templates/be/platform/graphql/index.ts +1 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +114 -0
- package/templates/be/platform/graphql/localize-errors.mapper.ts +51 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +11 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +21 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +94 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +2 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +2 -2
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +2 -6
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +15 -0
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +1 -1
- package/templates/be/table/api/create.command.ts.tpl +10 -0
- package/templates/be/table/api/create.contracts.ts.tpl +8 -0
- package/templates/be/table/api/create.handler.ts.tpl +22 -0
- package/templates/be/table/api/create.mapper.ts.tpl +5 -0
- package/templates/be/table/api/create.resolver.ts.tpl +21 -0
- package/templates/be/table/api/create.type.ts.tpl +9 -0
- package/templates/be/table/api/feature.index.ts.tpl +1 -0
- package/templates/be/table/api/feature.module.ts.tpl +7 -0
- package/templates/be/table/api/get.contracts.ts.tpl +11 -0
- package/templates/be/table/api/get.handler.ts.tpl +22 -0
- package/templates/be/table/api/get.input.ts.tpl +12 -0
- package/templates/be/table/api/get.mapper.ts.tpl +9 -0
- package/templates/be/table/api/get.query.ts.tpl +10 -0
- package/templates/be/table/api/get.resolver.ts.tpl +27 -0
- package/templates/be/table/api/get.type.ts.tpl +9 -0
- package/templates/be/table/api/graphql.module.ts.tpl +8 -0
- package/templates/be/table/error.api.ts.tpl +19 -0
- package/templates/be/table/migration.sql.tpl +13 -21
- package/templates/be/table/service.api.ts.tpl +30 -0
- package/templates/be/table/sql.api.ts.tpl +7 -0
- package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignInForm/index.tsx +16 -18
- package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignOutButton/component.tsx +7 -3
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignOutButton/index.tsx +23 -0
- package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +49 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +4 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +29 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +27 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/effects/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/effects/useAuthSessionEffect.ts +14 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/swr/SwrSideEffects.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/swr/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/types.ts +26 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/client.ts +21 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/contracts.ts +27 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/index.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/api.ts +52 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +33 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +27 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/types/ambient/styles.d.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/ReduxProvider.tsx +11 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +8 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +3 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +26 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +14 -0
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +5 -2
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -2
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +1 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +23 -0
- package/templates/fe/skeleton/apps/landing/src/modules/types/ambient/styles.d.ts +2 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +51 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +10 -13
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +8 -5
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -17
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +3 -10
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +0 -6
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +11 -8
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +0 -3
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +9 -25
- package/templates/fe/table/graphql/mutation.ts.tpl +25 -0
- package/templates/fe/table/graphql/mutation.types.ts.tpl +7 -0
- package/templates/fe/table/graphql/query.ts.tpl +25 -0
- package/templates/fe/table/graphql/query.types.ts.tpl +7 -0
- package/templates/fe/table/graphql/useMutateSwr.ts.tpl +21 -0
- package/templates/fe/table/graphql/useQuerySwr.ts.tpl +17 -0
- package/templates/fe/transport/graphql/clients/clients/auth.ts +39 -0
- package/templates/fe/transport/graphql/clients/clients/index.ts +2 -0
- package/templates/fe/transport/graphql/clients/clients/no-auth.ts +33 -0
- package/templates/fe/transport/graphql/clients/index.ts +2 -0
- package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +9 -0
- package/templates/fe/transport/graphql/clients/links/error.ts +30 -0
- package/templates/fe/transport/graphql/clients/links/http.ts +16 -0
- package/templates/fe/transport/graphql/clients/links/index.ts +8 -0
- package/templates/fe/transport/graphql/clients/links/locale.ts +13 -0
- package/templates/fe/transport/graphql/clients/links/options.ts +17 -0
- package/templates/fe/transport/graphql/clients/links/refresh-token.ts +36 -0
- package/templates/fe/transport/graphql/clients/links/retry.ts +16 -0
- package/templates/fe/transport/graphql/clients/links/timeout.ts +28 -0
- package/templates/fe/transport/graphql/clients/response.ts +43 -0
- package/upgrade/check.mjs +1 -1
- package/upgrade/index.mjs +3 -2
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +0 -182
- package/templates/app/quality-config-lite/sonar-project.properties +0 -5
- package/templates/be/patterns/api/feature.index.http.ts.tpl +0 -1
- package/templates/be/patterns/api/http.controller.ts.tpl +0 -27
- package/templates/be/patterns/api/http.mapper.ts.tpl +0 -9
- package/templates/be/patterns/api/http.module.ts.tpl +0 -7
- package/templates/be/patterns/api/http.request.ts.tpl +0 -10
- package/templates/be/patterns/api/http.response.ts.tpl +0 -5
- package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +0 -24
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +0 -38
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +0 -25
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +0 -25
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +0 -17
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +0 -14
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +0 -3
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +0 -59
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +0 -29
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +0 -70
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +0 -58
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +0 -26
- package/templates/fe/table/read.ts.tpl +0 -16
- package/templates/fe/table/write.ts.tpl +0 -16
- /package/templates/be/patterns/api/{application.command.http.ts.tpl → application.command.principal.ts.tpl} +0 -0
- /package/templates/be/patterns/api/{application.handler.http.ts.tpl → application.handler.principal.ts.tpl} +0 -0
- /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
|
|
33
|
-
|
|
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
|
|
84
|
+
when: "A back-end call or the session"
|
|
82
85
|
write: >
|
|
83
|
-
fe
|
|
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/
|
|
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;
|
|
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
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
309
|
+
times or fills hooks/ with transport calls.
|
|
304
310
|
cases:
|
|
305
311
|
- id: case-1
|
|
306
|
-
when: "
|
|
307
|
-
write: "fe/apps/<app>/src/hooks/
|
|
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
|
|
313
|
-
write: "fe/apps/<app>/src/
|
|
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/
|
|
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
|
|
85
|
-
#
|
|
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
|
-
- "
|
|
893
|
-
nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để đọc dữ liệu
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
1183
|
-
nextStep_vi: "Chuyển handler sang feature back end
|
|
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
|
|
1231
|
-
title_vi: "Supabase
|
|
1232
|
-
meaning_vi: "Import/call Supabase ở `<path>` không thuộc ô `fe.modules.
|
|
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
|
|
1235
|
-
nextStep_vi: "Chuyển
|
|
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
|
|
1275
|
-
title_vi: "
|
|
1276
|
-
meaning_vi: "`fetch` ở `<file>` ngoài
|
|
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
|
|
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ừ
|
|
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,
|
|
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
|
|
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
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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);
|