@starci/eslint-canon-be 3.2.13 → 3.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/package.json +1 -1
  3. package/runtime/knowledge/hfs/canon-pins.yaml +207 -15
  4. package/runtime/knowledge/hfs/fe-stack.yaml +218 -0
  5. package/runtime/knowledge/hfs/rules.yaml +51 -48
  6. package/runtime/knowledge/hfs/slots.yaml +319 -179
  7. package/runtime/knowledge/patterns/fe/folder.yaml +24 -18
  8. package/runtime/knowledge/sonar-gate.yaml +10 -2
  9. package/runtime/modules/kernel/removed-vocabulary.yaml +48 -0
  10. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +0 -2
  11. package/runtime/scripts/hfs/architecture/config.mjs +3 -2
  12. package/runtime/scripts/hfs/architecture/dead-exports.mjs +6 -2
  13. package/runtime/scripts/hfs/architecture/error-masked.mjs +10 -1
  14. package/runtime/scripts/hfs/architecture/frontend.mjs +10 -3
  15. package/runtime/scripts/hfs/architecture/hfs.mjs +6 -3
  16. package/runtime/scripts/hfs/architecture/index.mjs +2 -0
  17. package/runtime/scripts/hfs/architecture/owners.mjs +19 -16
  18. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +4 -1
  19. package/runtime/scripts/hfs/architecture/server-action.mjs +75 -20
  20. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +2 -2
  21. package/runtime/scripts/hfs/architecture/supabase.mjs +36 -210
  22. package/runtime/scripts/hfs/architecture/tiers.mjs +1 -33
  23. package/runtime/scripts/hfs/architecture/transport-owner.mjs +110 -54
  24. package/runtime/scripts/hfs/architecture/type-context.mjs +4 -1
  25. package/runtime/scripts/hfs/edition-slots.mjs +13 -4
  26. package/runtime/scripts/hfs/manifest-shape.mjs +3 -3
  27. package/runtime/scripts/hfs/rule-catalog.mjs +21 -1
  28. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +4 -5
  29. package/runtime/scripts/hfs/architecture/supabase-results.mjs +0 -182
@@ -12,12 +12,12 @@
12
12
  # (`at`) that emits one of the rule's codes. scripts/checks/check-hfs-rules.mjs holds all of this to the packages, the
13
13
  # scripts and modules/kernel/failure-codes.yaml.
14
14
  #
15
- # Every rule is an error from 2.0. There is no baseline, allowlist or suppression file for a product. The runtime gate
15
+ # Every rule is an error. There is no baseline, allowlist or suppression file for a product. The runtime gate
16
16
  # (the StarCi runtime repository judged by knowledge/hfs/runtime-slots.yaml) has exactly one, the shrink-only sections of
17
17
  # modules/kernel/allowlist.yaml, held by RT_ALLOWLIST_SPRAWL. Codes are quoted: the failure-code
18
18
  # scanner reads this file as an emitter of every code it names.
19
- schema: starci/hfs-rules@2
20
- version: 2.0.0
19
+ schema: starci/hfs-rules@3
20
+ version: 3.0.0
21
21
  gates:
22
22
  pre-commit: the managed .husky/pre-commit at the app root - starci app hygiene (staged .starciwork and .starcistacks paths, secrets guard), typecheck, ESLint per side and stylelint on the staged files, prettier --check, the unit specs the staged files touch
23
23
  pre-push: typecheck, lint (starci app lint), format:check and the unit specs affected since origin/main; runs every pre-commit rule again
@@ -463,7 +463,6 @@ rules:
463
463
  kinds: [lint, design]
464
464
  gates: [land, ci]
465
465
  failureCodes: ["BE_BACKGROUND_UNOWNED"]
466
- editions: [full]
467
466
  enforcers:
468
467
  - {kind: eslint-be, id: background-unowned}
469
468
  - id: R47
@@ -472,7 +471,6 @@ rules:
472
471
  kinds: [check, codemod, lint]
473
472
  gates: [land, ci]
474
473
  failureCodes: ["BE_TEST_TOPOLOGY", "BE_CONTRACT_UNGUARDED"]
475
- editions: [full]
476
474
  enforcers:
477
475
  - {kind: hfs, id: test-topology, at: scripts/hfs/rules/test-topology.mjs}
478
476
  - {kind: machine, id: test-kind-unsupported, at: scripts/hfs/architecture/hfs.mjs}
@@ -488,7 +486,6 @@ rules:
488
486
  kinds: [lint, design]
