@starci/hfs 1.0.1 → 2.0.0

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 (246) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +110 -12
  3. package/bin/hfs.mjs +119 -15
  4. package/emit/compiler.mjs +35 -0
  5. package/emit/contracts.mjs +97 -0
  6. package/emit/operations-worker.mjs +24 -0
  7. package/emit/operations.mjs +126 -0
  8. package/emit/schema-worker.mjs +117 -0
  9. package/emit/static-graph.mjs +670 -0
  10. package/emit/type-schema.mjs +145 -0
  11. package/package.json +4 -1
  12. package/report/sonar.mjs +180 -0
  13. package/runtime/engine/admission.mjs +284 -0
  14. package/runtime/engine/digest.mjs +10 -0
  15. package/runtime/engine/ledger-db.mjs +1245 -0
  16. package/runtime/engine/machine-db.mjs +1484 -0
  17. package/runtime/engine/migrations/machine/0001-init.sql +887 -0
  18. package/runtime/engine/migrations/runtime/0001-init.sql +1072 -0
  19. package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +13 -0
  20. package/runtime/engine/migrations/runtime/0004-attempt-why.sql +43 -0
  21. package/runtime/engine/plain-object.mjs +5 -0
  22. package/runtime/knowledge/hfs/canon-pins.yaml +16 -58
  23. package/runtime/knowledge/hfs/slots.yaml +405 -137
  24. package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
  25. package/runtime/knowledge/sonar-gate.yaml +85 -0
  26. package/runtime/modules/kernel/failure-codes.yaml +1480 -16
  27. package/runtime/scripts/checks/architecture/backend.mjs +350 -0
  28. package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
  29. package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
  30. package/runtime/scripts/checks/architecture/clones.mjs +200 -0
  31. package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
  32. package/runtime/scripts/checks/architecture/config.mjs +310 -0
  33. package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
  34. package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
  35. package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
  36. package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
  37. package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
  38. package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
  39. package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
  40. package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
  41. package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
  42. package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
  43. package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
  44. package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
  45. package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
  46. package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
  47. package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
  48. package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
  49. package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
  50. package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
  51. package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
  52. package/runtime/scripts/checks/architecture/index.mjs +316 -0
  53. package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
  54. package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
  55. package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
  56. package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
  57. package/runtime/scripts/checks/architecture/owners.mjs +89 -0
  58. package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
  59. package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
  60. package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
  61. package/runtime/scripts/checks/architecture/registration.mjs +319 -0
  62. package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
  63. package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
  64. package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
  65. package/runtime/scripts/checks/architecture/size-growth.mjs +73 -0
  66. package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
  67. package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
  68. package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
  69. package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
  70. package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
  71. package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
  72. package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
  73. package/runtime/scripts/checks/architecture/typescript.mjs +500 -0
  74. package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
  75. package/runtime/scripts/checks/architecture.mjs +41 -0
  76. package/runtime/scripts/checks/common.mjs +37 -0
  77. package/runtime/scripts/checks/typescript-programs.mjs +82 -0
  78. package/runtime/scripts/lib/artifact-hold.mjs +89 -0
  79. package/runtime/scripts/lib/artifact-store.mjs +103 -0
  80. package/runtime/scripts/lib/fs-kind.mjs +10 -0
  81. package/runtime/scripts/lib/git.mjs +53 -0
  82. package/runtime/scripts/lib/hfs-allows.mjs +57 -0
  83. package/runtime/scripts/lib/hfs-check.mjs +254 -28
  84. package/runtime/scripts/lib/hfs-rules/contract.mjs +126 -0
  85. package/runtime/scripts/lib/hfs-rules/deps.mjs +63 -0
  86. package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
  87. package/runtime/scripts/lib/hfs-rules/frontend.mjs +124 -0
  88. package/runtime/scripts/lib/hfs-rules/lint-suppression.mjs +34 -0
  89. package/runtime/scripts/lib/hfs-rules/pipeline.mjs +51 -0
  90. package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +64 -0
  91. package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
  92. package/runtime/scripts/lib/hfs-rules/repo-local-checks.mjs +32 -0
  93. package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
  94. package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +31 -0
  95. package/runtime/scripts/lib/hfs-rules/stacks.mjs +54 -0
  96. package/runtime/scripts/lib/hfs-rules/test-topology.mjs +31 -0
  97. package/runtime/scripts/lib/hfs-slots.mjs +88 -40
  98. package/runtime/scripts/lib/hfs-tree.mjs +80 -0
  99. package/runtime/scripts/lib/hfs-view.mjs +68 -0
  100. package/runtime/scripts/lib/json.mjs +22 -0
  101. package/runtime/scripts/lib/language.mjs +107 -0
  102. package/runtime/scripts/lib/path-key.mjs +2 -0
  103. package/runtime/scripts/lib/redact.mjs +148 -0
  104. package/runtime/scripts/lib/repo-identity.mjs +50 -0
  105. package/runtime/scripts/lib/safe-remove.mjs +179 -0
  106. package/runtime/scripts/lib/secret-patterns.mjs +44 -0
  107. package/runtime/scripts/lib/sleep-sync.mjs +17 -0
  108. package/runtime/scripts/lib/stack-declaration.mjs +52 -0
  109. package/runtime/scripts/lib/stack-services.mjs +217 -0
  110. package/runtime/scripts/lib/test-secrets.mjs +120 -0
  111. package/scaffold/service.mjs +333 -0
  112. package/sync/format.mjs +46 -0
  113. package/sync/hygiene.mjs +56 -24
  114. package/sync/index.mjs +126 -41
  115. package/sync/managed.mjs +170 -0
  116. package/sync/skeleton.mjs +32 -10
  117. package/sync/sonar-key.mjs +13 -0
  118. package/sync/ts-strict.mjs +48 -0
  119. package/templates/{common → be/ci-workflows/github/workflows}/ci.yml +13 -13
  120. package/templates/be/{e2e.yml → ci-workflows/github/workflows/e2e.yml} +1 -0
  121. package/templates/be/hooks/husky/pre-commit +13 -0
  122. package/templates/be/hooks/husky/pre-push +7 -0
  123. package/templates/be/package-scripts/package.json +21 -0
  124. package/templates/be/{sonar-project.properties → quality-config/sonar-project.properties} +2 -3
  125. package/templates/be/skeleton/apps/__app__/src/app.module.ts +14 -4
  126. package/templates/be/skeleton/apps/__app__/src/main.ts +9 -6
  127. package/templates/be/skeleton/scripts/.gitkeep +0 -0
  128. package/templates/be/skeleton/src/features/system-health/application/check-liveness.contracts.ts +2 -0
  129. package/templates/be/skeleton/src/features/system-health/application/check-liveness.handler.ts +22 -0
  130. package/templates/be/skeleton/src/features/system-health/application/check-liveness.query.ts +11 -0
  131. package/templates/be/skeleton/src/features/system-health/index.ts +1 -1
  132. package/templates/be/skeleton/src/features/system-health/system-health.module.ts +3 -3
  133. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +8 -9
  134. package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +3 -6
  135. package/templates/be/skeleton/src/modules/domain/liveness/index.ts +3 -0
  136. package/templates/be/skeleton/src/modules/domain/liveness/liveness.contracts.ts +9 -0
  137. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +7 -0
  138. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module.ts +14 -0
  139. package/templates/be/skeleton/src/modules/domain/liveness/liveness.options.ts +2 -0
  140. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.spec.ts +40 -0
  141. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.ts +26 -0
  142. package/templates/be/skeleton/src/modules/platform/clock/clock.decorators.ts +9 -0
  143. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +7 -0
  144. package/templates/be/skeleton/src/modules/platform/clock/clock.module.ts +19 -0
  145. package/templates/be/skeleton/src/modules/platform/clock/clock.options.ts +2 -0
  146. package/templates/be/skeleton/src/modules/platform/clock/clock.port.ts +5 -0
  147. package/templates/be/skeleton/src/modules/platform/clock/index.ts +4 -0
  148. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.spec.ts +20 -0
  149. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.ts +9 -0
  150. package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -0
  151. package/templates/be/skeleton/src/modules/platform/composition/index.ts +2 -0
  152. package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +9 -0
  153. package/templates/be/skeleton/src/modules/platform/config/index.ts +2 -3
  154. package/templates/be/skeleton/src/modules/platform/config/server.config.ts +1 -1
  155. package/templates/be/skeleton/src/modules/platform/config/server.options.ts +0 -3
  156. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +5 -0
  157. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.decorators.ts +9 -0
  158. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.handler.ts +27 -0
  159. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.log-events.ts +5 -0
  160. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +7 -0
  161. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module.ts +18 -0
  162. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.options.ts +2 -0
  163. package/templates/be/skeleton/src/modules/platform/cqrs/index.ts +4 -0
  164. package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +8 -12
  165. package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +5 -0
  166. package/templates/be/skeleton/src/modules/platform/errors/index.ts +1 -1
  167. package/templates/be/skeleton/src/modules/platform/logging/index.ts +4 -4
  168. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +99 -0
  169. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +38 -0
  170. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +9 -0
  171. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +7 -0
  172. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +7 -0
  173. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +23 -10
  174. package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +2 -0
  175. package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +15 -0
  176. package/templates/be/tool-config/eslint.config.mjs +3 -0
  177. package/templates/be/tool-config/jest.config.js +1 -0
  178. package/templates/be/tool-config/prettierignore +8 -0
  179. package/templates/be/tool-config/prettierrc +1 -0
  180. package/templates/be/tool-config/src/tests/tsconfig.json +5 -0
  181. package/templates/be/tool-config/tsconfig.build.json +5 -0
  182. package/templates/be/tool-config/tsconfig.json +11 -0
  183. package/templates/common/gitignore.base +1 -1
  184. package/templates/fe/ci-workflows/github/workflows/ci.yml +54 -0
  185. package/templates/fe/hooks/husky/pre-commit +16 -0
  186. package/templates/fe/hooks/husky/pre-push +6 -0
  187. package/templates/fe/package-scripts/package.json +17 -0
  188. package/templates/fe/parts/api-client.ts +44 -0
  189. package/templates/fe/parts/api-outcome.ts +7 -0
  190. package/templates/fe/quality-config/sonar-project.properties +8 -0
  191. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +1 -1
  192. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +1 -1
  193. package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +1 -1
  194. package/templates/fe/skeleton/scripts/.gitkeep +0 -0
  195. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +1 -0
  196. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +3 -0
  197. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +1 -0
  198. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +4 -0
  199. package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +12 -0
  200. package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +2 -0
  201. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +9 -0
  202. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +5 -0
  203. package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +5 -0
  204. package/templates/fe/skeleton-shared/packages/__family__-api/package.json +12 -0
  205. package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +1 -0
  206. package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +3 -0
  207. package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +1 -0
  208. package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +5 -0
  209. package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +18 -0
  210. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +19 -0
  211. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +2 -0
  212. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +12 -0
  213. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +15 -0
  214. package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +5 -0
  215. package/templates/fe/tool-config/eslint.config.mjs +3 -0
  216. package/templates/fe/tool-config/prettierignore +10 -0
  217. package/templates/fe/tool-config/prettierrc +1 -0
  218. package/templates/fe/tool-config/stylelint.config.mjs +3 -0
  219. package/templates/fe/tool-config/tsconfig.json +4 -0
  220. package/templates/be/pre-commit +0 -8
  221. package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +0 -31
  222. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +0 -13
  223. package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +0 -36
  224. package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +0 -19
  225. package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +0 -15
  226. package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +0 -38
  227. package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +0 -33
  228. package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +0 -35
  229. package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +0 -9
  230. package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +0 -19
  231. package/templates/common/codecov.yml +0 -13
  232. package/templates/common/pre-push +0 -5
  233. package/templates/fe/e2e.yml +0 -22
  234. package/templates/fe/pre-commit +0 -7
  235. package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +0 -10
  236. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +0 -27
  237. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +0 -10
  238. package/templates/fe/sonar-project.properties +0 -11
  239. /package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts} +0 -0
  240. /package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts} +0 -0
  241. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/next.config.ts +0 -0
  242. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/config.ts +0 -0
  243. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/navigation.ts +0 -0
  244. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/request.ts +0 -0
  245. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/routing.ts +0 -0
  246. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/proxy.ts +0 -0