489
487
  gates: [settle, land, ci]
490
488
  failureCodes: ["BE_SPEC_QUALITY"]
491
- editions: [full]
492
489
  enforcers:
493
490
  - {kind: eslint-be, id: spec-no-source-read}
494
491
  - {kind: eslint-be, id: e2e-uses-production-transport}
@@ -521,7 +518,7 @@ rules:
521
518
  - {kind: eslint-fe, id: no-hardcoded-endpoint-fallback}
522
519
  - id: R50
523
520
  code: "FE_TRANSPORT_OWNER"
524
- law: "One `fetch` per front end, in the transport client: `fe/apps/<app>/src/modules/api/client.ts` of a one-app front end or `fe/packages/<family>-api/src/client.ts` shared by every app; timeout and abort; a client read is SWR whose key carries the identity of its result, and a mutation is tied to its resource."
521
+ law: "One transport per front end: the browser reaches the back end only through the auth and no-auth Apollo clients and their link chain (retry, error, timeout, token refresh, access token, http) in `fe/apps/<app>/src/modules/api/graphql/clients/` when one app calls the back end, or `fe/packages/<family>-api/src/graphql/clients/` shared by every app that does when several do (an app that calls no back end, such as a marketing landing, holds no transport), and through the one axios instance of `modules/api/rest` for uploads, presigned URLs and auth endpoints; no other `fetch`, HTTP or GraphQL client, and no token or locale held in module state; a component reads through an SWR hook of `hooks/swr` whose key carries the identity of its result, and a mutation hook is tied to its resource."
525
522
  kinds: [lint, design]
526
523
  gates: [settle, land, ci]
527
524
  failureCodes: ["FE_TRANSPORT_OWNER", "FE_EFFECT_FETCH", "FE_SWR_KEY_IDENTITY", "FE_SWR_MUTATION_RESOURCE_IDENTITY"]
@@ -536,7 +533,7 @@ rules:
536
533
  - {kind: eslint-fe, id: swr-data-lifecycle}
537
534
  - id: R51
538
535
  code: "FE_HTTP_STATUS_COLLAPSE"
539
- law: "One `Outcome<T>` union per front end (the api slot's `outcome.ts`); 401 and 403 become `refused`."
536
+ law: "One `GraphQLResponse<T>` envelope (`success`, `message`, `error`, `data`) per front end, declared in `modules/api/graphql/types.ts` (or the shared api package's); a failure is never collapsed into empty data, and an UNAUTHENTICATED or FORBIDDEN answer (401, 403) becomes `refused`: the error link refreshes the token once, then the session sends the user to sign-in."
540
537
  kinds: [lint, design]
541
538
  gates: [settle, land, ci]
542
539
  failureCodes: ["FE_HTTP_STATUS_COLLAPSE", "FE_OUTCOME_KIND_UNHANDLED"]
@@ -547,7 +544,7 @@ rules:
547
544
  - {kind: eslint-fe, id: one-outcome-union}
548
545
  - id: R52
549
546
  code: "FE_WIRE_GENERATED"
550
- law: "Wire types generated from the be contract snapshots the fe side reads."
547
+ law: "Wire types are generated by graphql-codegen from `be/contracts/<app>/schema.graphql`, read in place and never copied, into `modules/api/graphql/__generated__`; an operation's request and response types re-export the generated ones and are never typed by hand."
551
548
  kinds: [lint, check, design]
552
549
  gates: [settle, land, ci]
553
550
  failureCodes: ["FE_WIRE_GENERATED"]
@@ -589,7 +586,7 @@ rules:
589
586
  - {kind: eslint-fe, id: client-reaches-server}
590
587
  - id: R56
591
588
  code: "FE_HOOKS_ARE_HOOKS"
592
- law: "`hooks/` hold hooks and one shared file per domain; a product hook is defined only there and imported through its index."
589
+ law: "`hooks/` hold hooks only: `hooks/swr` (one SWR wrapper per modules/api operation), `hooks/rhf` (forms), `hooks/zustand/<store>`, `hooks/socketio`, `hooks/effects`, `hooks/reuseables` and `hooks/<domain>` with one shared file per domain; a product hook is defined only there and imported through its index."
593
590
  kinds: [lint, codemod]
594
591
  gates: [settle, land, ci]
595
592
  failureCodes: ["FE_HOOKS_ARE_HOOKS", "FE_CUSTOM_HOOK_LOCATION", "FE_BLOCK_PRODUCT_HOOK_DEFINITION", "FE_COMPONENT_DEEP_HOOK_IMPORT"]
@@ -990,13 +987,13 @@ rules:
990
987
  - {kind: eslint-fe, id: component-purity}
991
988
  - id: R93
992
989
  code: "FE_VENDOR_BOUNDARY"
993
- law: "Vendor primitives and icons reach components only through their named owner: heroicons through the icon leaf at the glyph scale, every vendor primitive behind a named owner; the Grammar package is imported only through its selected code and style entry and keeps its declared contract."
990
+ law: "Vendor primitives and icons reach components only through their named owner: the one icon set of knowledge/hfs/fe-stack.yaml through the icon leaf at the glyph scale, every vendor primitive behind a named owner; the Grammar package is imported only through its selected code and style entry and keeps its declared contract."
994
991
  kinds: [lint, codemod]
995
992
  gates: [pre-commit, pre-push, settle, land, ci]
996
993
  failureCodes: ["FE_VENDOR_BOUNDARY", "ARCH_GRAMMAR_CONTRACT_INVALID", "ARCH_GRAMMAR_EXPORT_BYPASS"]
997
994
  enforcers:
998
995
  - {kind: eslint-fe, id: no-vendor-icon-outside-icon-leaf}
999
- - {kind: eslint-fe, id: heroicons-is-the-glyph-vendor}
996
+ - {kind: eslint-fe, id: one-icon-set}
1000
997
  - {kind: eslint-fe, id: no-off-scale-glyph-size}
1001
998
  - {kind: eslint-fe, id: no-decorative-icon-in-metric-cell}
1002
999
  - {kind: eslint-fe, id: vendor-primitive-has-named-owner}
@@ -1040,7 +1037,6 @@ rules:
1040
1037
  kinds: [lint]
1041
1038
  gates: [pre-push, settle, land, ci]
1042
1039
  failureCodes: ["BE_TEST_BUILDER_HOME"]
1043
- editions: [full]
1044
1040
  enforcers:
1045
1041
  - {kind: eslint-be, id: persisting-builder-in-builders-slot}
1046
1042
  - id: R99
@@ -1049,7 +1045,6 @@ rules:
1049
1045
  kinds: [lint]
1050
1046
  gates: [pre-push, settle, land, ci]
1051
1047
  failureCodes: ["BE_TEST_ROW_BY_BUILDER"]
1052
- editions: [full]
1053
1048
  enforcers:
1054
1049
  - {kind: eslint-be, id: spec-no-raw-insert}
1055
1050
  - {kind: eslint-be, id: spec-no-repeated-row-literal}
@@ -1059,7 +1054,6 @@ rules:
1059
1054
  kinds: [lint]
1060
1055
  gates: [pre-push, settle, land, ci]
1061
1056
  failureCodes: ["BE_TEST_CONSTRAINTS_ON"]
1062
- editions: [full]
1063
1057
  enforcers:
1064
1058
  - {kind: eslint-be, id: tests-keep-constraints}
1065
1059
  - id: R101
@@ -1068,7 +1062,6 @@ rules:
1068
1062
  kinds: [lint]
1069
1063
  gates: [pre-push, settle, land, ci]
1070
1064
  failureCodes: ["BE_TEST_BUILDER_ARRANGES"]
1071
- editions: [full]
1072
1065
  enforcers:
1073
1066
  - {kind: eslint-be, id: builder-arranges-only}
1074
1067
  - id: R102
@@ -1077,7 +1070,6 @@ rules:
1077
1070
  kinds: [check, lint]
1078
1071
  gates: [land, ci]
1079
1072
  failureCodes: ["BE_SPEC_PLACEMENT"]
1080
- editions: [full]
1081
1073
  enforcers:
1082
1074
  - {kind: hfs, id: spec-placement, at: scripts/hfs/rules/spec-placement.mjs}
1083
1075
  - {kind: eslint-be, id: spec-placement}
@@ -1366,7 +1358,6 @@ rules:
1366
1358
  kinds: [check, codemod]
1367
1359
  gates: [settle, land, ci]
1368
1360
  failureCodes: ["BE_EVENT_CLASS_CONTRACT"]
1369
- editions: [full]
1370
1361
  enforcers:
1371
1362
  - {kind: hfs, id: event-class-contract, at: scripts/hfs/rules/event-bus.mjs}
1372
1363
  - id: R152
@@ -1375,7 +1366,6 @@ rules:
1375
1366
  kinds: [lint]