@@ -1,5 +1,5 @@
1
- export { createJsonLogger } from "./json-logger"
2
- export { LogId } from "./log-id"
3
- export { Logger } from "./logger.port"
4
- export type { LogPayload } from "./logger.port"
1
+ export { createJsonLogger } from "./json-logger.service"
2
+ export { InjectLogger } from "./logging.decorators"
3
+ export { LoggingLogEvent } from "./logging.log-events"
5
4
  export { LoggingModule } from "./logging.module"
5
+ export type { Logger } from "./logging.port"
@@ -0,0 +1,99 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { FakeClock } from "@starci/jest-preset"
3
+ import { CLOCK } from "@modules/platform/clock"
4
+ import { JsonLoggerService, createJsonLogger } from "./json-logger.service"
5
+
6
+ const TIME = "2026-01-01T00:00:00.000Z"
7
+
8
+ const build = async () => {
9
+ const clock = new FakeClock(TIME)
10
+ const moduleRef = await Test.createTestingModule({
11
+ providers: [JsonLoggerService, { provide: CLOCK, useValue: clock }],
12
+ }).compile()
13
+ return { service: moduleRef.get(JsonLoggerService), clock }
14
+ }
15
+
16
+ /** One JSON line, the way the logger writes it. */
17
+ const line = (fields: Record<string, unknown>): string => `${JSON.stringify(fields)}\n`
18
+
19
+ describe("JsonLoggerService", () => {
20
+ const stdout: Array<string> = []
21
+ const stderr: Array<string> = []
22
+
23
+ beforeEach(() => {
24
+ stdout.length = 0
25
+ stderr.length = 0
26
+ jest.spyOn(process.stdout, "write").mockImplementation((chunk) => {
27
+ stdout.push(String(chunk))
28
+ return true
29
+ })
30
+ jest.spyOn(process.stderr, "write").mockImplementation((chunk) => {
31
+ stderr.push(String(chunk))
32
+ return true
33
+ })
34
+ })
35
+
36
+ afterEach(() => {
37
+ jest.restoreAllMocks()
38
+ })
39
+
40
+ describe("info", () => {
41
+ it("stamps the line with the clock and writes it to stdout", async () => {
42
+ const { service } = await build()
43
+
44
+ service.info("thing.happened", { id: 1 })
45
+
46
+ expect(stdout).toEqual([line({ level: "info", event: "thing.happened", time: TIME, id: 1 })])
47
+ expect(stderr).toEqual([])
48
+ })
49
+ })
50
+
51
+ describe("warn", () => {
52
+ it("writes the line to stderr, stamped with the moment of the call", async () => {
53
+ const { service, clock } = await build()
54
+ clock.advance(5_000)
55
+
56
+ service.warn("thing.slow")
57
+
58
+ expect(stderr).toEqual([line({ level: "warn", event: "thing.slow", time: "2026-01-01T00:00:05.000Z" })])
59
+ expect(stdout).toEqual([])
60
+ })
61
+ })
62
+
63
+ describe("error", () => {
64
+ it("serializes an Error cause by name and message", async () => {
65
+ const { service } = await build()
66
+
67
+ service.error("thing.failed", new TypeError("boom"), { id: 2 })
68
+
69
+ expect(stderr).toEqual([
70
+ line({
71
+ level: "error",
72
+ event: "thing.failed",
73
+ time: TIME,
74
+ errorName: "TypeError",
75
+ errorMessage: "boom",
76
+ id: 2,
77
+ }),
78
+ ])
79
+ })
80
+
81
+ it("serializes a cause that is not an Error to its text", async () => {
82
+ const { service } = await build()
83
+
84
+ service.error("thing.failed", "plain text")
85
+
86
+ expect(stderr).toEqual([
87
+ line({ level: "error", event: "thing.failed", time: TIME, errorMessage: "plain text" }),
88
+ ])
89
+ })
90
+ })
91
+
92
+ describe("createJsonLogger", () => {
93
+ it("builds a logger stamped by the given clock", () => {
94
+ createJsonLogger(new FakeClock(TIME)).info("thing.started")
95
+
96
+ expect(stdout).toEqual([line({ level: "info", event: "thing.started", time: TIME })])
97
+ })
98
+ })
99
+ })
@@ -0,0 +1,38 @@
1
+ import { Injectable } from "@nestjs/common"
2
+ import { InjectClock } from "@modules/platform/clock"
3
+ import type { Clock } from "@modules/platform/clock"
4
+ import type { LogFields, Logger } from "./logging.port"
5
+
6
+ type Level = "info" | "warn" | "error"
7
+
8
+ @Injectable()
9
+ /** The default adapter: one JSON object per line, stamped by the Clock; info goes to stdout, warn and error to stderr. */
10
+ export class JsonLoggerService implements Logger {
11
+ constructor(@InjectClock() private readonly clock: Clock) {}
12
+
13
+ /** Writes an info line. */
14
+ info(event: string, fields?: LogFields): void {
15
+ this.write("info", process.stdout, event, fields)
16
+ }
17
+
18
+ /** Writes a warn line. */
19
+ warn(event: string, fields?: LogFields): void {
20
+ this.write("warn", process.stderr, event, fields)
21
+ }
22
+
23
+ /** Writes an error line with the cause serialized by name and message. */
24
+ error(event: string, cause: unknown, fields?: LogFields): void {
25
+ const detail =
26
+ cause instanceof Error
27
+ ? { errorName: cause.name, errorMessage: cause.message }
28
+ : { errorMessage: String(cause) }
29
+ this.write("error", process.stderr, event, { ...detail, ...fields })
30
+ }
31
+
32
+ private write(level: Level, sink: NodeJS.WriteStream, event: string, fields?: LogFields): void {
33
+ sink.write(`${JSON.stringify({ level, event, time: this.clock.now().toISOString(), ...fields })}\n`)
34
+ }
35
+ }
36
+
37
+ /** Builds the JSON logger stamping lines with `clock`; main.ts uses it before the DI container exists. */
38
+ export const createJsonLogger = (clock: Clock): Logger => new JsonLoggerService(clock)
@@ -0,0 +1,9 @@
1
+ import { injector } from "@modules/platform/composition"
2
+ import type { TypedParameterDecorator } from "@modules/platform/composition"
3
+ import type { Logger } from "./logging.port"
4
+
5
+ /** Token of the Logger port. */
6
+ export const LOGGER: unique symbol = Symbol("platform.logging.logger")
7
+
8
+ /** Injects the Logger port. Parameter type: Logger. */
9
+ export const InjectLogger = (): TypedParameterDecorator<Logger> => injector<Logger>(LOGGER)
@@ -0,0 +1,7 @@
1
+ /** Log events of the process lifecycle: the entry points (main.ts of each app) log these. */
2
+ export enum LoggingLogEvent {
3
+ /** The HTTP listener is bound and the service answers requests. */
4
+ ServerStarted = "server.started",
5
+ /** The service failed before it could serve; the process exits non-zero. */
6
+ StartupFailed = "server.startup_failed",
7
+ }
@@ -0,0 +1,7 @@
1
+ import { ConfigurableModuleBuilder } from "@nestjs/common"
2
+ import type { LoggingOptions } from "./logging.options"
3
+
4
+ /** The configurable-module base of the logging capability; `isGlobal` is decided by the app root. */
5
+ export const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder<LoggingOptions>()
6
+ .setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
7
+ .build()
@@ -1,11 +1,24 @@
1
- import { Global, Module } from "@nestjs/common"
2
- import { createJsonLogger } from "./json-logger"
3
- import { Logger } from "./logger.port"
1
+ import { Module } from "@nestjs/common"
2
+ import type { DynamicModule } from "@nestjs/common"
3
+ import { CLOCK } from "@modules/platform/clock"
4
+ import type { Clock } from "@modules/platform/clock"
5
+ import { createJsonLogger } from "./json-logger.service"
6
+ import { LOGGER } from "./logging.decorators"
7
+ import { ConfigurableModuleClass, OPTIONS_TYPE } from "./logging.module-definition"
4
8
 