1376
1367
  gates: [pre-commit, pre-push, settle, land, ci]
1377
1368
  failureCodes: ["BE_OUTBOX_WRITE_TX"]
1378
- editions: [full]
1379
1369
  enforcers:
1380
1370
  - {kind: eslint-be, id: outbox-write-tx}
1381
1371
  - id: R153
@@ -1384,7 +1374,6 @@ rules:
1384
1374
  kinds: [lint]
1385
1375
  gates: [pre-commit, pre-push, settle, land, ci]
1386
1376
  failureCodes: ["BE_EVENT_CONSUMER_SHAPE"]
1387
- editions: [full]
1388
1377
  enforcers:
1389
1378
  - {kind: eslint-be, id: event-consumer-shape}
1390
1379
  - id: R154
@@ -1393,7 +1382,6 @@ rules:
1393
1382
  kinds: [lint]
1394
1383
  gates: [pre-commit, pre-push, settle, land, ci]
1395
1384
  failureCodes: ["BE_QUEUE_PRODUCER_SHAPE"]
1396
- editions: [full]
1397
1385
  enforcers:
1398
1386
  - {kind: eslint-be, id: queue-producer-shape}
1399
1387
  - id: R155
@@ -1420,7 +1408,6 @@ rules:
1420
1408
  kinds: [lint]
1421
1409
  gates: [pre-commit, pre-push, settle, land, ci]
1422
1410
  failureCodes: ["BE_JOB_WRITE_OUTSIDE_OWNER"]
1423
- editions: [full]
1424
1411
  enforcers:
1425
1412
  - {kind: eslint-be, id: job-write-owner}
1426
1413
  - id: R158
@@ -1429,7 +1416,6 @@ rules:
1429
1416
  kinds: [lint]
1430
1417
  gates: [pre-commit, pre-push, settle, land, ci]
1431
1418
  failureCodes: ["BE_JOB_FENCE_REQUIRED"]
1432
- editions: [full]
1433
1419
  enforcers:
1434
1420
  - {kind: eslint-be, id: job-fence-required}
1435
1421
  - id: R159
@@ -1438,7 +1424,6 @@ rules:
1438
1424
  kinds: [lint]
1439
1425
  gates: [pre-commit, pre-push, settle, land, ci]
1440
1426
  failureCodes: ["BE_JOB_RUN_KEY"]
1441
- editions: [full]
1442
1427
  enforcers:
1443
1428
  - {kind: eslint-be, id: job-run-key}
1444
1429
  - id: R160
@@ -1447,7 +1432,6 @@ rules:
1447
1432
  kinds: [lint]
1448
1433
  gates: [pre-commit, pre-push, settle, land, ci]
1449
1434
  failureCodes: ["BE_JOB_SHAPE"]
1450
- editions: [full]
1451
1435
  enforcers:
1452
1436
  - {kind: eslint-be, id: job-shape}
1453
1437
  - id: R161
@@ -1456,7 +1440,6 @@ rules:
1456
1440
  kinds: [lint]
1457
1441
  gates: [pre-commit, pre-push, settle, land, ci]
1458
1442
  failureCodes: ["BE_PROJECTION_WRITE_OWNER"]
1459
- editions: [full]
1460
1443
  enforcers:
1461
1444
  - {kind: eslint-be, id: projection-write-owner}
1462
1445
  - id: R162
@@ -1465,7 +1448,6 @@ rules:
1465
1448
  kinds: [lint]
1466
1449
  gates: [pre-commit, pre-push, settle, land, ci]
1467
1450
  failureCodes: ["BE_PROJECTION_SHAPE"]
1468
- editions: [full]
1469
1451
  enforcers:
1470
1452
  - {kind: eslint-be, id: projection-shape}
1471
1453
  - id: R163
@@ -1498,7 +1480,6 @@ rules:
1498
1480
  kinds: [check, codemod]
1499
1481
  gates: [land, ci]
1500
1482
  failureCodes: ["HFS_EVENT_CONTRACT"]
1501
- editions: [full]
1502
1483
  enforcers:
1503
1484
  - {kind: hfs, id: services-event-contract, at: scripts/hfs/rules/services.mjs}
1504
1485
  - id: R167
@@ -1524,7 +1505,6 @@ rules:
1524
1505
  kinds: [check]
1525
1506
  gates: [land, ci]
1526
1507
  failureCodes: ["BE_SAGA_STEP_COMPENSATION"]
1527
- editions: [full]
1528
1508
  enforcers:
1529
1509
  - {kind: hfs, id: saga-step-compensation, at: scripts/hfs/rules/saga.mjs}
1530
1510
  - id: R170
@@ -1533,7 +1513,6 @@ rules:
1533
1513
  kinds: [check]
1534
1514
  gates: [land, ci]
1535
1515
  failureCodes: ["BE_SAGA_STATE_VERSIONED"]
1536
- editions: [full]
1537
1516
  enforcers:
1538
1517
  - {kind: hfs, id: saga-state-versioned, at: scripts/hfs/rules/saga.mjs}
1539
1518
  - id: R171
@@ -1542,7 +1521,6 @@ rules:
1542
1521
  kinds: [check]
1543
1522
  gates: [land, ci]
1544
1523
  failureCodes: ["BE_SAGA_EVENT_CONTRACT"]
1545
- editions: [full]
1546
1524
  enforcers:
1547
1525
  - {kind: hfs, id: saga-event-contract, at: scripts/hfs/rules/saga.mjs}
1548
1526
  - id: R172
@@ -1551,7 +1529,6 @@ rules:
1551
1529
  kinds: [check]
1552
1530
  gates: [land, ci]
1553
1531
  failureCodes: ["BE_SAGA_CONSUMER_DEDUPE"]
1554
- editions: [full]
1555
1532
  enforcers:
1556
1533
  - {kind: hfs, id: saga-consumer-dedupe, at: scripts/hfs/rules/saga.mjs}
1557
1534
  - id: R173
@@ -1593,7 +1570,6 @@ rules:
1593
1570
  kinds: [check]
1594
1571
  gates: [settle, land, ci]
1595
1572
  failureCodes: ["BE_CONTRACT_BREAKING"]
1596
- editions: [full]
1597
1573
  enforcers:
1598
1574
  - {kind: hfs, id: contract-compat, at: scripts/hfs/rules/contract-compat.mjs}
1599
1575
  - id: R178
@@ -1646,7 +1622,7 @@ rules:
1646
1622
  - {kind: eslint-be, id: webhook-verify-first}
1647
1623
  - id: R184
1648
1624
  code: "BE_REALTIME_SHAPE"
1649
- law: "A realtime file is the one door its role names: `<channel>.gateway.ts` holds one `@WebSocketGateway` class with `@SubscribeMessage` handlers and `<x>.subscription.ts` holds one `@Resolver` class with `@Subscription` operations, and neither carries a query, a mutation, a field resolver, a REST route or the other protocol."
1625
+ law: "A realtime file is the one door its role names: `<channel>.gateway.ts` holds one `@WebSocketGateway` class with `@SubscribeMessage` handlers and `<x>.subscription.ts` holds one `@Resolver` class with `@Subscription` operations, and neither carries a query, a mutation, a field resolver, a REST route or the other protocol. A lite app serves the same socket.io gateway from its api process and no graphql-ws subscription (R212 case-2)."
1650
1626
  kinds: [lint]
1651
1627
  gates: [pre-commit, pre-push, settle, land, ci]
1652
1628
  failureCodes: ["BE_REALTIME_SHAPE"]
@@ -1870,12 +1846,29 @@ rules:
1870
1846
  - {kind: runtime, id: slot-id-shape, at: scripts/checks/check-slot-id-shape.mjs}
1871
1847
  - id: R212
1872
1848
  code: "HFS_EDITION_INVALID"
1873
- law: "hfs.json names an edition the manifest knows (full, or lite; absent means full), and a lite app holds none of what lite does not have: no spec or test file, test script, test dependency or test-tool config, no UAT record, no supabase/functions, no worker app, no event, queue, job, projection or saga pattern, no saga, reactors, jobs or realtime kind."
1849
+ law: "hfs.json names an edition the manifest knows (full, or lite; absent means full). Lite = full minus self-hosted infrastructure: the same monorepo, code structure, slots, patterns and back-end unit specs, every pattern port implemented on Supabase (Postgres, Auth, Storage, Supabase Queues/pgmq for queue and event bus, Postgres locks, pg_cron), the realtime pattern on the same socket.io gateway run inside the be api process (one instance, no Redis adapter), and one front-end standard for both editions (knowledge/hfs/fe-stack.yaml); a lite app holds none of the infrastructure lite does not run: no e2e or contract spec, no `test:e2e`, `test:contract`, `test:stack` or `test:browser` script, no e2e workflow or browser journey, no UAT record, no test container, toxiproxy or browser-runner dependency, no BullMQ, Kafka, Redis or graphql-ws dependency (the libraries lite infraOwners own nowhere), no .starcistacks infra/runtime/seeds tree and no supabase/functions."
1874
1850
  kinds: [check]