5
- /** Provides the logging port to every capability; one of the three platform modules that may be global. */
6
- @Global()
7
- @Module({
8
- providers: [{ provide: Logger, useFactory: (): Logger => createJsonLogger() }],
9
- exports: [Logger],
10
- })
11
- export class LoggingModule {}
9
+ @Module({})
10
+ /** Provides the Logger port as the JSON-lines adapter stamped by the Clock the app registers. */
11
+ export class LoggingModule extends ConfigurableModuleClass {
12
+ /** Registers the capability once per app. */
13
+ static register(options: typeof OPTIONS_TYPE): DynamicModule {
14
+ const base = super.register(options)
15
+ return {
16
+ ...base,
17
+ providers: [
18
+ ...(base.providers ?? []),
19
+ { provide: LOGGER, inject: [CLOCK], useFactory: (clock: Clock) => createJsonLogger(clock) },
20
+ ],
21
+ exports: [LOGGER],
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,2 @@
1
+ /** The logging capability takes no options; the empty record keeps `isGlobal` assignable through the configurable module. */
2
+ export type LoggingOptions = Record<never, never>
@@ -0,0 +1,15 @@
1
+ /** The structured data of one log line; values are plain data, never a request, a token or a secret. */
2
+ export interface LogFields {
3
+ /** The value logged under `name`. */
4
+ readonly [name: string]: unknown
5
+ }
6
+
7
+ /** The logging port: owners log through it with an enum member of their own `<owner>.log-events.ts`, never `console`. */
8
+ export interface Logger {
9
+ /** A normal event worth keeping. */
10
+ info(event: string, fields?: LogFields): void
11
+ /** Something unexpected the service recovered from. */
12
+ warn(event: string, fields?: LogFields): void
13
+ /** A failure: the cause is serialized by name and message, never dumped whole. */
14
+ error(event: string, cause: unknown, fields?: LogFields): void
15
+ }
@@ -0,0 +1,3 @@
1
+ import { loadHfs, starciBeConfig } from "@starci/eslint-canon-be"
2
+
3
+ export default starciBeConfig({ hfs: loadHfs(import.meta.url) })
@@ -0,0 +1 @@
1
+ module.exports = require("@starci/jest-preset").starciJestConfig()
@@ -0,0 +1,8 @@
1
+ {{header}}
2
+ dist/
3
+ coverage/
4
+ node_modules/
5
+ package-lock.json
6
+ contracts/
7
+ .starcistacks/
8
+ .starciwork/
@@ -0,0 +1 @@
1
+ "@starci/prettier-config"
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": ["../../tsconfig.json", "@starci/tsconfig/e2e.json"],
3
+ "include": ["./**/*.ts"],
4
+ "exclude": []
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": ["./tsconfig.json", "@starci/tsconfig/build.json"],
3
+ "compilerOptions": { "outDir": "./dist" },
4
+ "exclude": ["node_modules", "dist", "**/*.spec.ts", "src/tests"]
5
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "@starci/tsconfig/be.json",
3
+ "compilerOptions": {
4
+ "paths": {
5
+ "@features/*": ["./src/features/*"],
6
+ "@modules/*": ["./src/modules/*"],
7
+ "@tests/*": ["./src/tests/*"]
8
+ }
9
+ },
10
+ "exclude": ["node_modules", "dist", "src/tests/world", "src/tests/integration", "src/tests/e2e", "src/tests/contract"]
11
+ }
@@ -2,8 +2,8 @@
2
2
  node_modules/
3
3
  dist/
4
4
  coverage/
5
+ reports/
5
6
  test-results/
6
- playwright-report/
7
7
  *.tsbuildinfo
8
8
  .scannerwork/
9
9
  .turbo/
@@ -0,0 +1,54 @@
1
+ {{header}}
2
+ name: ci
3
+
4
+ on:
5
+ push:
6
+ branches: [main]
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ ci:
14
+ runs-on: ubuntu-latest
15
+ env:
16
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-node@v4
20
+ with:
21
+ node-version: {{nodeMajor}}
22
+ cache: npm
23
+ - run: npm ci
24
+ - name: hfs check
25
+ run: npm run hfs:report
26
+ - name: lint
27
+ run: npm run lint:check
28
+ - name: eslint report
29
+ if: ${{ !cancelled() }}
30
+ run: npm run lint:report
31
+ - name: eslint sonar import
32
+ if: ${{ !cancelled() }}
33
+ run: npx hfs report eslint reports/eslint.json reports/eslint.sonar.json
34
+ - name: stylelint report
35
+ if: ${{ !cancelled() }}
36
+ run: npm run lint:report:css
37
+ - name: stylelint sonar import
38
+ if: ${{ !cancelled() }}
39
+ run: npx hfs report stylelint reports/stylelint.json reports/stylelint.sonar.json
40
+ - name: format
41
+ run: npm run format:check
42
+ - name: typecheck
43
+ run: npm run typecheck
44
+ - name: build
45
+ run: npm run build
46
+ - uses: SonarSource/sonarqube-scan-action@v7
47
+ if: ${{ !cancelled() && env.SONAR_TOKEN != '' }}
48
+ env:
49
+ SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
50
+ - uses: SonarSource/sonarqube-quality-gate-action@v1
51
+ if: ${{ !cancelled() && env.SONAR_TOKEN != '' }}
52
+ timeout-minutes: 10
53
+ env:
54
+ SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
@@ -0,0 +1,16 @@
1
+ {{header}}
2
+ # Commit gate: the secrets guard, then types, lint and format of the staged files.
3
+ npx hfs work-hygiene
4
+ npm run typecheck
5
+ sources=$(git diff --cached --name-only --diff-filter=ACMR -- '*.ts' '*.tsx' '*.mts' '*.cts' '*.js' '*.jsx' '*.mjs' '*.cjs' || true)
6
+ if [ -n "$sources" ]; then
7
+ npx eslint --max-warnings=0 --no-warn-ignored $sources
8
+ fi
9
+ styles=$(git diff --cached --name-only --diff-filter=ACMR -- '*.css' || true)
10
+ if [ -n "$styles" ]; then
11
+ npx stylelint $styles
12
+ fi
13
+ formatted=$(git diff --cached --name-only --diff-filter=ACMR || true)
14
+ if [ -n "$formatted" ]; then
15
+ npx prettier --check --ignore-unknown $formatted
16
+ fi
@@ -0,0 +1,6 @@
1
+ {{header}}
2
+ # Push gate: types, lint (eslint over the repository, stylelint over the CSS), format, the architecture check on the owners this push touches.
3
+ npm run typecheck
4
+ npm run lint:check
5
+ npm run format:check
6
+ npm run hfs:check -- --fast
@@ -0,0 +1,17 @@
1
+ {
2
+ "scripts": {
3
+ "prepare": "husky",
4
+ {{appScripts}}
5
+ "codegen": "npm run codegen --workspaces --if-present",
6
+ "build": "npm run build --workspaces --if-present",
7
+ "typecheck": "npm run typecheck --workspaces --if-present",
8
+ "lint": "npm run codegen --silent && eslint . --fix --max-warnings=0 && stylelint \"{{styleGlob}}\" --fix",
9
+ "lint:check": "npm run codegen --silent && eslint . --max-warnings=0 && stylelint \"{{styleGlob}}\"",
10
+ "lint:report": "eslint . --format json --output-file reports/eslint.json",
11
+ "lint:report:css": "stylelint \"{{styleGlob}}\" --formatter json --output-file reports/stylelint.json",
12
+ "format": "prettier --write .",
13
+ "format:check": "prettier --check .",
14
+ "hfs:check": "hfs check",
15
+ "hfs:report": "hfs check --sonar reports/hfs.sonar.json"
16
+ }
17
+ }
@@ -0,0 +1,44 @@
1
+ import type { Outcome } from "./outcome"
2
+
3
+ /** How long a request waits before it is abandoned and answered `unavailable`. */
4
+ const REQUEST_TIMEOUT_MS = 8000
5
+
6
+ /** One request of the client: the caller's own signal is joined with the timeout. */
7
+ export interface ClientRequest {
8
+ readonly url: string
9
+ readonly method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
10
+ readonly body?: unknown
11
+ readonly signal?: AbortSignal
12
+ }
13
+
14
+ /** The status of a response as an Outcome kind; 401 and 403 are `refused`, nothing collapses into `null`. */
15
+ const toOutcome = async (response: Response): Promise<Outcome<unknown>> => {
16
+ if (response.status === 401 || response.status === 403) return { kind: "refused" }
17
+ if (response.status === 404) return { kind: "not-found" }
18
+ if (response.status === 400 || response.status === 422) return { kind: "invalid" }
19
+ if (!response.ok) return { kind: "unavailable" }
20
+ try {
21
+ return { kind: "ok", data: (await response.json()) as unknown }
22
+ } catch {
23
+ return { kind: "unavailable" }
24
+ }
25
+ }
26
+
27
+ /**
28
+ * The repository's one fetch. It always carries a timeout signal, never throws, and answers an Outcome; the body is
29
+ * `unknown` until a generated wire type narrows it.
30
+ */
31
+ export const request = async ({ url, method = "GET", body, signal }: ClientRequest): Promise<Outcome<unknown>> => {
32
+ const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS)
33
+ try {
34
+ const response = await fetch(url, {
35
+ method,
36
+ headers: { accept: "application/json", ...(body === undefined ? {} : { "content-type": "application/json" }) },
37
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
38
+ signal: signal === undefined ? timeout : AbortSignal.any([signal, timeout]),
39
+ })
40
+ return await toOutcome(response)
41
+ } catch {
42
+ return { kind: "unavailable" }
43
+ }
44
+ }
@@ -0,0 +1,7 @@
1
+ /** The one result every read of the backend returns: a status is a kind, never a thrown error and never `null`. */
2
+ export type Outcome<T> =
3
+ | { readonly kind: "ok"; readonly data: T }
4
+ | { readonly kind: "refused" }
5
+ | { readonly kind: "invalid" }
6
+ | { readonly kind: "not-found" }
7
+ | { readonly kind: "unavailable" }
@@ -0,0 +1,8 @@
1
+ {{header}}
2
+ sonar.projectKey={{sonarKey}}
3
+ sonar.sourceEncoding=UTF-8
4
+ sonar.sources={{sonarRoots}}
5
+ sonar.exclusions=**/.next/**,**/node_modules/**,**/src/messages/**
6
+ sonar.typescript.tsconfigPaths={{tsconfigPaths}}
7
+ sonar.externalIssuesReportPaths=reports/hfs.sonar.json,reports/eslint.sonar.json,reports/stylelint.sonar.json
8
+ sonar.nodejs.maxspace=8192
@@ -2,7 +2,7 @@ import { hasLocale, NextIntlClientProvider } from "next-intl"
2
2
  import { getMessages, setRequestLocale } from "next-intl/server"
3
3
  import { notFound } from "next/navigation"
4
4
  import type { ReactNode } from "react"
5
- import { routing } from "../../modules/i18n/routing"
5
+ import { routing } from "../../modules/i18n"
6
6
  import "../globals.css"
7
7
 
8
8
  interface LocaleLayoutProps {
@@ -1,5 +1,5 @@
1
1
  import { getTranslations } from "next-intl/server"
2
- import { Link } from "../../modules/i18n/navigation"
2
+ import { Link } from "../../modules/i18n"
3
3
 
4
4
  /** Shown when a route calls `notFound()`. */
5
5
  const NotFound = async () => {
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import { DEFAULT_LOCALE } from "../modules/i18n/config"
3
+ import { DEFAULT_LOCALE } from "../modules/i18n"
4
4
  import messages from "../modules/i18n/messages/vi.json"
5
5
 
6
6
  interface GlobalErrorProps {
File without changes
@@ -0,0 +1 @@
1
+ {{> fe/parts/api-client.ts}}
@@ -0,0 +1,3 @@
1
+ export { request } from "./client"
2
+ export type { ClientRequest } from "./client"
3
+ export type { Outcome } from "./outcome"
@@ -0,0 +1 @@
1
+ {{> fe/parts/api-outcome.ts}}
@@ -0,0 +1,4 @@
1
+ export { DEFAULT_LOCALE, LOCALES } from "./config"
2
+ export type { Locale } from "./config"
3
+ export { getPathname, Link, redirect, usePathname, useRouter } from "./navigation"
4
+ export { routing } from "./routing"
@@ -0,0 +1,12 @@
1
+ import type { NextConfig } from "next"
2
+ import createNextIntlPlugin from "next-intl/plugin"
3
+
4
+ const withNextIntl = createNextIntlPlugin("./src/modules/i18n/request.ts")
5
+
6
+ /** Next config of the {{app}} app: next-intl wired to the request config, Turbopack rooted at the repository, the shared packages compiled from source. */
7
+ const nextConfig: NextConfig = {
8
+ turbopack: { root: process.cwd() },
9
+ transpilePackages: ["@{{family}}/i18n", "@{{family}}/api"],
10
+ }
11
+
12
+ export default withNextIntl(nextConfig)
@@ -0,0 +1,2 @@
1
+ export { request } from "@{{family}}/api"
2
+ export type { ClientRequest, Outcome } from "@{{family}}/api"
@@ -0,0 +1,9 @@
1
+ import { createAppI18n } from "@{{family}}/i18n"
2
+
3
+ /** This app's locales and the next-intl stack (routing, navigation) built from them by the shared i18n package. */
4
+ const i18n = createAppI18n({ locales: ["vi"], defaultLocale: "vi" })
5
+
6
+ export const { DEFAULT_LOCALE, LOCALES, getPathname, Link, redirect, routing, usePathname, useRouter } = i18n
7
+
8
+ /** A served locale. */
9
+ export type Locale = (typeof LOCALES)[number]
@@ -0,0 +1,5 @@
1
+ import { createRequestConfig } from "@{{family}}/i18n/request"
2
+ import { routing } from "./index"
3
+
4
+ /** Loads the catalog of the requested locale from this app's `messages/`; the shared package resolves the locale. */
5
+ export default createRequestConfig(routing, async (locale) => (await import(`./messages/${locale}.json`)).default)
@@ -0,0 +1,5 @@
1
+ import { createProxy } from "@{{family}}/i18n/proxy"
2
+ import { routing } from "./modules/i18n"
3
+
4
+ /** Negotiates the locale and redirects; the default locale (vi) is served without a prefix. */
5
+ export default createProxy(routing)
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@{{family}}/api",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "typecheck": "tsc --noEmit --pretty false"
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ {{> fe/parts/api-client.ts}}
@@ -0,0 +1,3 @@
1
+ export { request } from "./client"
2
+ export type { ClientRequest } from "./client"
3
+ export type { Outcome } from "./outcome"
@@ -0,0 +1 @@
1
+ {{> fe/parts/api-outcome.ts}}
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "@starci/tsconfig/next.json",
3
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
4
+ "exclude": ["node_modules"]
5
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@{{family}}/i18n",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./proxy": "./src/proxy.ts",
9
+ "./request": "./src/request.ts"
10
+ },
11
+ "scripts": {
12
+ "typecheck": "tsc --noEmit --pretty false"
13
+ },
14
+ "peerDependencies": {
15
+ "next": ">=16",
16
+ "next-intl": ">=4"
17
+ }
18
+ }
@@ -0,0 +1,19 @@
1
+ import { createNavigation } from "next-intl/navigation"
2
+ import { defineRouting } from "next-intl/routing"
3
+
4
+ /** The locales an app serves and the one it serves at the unprefixed path. */
5
+ export interface AppI18nOptions<Locale extends string> {
6
+ readonly locales: readonly [Locale, ...Locale[]]
7
+ readonly defaultLocale: Locale
8
+ }
9
+
10
+ /**
11
+ * The next-intl stack of one app, written once for the repository: one routing table (default locale unprefixed) and the
12
+ * locale-aware navigation built on it. Components import `Link` and `redirect` from their app's i18n module, never from
13
+ * `next/link` or `next/navigation`. The proxy and the request config are the `./proxy` and `./request` entries, so a client
14
+ * bundle that imports this one carries no server code.
15
+ */
16
+ export const createAppI18n = <const Locale extends string>({ locales, defaultLocale }: AppI18nOptions<Locale>) => {
17
+ const routing = defineRouting({ locales, defaultLocale, localePrefix: "as-needed" })
18
+ return { LOCALES: locales, DEFAULT_LOCALE: defaultLocale, routing, ...createNavigation(routing) }
19
+ }
@@ -0,0 +1,2 @@
1
+ export { createAppI18n } from "./app"
2
+ export type { AppI18nOptions } from "./app"
@@ -0,0 +1,12 @@
1
+ import createMiddleware from "next-intl/middleware"
2
+ import type { defineRouting } from "next-intl/routing"
3
+ import { NextResponse, type NextRequest } from "next/server"
4
+
5
+ /** The API, the health probe, framework files and files with an extension are never locale-negotiated. */
6
+ const UNNEGOTIATED = /^\/(?:api|health|_next|_vercel)(?:\/|$)|\.[^/]+$/
7
+
8
+ /** The proxy of an app: negotiates the locale and redirects, and lets everything that is not a page through untouched. */
9
+ export const createProxy = (routing: ReturnType<typeof defineRouting>) => {
10
+ const negotiate = createMiddleware(routing)
11
+ return (request: NextRequest) => (UNNEGOTIATED.test(request.nextUrl.pathname) ? NextResponse.next() : negotiate(request))
12
+ }