1875
1851
  gates: [pre-push, settle, land, ci]
1876
1852
  failureCodes: ["HFS_EDITION_INVALID", "HFS_EDITION_FORBIDDEN_PRESENT"]
1877
1853
  enforcers:
1878
1854
  - {kind: hfs, id: edition, at: scripts/hfs/rules/edition.mjs}
1855
+ cases:
1856
+ - id: case-1
1857
+ date: "2026-10-12"
1858
+ source: owner
1859
+ quote: "For a lite app the back end is exactly the same too, only deployed more simply; the fe and be of a lite app are exactly the same as a full app, except there is no e2e - unit tests stay; a lite app has no infra, a monorepo + Supabase; fe and be in a monorepo like a full app. (translated from Vietnamese)"
1860
+ ruling: "Lite = the full architecture minus self-hosted infrastructure. The same be/apps (api, cli, optional worker), be/src/{features,modules}, fe/apps and fe/packages, supabase/, .starcistacks and .starciwork; the same CQRS shape and patterns declared in hfs.json the same way. Only adapters and delivery differ, all on Supabase: schema authority in supabase/migrations; the queue pattern on Supabase Queues (pgmq) behind the same platform/queue ports (outbox to queue, processors, retry and backoff by visibility timeout and read_ct, archive on dead letter), no BullMQ or Redis; the event-bus pattern on pgmq-backed topics (or in process for a single api) behind the same EventPublisher/EventConsumer ports, no Kafka; realtime on Supabase Realtime (replaced by case-2: the socket.io gateway in the api process); cache and locks on Postgres (advisory locks or an unlogged table); schedules on pg_cron (+ pg_net) or the platform scheduler; storage and auth on Supabase. Back-end unit specs are required exactly as in full (unitRoles, logicRoles, 100 per file, @starci/jest-preset doubles, builders); integration specs are optional and run only on the local Supabase CLI stack (`starci supabase start`, no toxiproxy, no own containers); there is no e2e or contract world and no app.ci-e2e. .starcistacks holds env and secret custody only. The front end and the grammar stay exactly as in full: no tests (R97). The queue transport question was confirmed the same day: Supabase Queues (pgmq). Upgrade lite -> full stays additive: swap the adapters, add the e2e and contract world and the stack."
1861
+ - id: case-2
1862
+ date: "2026-10-13"
1863
+ source: owner
1864
+ quote: "Lite and full apps are the same code structure; lite only swaps the infrastructure for Supabase, has no self-hosted infrastructure and no e2e, and saga and jobs are built exactly like full. The front end of lite and full follows starci-academy: the browser calls the back end directly through modules/api/graphql (Apollo auth and no-auth clients with the link chain) and axios only for non-GraphQL calls, hooks/swr wraps every operation and components call hooks only, modules/toast reports mutation outcomes. Forbidden: Server Actions, Next route handlers except health/live, front-end data reads from Supabase (Supabase on the front end is auth only) and the rpc + polling command pattern; modules/db goes away. Every back-end feature exposes GraphQL for the front end; heavy work is a mutation that enqueues a job and the front end follows its progress. (translated from Vietnamese)"
1865
+ ruling: "Lite and full share one code structure; the edition changes only the infrastructure (Supabase Postgres, Auth, Storage, pgmq and pg_cron instead of self-hosted services), runs the socket.io gateway inside the be api process (one instance, no Redis adapter; be.feature.realtime.websocket is legal in lite and the Supabase Realtime door is gone) and has no e2e. The front end is one standard for both editions, catalogued in knowledge/hfs/fe-stack.yaml (one canonical library, HFS home and pattern per concern, versions in canon-pins.yaml): Apollo GraphQL in modules/api/graphql with codegen from be/contracts/<app>/schema.graphql, axios in modules/api/rest for uploads, presigned URLs and auth endpoints, SWR hooks in hooks/swr, react-hook-form with zod in hooks/rhf, redux for app-wide state, zustand for local state, socket.io-client in hooks/socketio, modules/toast for mutation outcomes and modules/auth as the only front-end Supabase importer (auth only). The front end serves no API: no Server Action anywhere (FE_SERVER_ACTION_FORBIDDEN) and no route handler beyond health/live and the fe.route.callback slot (FE_ROUTE_HANDLER_FORBIDDEN); a package outside the catalog's choice is FE_STACK_FORBIDDEN. Back-end features expose GraphQL (be.transport.graphql) for the front end and keep REST for webhooks, the public API and OAuth callbacks; heavy work is a mutation that enqueues a fenced job or saga, and the front end polls a status query with SWR refreshInterval or listens on a socket namespace."
1866
+ - id: case-3
1867
+ date: "2026-10-13"
1868
+ source: owner
1869
+ quote: "As the student proposes. (translated from Vietnamese; the owner adopted the proposed HFS standard for front end and back end, lite = full)"
1870
+ ruling: "One HFS tree for both editions: the repository root, be/ (apps, contracts, features, modules, tests) and fe/apps/<app>/src have the same shape in lite and full, and code above the platform/integrations seam is the same. The editions differ only behind that seam: the database and its migration authority (lite supabase/migrations; full TypeORM entities and migrations in be.persistence), the auth provider (lite Supabase Auth through fe.modules.auth and a JWKS-verified back end; full the app's identity provider), object storage (lite Supabase Storage; full S3-compatible storage), queue and jobs (lite pgmq and pg_cron; full BullMQ) behind platform/queue, realtime (lite the socket.io gateway in the api process, one instance; full socket.io scaled with a Redis adapter), the event-bus adapter (lite outbox and pgmq; full outbox and Kafka) and hosting; lite has no e2e. Migration baseline at the cutover of an existing app: the live schema is dumped into one baseline migration written to the current standard and marked applied on production without a re-run, the older migrations move to app.supabase.migrations.archive (supabase/migrations/_archive/, never applied and never judged), and new migrations append after the baseline. Assets: static templates, copy and presets are TS constants in the owning domain capability (<x>.templates.ts) and user-editable ones are seed rows; small build-time binaries such as fonts live in be.assets (be/src/assets/<kind>/); large and user media live in object storage and are fetched at runtime; there is no be/resources/."
1871
+
1879
1872
  - id: R213
1880
1873
  code: "DB_MIGRATION_SHAPE"
1881
1874
  law: "A migration is `supabase/migrations/<ts14>_<kebab>.sql`; the stamp is a valid UTC instant not in the future, new migrations sort strictly after every migration on the base branch, and a migration already on the base is immutable (a diff or a removal is a finding); a file that does not parse as PostgreSQL is a shape failure."
@@ -1910,27 +1903,19 @@ rules:
1910
1903
  - {kind: hfs, id: db-config-types, at: scripts/hfs/rules/database.mjs}
1911
1904
  - id: R217
1912
1905
  code: "FE_SUPABASE_CLIENT_OWNER"
1913
- law: "The database has one door per side: `@supabase/ssr` and `@supabase/supabase-js` are imported and called only by the fe db owner (`fe.modules.db`, `fe.package.db`) and, on the back end, by `be.integrations.supabase`, and every client carries the committed `Database` type; the front end has no route handler beyond health/live and the auth callback slot, and holds no credential: `fe/` names no service-role or `*_SECRET`, `*_PRIVATE`, `*_TOKEN`, `*_PASSWORD` value (public configuration is read once by `modules/config`)."
1906
+ law: "Supabase has one door per side: on the front end `@supabase/supabase-js` and `@supabase/ssr` are imported only by `fe.modules.auth` and only their auth API is called (sign-in, sign-out, session, access token; never `.from`, `.rpc`, `.storage` or `.channel`, so the front end reads and writes no data through Supabase), and on the back end only `be.integrations.supabase` imports them, every back-end client carrying the committed `Database` type; the front end holds no credential: `fe/` names no service-role or `*_SECRET`, `*_PRIVATE`, `*_TOKEN`, `*_PASSWORD` value (public configuration is read once by `modules/config`)."
1914
1907
  kinds: [check, design]
1915
1908
  gates: [pre-push, settle, land, ci]
1916
- failureCodes: ["FE_SUPABASE_CLIENT_OWNER", "BE_SUPABASE_CLIENT_OWNER", "FE_ROUTE_HANDLER_FORBIDDEN", "FE_SERVICE_ROLE_FORBIDDEN"]
1909
+ failureCodes: ["FE_SUPABASE_CLIENT_OWNER", "BE_SUPABASE_CLIENT_OWNER", "FE_SERVICE_ROLE_FORBIDDEN"]
1917
1910
  enforcers:
1918
1911
  - {kind: machine, id: supabase-client-owner, at: scripts/hfs/architecture/supabase.mjs}
1919
1912
  - {kind: machine, id: supabase-service-role, at: scripts/hfs/architecture/supabase.mjs}
1920
- - id: R218
1921
- code: "FE_DB_RESULT_TYPED"
1922
- law: "A Supabase result is typed by `Database` inference and never swallowed: no explicit generic on `single`/`maybeSingle`/`returns`, no cast or non-null assertion on a result, every list read carries `.limit()` or a keyset bound, every awaited call reads `error` or hands the whole result to `toOutcome`, and a failure never becomes empty data."
1923
- kinds: [check, design]
1924
- gates: [pre-push, settle, land, ci]
1925
- failureCodes: ["FE_DB_RESULT_TYPED", "FE_DB_ERROR_HANDLED"]
1926
- enforcers:
1927
- - {kind: machine, id: supabase-result, at: scripts/hfs/architecture/supabase.mjs}
1928
1913
  - id: R219
1929
- code: "FE_AUTH_SESSION_TRUST"
1930
- law: "The principal is verified, never trusted: server code never calls `auth.getSession()` (`getClaims()` through `getPrincipal()`, `auth.getUser()` for money, account security and role changes); a `write-*.ts` Server Action awaits `getPrincipal()` first, refuses an anonymous principal (the sign-in action is the one exception) and parses its input by a schema before any use; a back-end principal comes only from a Supabase access token verified against the project JWKS (signature, issuer, audience, expiry, pinned algorithm), never decoded unverified and never from the request body."
1914
+ code: "BE_SUPABASE_JWT_VERIFIED"
1915
+ law: "The principal is verified, never trusted: a back-end principal comes only from a Supabase access token verified against the project JWKS (signature, issuer, audience, expiry, pinned algorithm), never decoded unverified and never from the request body."
1931
1916
  kinds: [check, design]
1932
1917
  gates: [pre-push, settle, land, ci]
1933
- failureCodes: ["FE_AUTH_SESSION_TRUST", "FE_DB_WRITE_SHAPE", "BE_SUPABASE_JWT_VERIFIED"]
1918
+ failureCodes: ["BE_SUPABASE_JWT_VERIFIED"]
1934
1919
  enforcers:
1935
1920
  - {kind: machine, id: supabase-principal, at: scripts/hfs/architecture/supabase.mjs}
1936
1921
  - id: R220
@@ -2153,3 +2138,21 @@ rules:
2153
2138
  failureCodes: ["RT_OP_JUDGE_UNDECLARED"]
2154
2139
  enforcers:
2155
2140
  - {kind: runtime, id: op-judge, at: scripts/checks/check-op-judge.mjs}
2141
+ - id: R243
2142
+ code: "FE_SERVER_ACTION_FORBIDDEN"
2143
+ law: "The front end serves no API, in every app and edition (R212 case-2): no file under `fe/` carries a `\"use server\"` directive (module or function level), and the only route handlers are `app/health/live/route.ts` and the auth callback of the `fe.route.callback` slot; reads and writes reach the back end through `modules/api` and the SWR hooks, and a webhook, cron, public API or third-party callback is a back-end feature."
2144
+ kinds: [check, lint]
2145
+ gates: [pre-commit, pre-push, settle, land, ci]
2146
+ failureCodes: ["FE_SERVER_ACTION_FORBIDDEN", "FE_ROUTE_HANDLER_FORBIDDEN"]
2147
+ enforcers:
2148
+ - {kind: machine, id: server-action-forbidden, at: scripts/hfs/architecture/server-action.mjs}
2149
+ - {kind: eslint-fe, id: no-server-action}
2150
+ - id: R244
2151
+ code: "FE_STACK_FORBIDDEN"
2152
+ law: "A front end uses the one canonical library of each concern of knowledge/hfs/fe-stack.yaml: no front-end package.json (an app or a `fe/packages/*` package) depends on, and no source file under `fe/` imports, a package that the catalog lists under a concern's `forbids`; a missing concern is added to the catalog (with its pin in canon-pins.yaml), never installed beside it."
2153
+ kinds: [check, lint]
2154
+ gates: [pre-commit, pre-push, settle, land, ci]
2155
+ failureCodes: ["FE_STACK_FORBIDDEN"]
2156
+ enforcers:
2157
+ - {kind: hfs, id: fe-stack, at: scripts/hfs/rules/fe-stack.mjs}
2158
+ - {kind: eslint-fe, id: fe-stack-import}