@starci/hfs 4.7.37 → 4.7.38

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 (26) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/package.json +1 -1
  3. package/runtime/knowledge/grammars/index.yaml +1 -1
  4. package/runtime/knowledge/hfs/canon-pins.yaml +13 -8
  5. package/runtime/knowledge/hfs/fe-stack.yaml +1 -1
  6. package/scaffold/app.mjs +5 -5
  7. package/scaffold/lite-exports.mjs +1 -1
  8. package/templates/app/skeleton/.starcistacks/production/README.md +2 -2
  9. package/templates/be/skeleton/apps/__app__/src/main.ts +7 -2
  10. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +1 -1
  11. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
  12. package/templates/be/skeleton/src/modules/platform/http/index.ts +1 -5
  13. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +3 -1
  14. package/templates/be/skeleton/src/modules/platform/logging/index.ts +2 -13
  15. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +18 -6
  16. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +0 -1
  17. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/component.tsx +1 -1
  18. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignOutButton/component.tsx +1 -1
  19. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +1 -1
  20. package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +5 -5
  21. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/component.tsx +1 -1
  22. package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/classNames.ts +2 -2
  23. package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/index.tsx +1 -1
  24. package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/classNames.ts +2 -2
  25. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +0 -92
  26. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +0 -52
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.7.38 - 2026-10-26
4
+ - Changed: the bundled canon pins select @starci/stylelint-canon 2.0.13, @starci/eslint-canon-be 3.4.46, @starci/eslint-canon-fe 9.1.1, the Grammar lint families 0.12.5 and @starci/grammar 0.12.5.
5
+ - Fixed: the full and lite scaffold templates, skeletons and specs follow the scaffold end-to-end and repaint fixes made after 4.7.37.
6
+
3
7
  ## 4.7.37 - 2026-10-26
4
8
  - Fixed: the scaffold SiteShell declares viewport=flow so Grammar lint reads it as site chrome, the hygiene sync follows the scaffold end-to-end fixes, and the scaffold json-logger spec expects the logger field order.
5
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/hfs",
3
- "version": "4.7.37",
3
+ "version": "4.7.38",
4
4
  "description": "The StarCi app command implementation for scaffolding, linting, checking and synchronizing one product repository.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -77,7 +77,7 @@ families:
77
77
  package: '@starci/grammar'
78
78
  entry: '@starci/grammar/common'
79
79
  styleEntry: '@starci/grammar/common.css'
80
- peers: [react, '@heroui/react', '@xyflow/react']
80
+ peers: [react, '@heroui/react', '@xyflow/react', react-aria-components]
81
81
  packageSnapshot: common/DNA.yaml
82
82
  scope: |
83
83
  DNA snapshot only. This catalog carries no Common-side family boundary, idiom set or composition
@@ -23,42 +23,42 @@ pins:
23
23
  source: packages/heroicons/package.json
24
24
  why: 'StarCi custom Heroicons-compatible cuts; eslint-grammar-starci (icon.mjs) admits @starci/heroicons/24/outline and /16/solid as icon sources, so an app that uses them gets this exact version.'
25
25
  '@starci/grammar':
26
- version: 0.12.4
26
+ version: 0.12.5
27
27
  group: starci
28
28
  install: registry
29
29
  side: fe
30
30
  source: packages/grammar/package.json
31
31
  why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
32
32
  '@starci/eslint-canon-be':
33
- version: 3.4.45
33
+ version: 3.4.46
34
34
  group: starci
35
35
  install: registry
36
36
  side: be
37
37
  source: packages/eslint/be/package.json
38
38
  why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
39
39
  '@starci/eslint-canon-fe':
40
- version: 9.1.0
40
+ version: 9.1.1
41
41
  group: starci
42
42
  install: registry
43
43
  side: fe
44
44
  source: packages/eslint/fe/package.json
45
45
  why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
46
46
  '@starci/eslint-grammar-starci':
47
- version: 0.12.4
47
+ version: 0.12.5
48
48
  group: starci
49
49
  install: registry
50
50
  side: fe
51
51
  source: packages/eslint/grammar-starci/package.json
52
52
  why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
53
53
  '@starci/eslint-grammar-offset-pop':
54
- version: 0.12.4
54
+ version: 0.12.5
55
55
  group: starci
56
56
  install: registry
57
57
  side: fe
58
58
  source: packages/eslint/grammar-offset-pop/package.json
59
59
  why: 'the Offset Pop Grammar component API lint, versioned with @starci/grammar; its shared component contract comes from the StarCi family lint without copying rules.'
60
60
  '@starci/stylelint-canon':
61
- version: 2.0.12
61
+ version: 2.0.13
62
62
  group: starci
63
63
  install: registry
64
64
  side: fe
@@ -91,14 +91,14 @@ pins:
91
91
  source: packages/test-world/package.json
92
92
  why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
93
93
  '@starci/cli':
94
- version: 1.7.37
94
+ version: 1.7.38
95
95
  group: starci
96
96
  install: registry
97
97
  side: both
98
98
  source: packages/cli/package.json
99
99
  why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
100
100
  '@starci/hfs':
101
- version: 4.7.37
101
+ version: 4.7.38
102
102
  group: starci
103
103
  install: registry
104
104
  side: both
@@ -299,6 +299,11 @@ pins:
299
299
  group: ui
300
300
  side: fe
301
301
  why: 'Grammar FlowGraph imports React Flow from @xyflow/react; every Grammar consumer declares this required peer at the version used by the Grammar package.'
302
+ react-aria-components:
303
+ version: 1.21.1
304
+ group: ui
305
+ side: fe
306
+ why: 'Grammar imports React Aria Components directly (Sidebar, Field); every Grammar consumer declares this required peer at the version HeroUI resolves.'
302
307
  '@heroui/react':
303
308
  version: 3.2.6
304
309
  group: ui
@@ -99,7 +99,7 @@ concerns:
99
99
  why: useTranslations for copy and the next-intl formatter for numbers, fixed dates and currency; FE-DATE owns live time display.
100
100
  - id: ui
101
101
  tier: core
102
- packages: ['@heroui/react', '@heroui/styles', tailwindcss, '@starci/grammar']
102
+ packages: ['@heroui/react', '@heroui/styles', tailwindcss, '@starci/grammar', react-aria-components]
103
103
  home: [fe.components, fe.feature]
104
104
  pattern: FE-UI
105
105
  forbids: ['@mantine/core', '@mui/material', '@chakra-ui/react', antd, styled-components, '@emotion/react', '@emotion/styled']
package/scaffold/app.mjs CHANGED
@@ -199,7 +199,7 @@ const LITE_STARTER_DEPENDENCIES = Object.freeze({
199
199
  */
200
200
  const FE_APP_DEPENDENCIES = Object.freeze({
201
201
  '@<project>/i18n': '*', '@<project>/ui': '*',
202
- '@heroui/react': null, '@heroui/styles': null, '@starci/grammar': null, '@xyflow/react': null, next: null, 'next-intl': null, react: null, 'react-dom': null,
202
+ '@heroui/react': null, '@heroui/styles': null, '@starci/grammar': null, '@xyflow/react': null, 'react-aria-components': null, next: null, 'next-intl': null, react: null, 'react-dom': null,
203
203
  });
204
204
 
205
205
  /**
@@ -207,7 +207,7 @@ const FE_APP_DEPENDENCIES = Object.freeze({
207
207
  * the front end is the Auth client of modules/auth only) and server-only for its own next-intl request config.
208
208
  */
209
209
  const LITE_FE_APP_DEPENDENCIES = Object.freeze({
210
- '@heroicons/react': null, '@heroui/react': null, '@heroui/styles': null, '@starci/grammar': null, '@starci/heroicons': null, '@xyflow/react': null,
210
+ '@heroicons/react': null, '@heroui/react': null, '@heroui/styles': null, '@starci/grammar': null, '@starci/heroicons': null, '@xyflow/react': null, 'react-aria-components': null,
211
211
  '@supabase/supabase-js': null, next: null, 'next-intl': null, react: null, 'react-dom': null, 'server-only': null,
212
212
  });
213
213
 
@@ -218,7 +218,7 @@ const LITE_FE_APP_DEPENDENCIES = Object.freeze({
218
218
  * ./routing, client-safe: the default locale a client boundary reads).
219
219
  */
220
220
  const FE_PACKAGES = Object.freeze({
221
- ui: { exports: ['.'], dependencies: { '@heroui/react': null, '@starci/grammar': null, '@starci/heroicons': null, '@xyflow/react': null, 'next-intl': null, react: null, 'react-dom': null } },
221
+ ui: { exports: ['.'], dependencies: { '@heroui/react': null, '@starci/grammar': null, '@starci/heroicons': null, '@xyflow/react': null, 'react-aria-components': null, 'next-intl': null, react: null, 'react-dom': null } },
222
222
  i18n: { exports: ['.', './proxy', './routing'], dependencies: { next: null, 'next-intl': null, 'server-only': null } },
223
223
  });
224
224
 
@@ -304,11 +304,11 @@ export function peerDependencies(pkg, pins, pairs = parseYaml(fs.readFileSync(PE
304
304
  /**
305
305
  * The fe-stack core packages one version of which the whole install resolves (npm `overrides`, at their canon pins): a
306
306
  * transitive dependency otherwise brings its own (zustand 4 under @xyflow/react, dayjs under typeorm, a hoisted newer zod
307
- * that breaks the zodResolver types), and @xyflow/react itself stays the grammar's one version. Two are security overrides, each dropped when
307
+ * that breaks the zodResolver types), and @xyflow/react and react-aria-components stay the grammar's one version. Two are security overrides, each dropped when
308
308
  * upstream ships the fix (canon-pins.yaml says why): @graphql-tools/utils (@nestjs/graphql 13.x pulls <=12.0.0, GHSA-7mx3-vvmw-hjmv) and multer
309
309
  * (@nestjs/platform-express depends on exactly 2.4.0, GHSA-q7qp-rjg7-q26m).
310
310
  */
311
- const ROOT_OVERRIDES = Object.freeze(['@graphql-tools/utils', '@xyflow/react', 'dayjs', 'multer', 'zod', 'zustand']);
311
+ const ROOT_OVERRIDES = Object.freeze(['@graphql-tools/utils', '@xyflow/react', 'dayjs', 'multer', 'react-aria-components', 'zod', 'zustand']);
312
312
 
313
313
  /**
314
314
  * The root package.json of a new app, the monorepo root: name, workspaces, npm, its edition's dependencies with their runtime
@@ -50,7 +50,7 @@ const REGISTRATIONS = Object.freeze({
50
50
  {
51
51
  file: "be/src/modules/platform/database/index.ts",
52
52
  source: "./database.sql",
53
- names: ["sql"],
53
+ names: ["BATCH_ROWS", "sql"],
54
54
  beforeSource: "./errors/database.error",
55
55
  },
56
56
  {
@@ -1,6 +1,6 @@
1
1
  # Production stack
2
2
 
3
- The app and its dependencies run on Docker Swarm; Supabase, when declared, is a hosted service. The tracked model declares the application services, one digest-pinned Caddy ingress and one digest-pinned PostgreSQL service on an internal backing network, with its data on a named persistent volume and credentials supplied through mounted secrets. On the first boot of an empty data volume, PostgreSQL runs the mounted `postgres-init.sh` config to create the `app_be` role with access to this app's database and schema, without superuser, database-creation, role-creation, replication or RLS-bypass privileges. Existing data volumes skip initialization and retain their roles and credentials.
3
+ The app and its dependencies run on Docker Swarm; Supabase, when declared, is a hosted service. The tracked model declares the application services, one digest-pinned Caddy ingress and one digest-pinned PostgreSQL service on an internal backing network, with its data on a named persistent volume and credentials supplied through mounted secrets. On the first boot of an empty data volume, PostgreSQL runs the mounted `.starcistacks/production/infra/swarm/postgres-init.sh` config to create the `app_be` role with access to this app's database and schema, without superuser, database-creation, role-creation, replication or RLS-bypass privileges. Existing data volumes skip initialization and retain their roles and credentials.
4
4
  The model is a deployment declaration. It contains no owner credentials, real DNS receipt, completed release or restore proof. The public records under `.starcistacks/production/infra/swarm/` mark those owner-run operations as pending. Its all-zero application image digests are illustrative and must be replaced before deployment.
5
5
 
6
6
  The runtime template tracks no encrypted members. Scaffolding generates the declared database connection, application password and PostgreSQL administrator password members with placeholder values encrypted to this app's own recipient in `.sops.yaml`. Replace them with `starci app secret set production` using the matching host age identity in untracked local custody. The sealed connection uses role `app_be`, database `{{project}}` and internal endpoint `postgres:5432`; its URI-encoded password matches the separate `primary-db-password` member. The administrator password is distinct. The API receives only its least-privilege connection; only PostgreSQL receives the two password files for initialization. The non-empty application password contains no line breaks. The service has no TTY: initialization creates the role without a password, then feeds the two `psql` `\password` prompts through stdin using Bash's builtin `printf`, so psql encrypts it before sending SQL. No password travels in an initialization command argument or a plaintext SQL statement.
@@ -27,7 +27,7 @@ Every item is pending. Swarm initialization, provider URIs, DNS changes and key
27
27
  | DNS | Replace the reserved `example.invalid` hosts consistently in the manifest, Caddyfile and public configuration; publish address records for the product, API and landing hosts and retain a provider receipt in `.starcistacks/production/infra/swarm/owner-actions.md`. |
28
28
  | TLS | Caddy issues certificates after real DNS resolves. Provision `{{project}}-production-ingress-data` and `{{project}}-production-ingress-config-data` on the selected node; retain an issuance and renewal receipt in `.starcistacks/production/infra/swarm/owner-actions.md`. |
29
29
  | Database | Prepare creates `{{project}}-production-postgres-data` on the declared stateful node; the normal up path starts PostgreSQL and initializes `app_be` on its first empty-volume boot. Seal the matching role, database and password described above before up. Existing databases retain their current roles and credentials; initialization never rewrites them. Record the recovery owner without copying credentials here. |
30
- | Secret custody | Replace generated placeholders in `.starcistacks/production/secrets/primary-db-url.enc`, `primary-db-password.enc` and `postgres-password.enc` with the matching application connection/password and distinct administrator password; record their key names in the environment roster. The age identity stays only in the host's untracked `.claude/secret.env`. |
30
+ | Secret custody | Replace generated placeholders in `.starcistacks/production/secrets/primary-db-url.enc`, `.starcistacks/production/secrets/primary-db-password.enc` and `.starcistacks/production/secrets/postgres-password.enc` with the matching application connection/password and distinct administrator password; record their key names in the environment roster. The age identity stays only in the host's untracked `.claude/secret.env`. |
31
31
  | External secrets | Materialize the sealed members in owner custody and create `{{project}}-production-primary-db-url-v1`, `{{project}}-production-primary-db-password-v1` and `{{project}}-production-postgres-password-v1`. The API and PostgreSQL read their separately granted mounted files. A later credential rotation updates the actual database role under owner authority and rotates matching immutable secrets; changing a mounted file does not rerun first-boot initialization. |
32
32
  | Public build configuration | Build the front end with its declared public site and GraphQL URLs. Public browser values stay outside sealed secret custody. |
33
33
  | Immutable release | Replace the illustrative all-zero application digests in the manifest, model and `.starcistacks/production/infra/swarm/releases/images.lock.yaml` with the verified release digests. Store each rendered release under `/srv/{{project}}/releases/<digest>`. Retain the last accepted record in `.starcistacks/production/infra/swarm/releases/previous.yaml`, then move the pointer and update `.starcistacks/production/infra/swarm/releases/current.yaml` after verification. |
@@ -12,7 +12,10 @@ const bootstrap = async (): Promise<void> => {
12
12
  const env = EnvSource.fromProcess()
13
13
  const options = read{{appPascal}}Env(env)
14
14
  const logger = createJsonLogger(new SystemClockService(), new SystemIds())
15
- const app = await NestFactory.create(AppModule.register(options), { rawBody: true, logger: createNestLogger(logger) })
15
+ const app = await NestFactory.create(AppModule.register(options), {
16
+ rawBody: true,
17
+ logger: createNestLogger(logger),
18
+ })
16
19
  applyCorrelation(app)
17
20
  applyHttpSecurity(app, options.httpSecurity, { trustProxy: 1 })
18
21
  app.enableShutdownHooks()
@@ -24,6 +27,8 @@ const bootstrap = async (): Promise<void> => {
24
27
  }
25
28
 
26
29
  bootstrap().catch((error: unknown) => {
27
- createJsonLogger(new SystemClockService(), new SystemIds()).error(LoggingLogEvent.StartupFailed, error, { service: "{{app}}" })
30
+ createJsonLogger(new SystemClockService(), new SystemIds()).error(LoggingLogEvent.StartupFailed, error, {
31
+ service: "{{app}}",
32
+ })
28
33
  process.exitCode = 1
29
34
  })
@@ -4,7 +4,7 @@ import { ClockModule } from "@modules/platform/clock"
4
4
  import { DatabaseModule } from "@modules/platform/database"
5
5
  import type { DatabaseConnectionConfig, DatabaseConnectionOptions } from "@modules/platform/database"
6
6
  import { IdsModule } from "@modules/platform/ids"
7
- import { HTTP_ERROR_KINDS, HTTP_MESSAGES, HttpModule } from "@modules/platform/http"
7
+ import { HttpModule } from "@modules/platform/http"
8
8
  import { LoggingModule } from "@modules/platform/logging"
9
9
  import type { CliAppOptions } from "../cli.options"
10
10
 
@@ -5,7 +5,7 @@ export { MigrationRunnerService } from "./migration-runner.service"
5
5
  export { DEFAULT_SEED_ENV } from "./seed-connections.client"
6
6
  export { SeedRunnerService } from "./seed-runner.service"
7
7
  export type { DatabaseConnectionConfig, DatabaseConnectionOptions } from "./database.options"
8
- export { BATCH_ROWS, LIST_ROWS_MAX, sql } from "./database.sql"
8
+ export { LIST_ROWS_MAX, sql } from "./database.sql"
9
9
  export { DATABASE_ERROR_KINDS } from "./errors/database.error"
10
10
  export { parsePrimaryDatabaseConfig } from "./primary.config"
11
11
  export { InjectPrimaryEntityManager, PRIMARY_ENTITY_MANAGER } from "./primary.decorators"
@@ -1,7 +1,3 @@
1
- export { HTTP_ERROR_KINDS, HttpError, HttpErrorCode } from "./errors/http.error"
1
+ export { HTTP_ERROR_KINDS } from "./errors/http.error"
2
2
  export { HTTP_MESSAGES } from "./messages/http.messages"
3
- export { callGraphql } from "./graphql-call.mapper"
4
- export type { GraphqlAnswer } from "./graphql-call.mapper"
5
- export { HTTP_CLIENT, InjectHttpClient } from "./http.decorators"
6
3
  export { HttpModule } from "./http.module"
7
- export type { HttpClient } from "./http.port"
@@ -15,7 +15,9 @@ describe("CorrelationService", () => {
15
15
  expect(service.fromHeaders({ "x-request-id": "request-1" })).toBe("request-1")
16
16
  expect(service.fromHeaders({})).toBe("00000000-0000-4000-8000-000000000001")
17
17
  expect(service.create()).toBe("00000000-0000-4000-8000-000000000002")
18
- expect(service.run("explicit-id", () => stampLogFields({}, "root-id"))).toEqual({ correlationId: "explicit-id" })
18
+ expect(service.run("explicit-id", () => stampLogFields({}, "root-id"))).toEqual({
19
+ correlationId: "explicit-id",
20
+ })
19
21
  expect(createCorrelationRunner(ids).run("bootstrap-id", () => stampLogFields({}, "root-id"))).toEqual({
20
22
  correlationId: "bootstrap-id",
21
23
  })
@@ -1,17 +1,6 @@
1
1
  export { createJsonLogger } from "./json-logger.service"
2
- export {
3
- InjectLogger,
4
- LOGGER,
5
- CORRELATION_IDS,
6
- CORRELATION_RUNNER,
7
- InjectCorrelationIds,
8
- InjectCorrelationRunner,
9
- } from "./logging.decorators"
10
- export { applyCorrelation, correlationMiddleware } from "./correlation-request.policy"
11
- export type { CorrelationIds, CorrelationRunner } from "./correlation.port"
12
- export type { CorrelationHeaders, CorrelationRequest } from "./correlation.contracts"
13
- export { createCorrelationRunner } from "./correlation.service"
14
- export { isCorrelationId } from "./correlation.policy"
2
+ export { InjectLogger, LOGGER } from "./logging.decorators"
3
+ export { applyCorrelation } from "./correlation-request.policy"
15
4
  export { LoggingLogEvent } from "./logging.log-events"
16
5
  export { LoggingModule } from "./logging.module"
17
6
  export type { Logger } from "./logging.port"
@@ -1,12 +1,24 @@
1
+ import { Test } from "@nestjs/testing"
1
2
  import { isRecord } from "./record.policy"
2
3
 
4
+ const RECORD_POLICY: unique symbol = Symbol("primitives.record")
5
+
3
6
  describe("isRecord", () => {
7
+ let subject: typeof isRecord
8
+
9
+ beforeEach(async () => {
10
+ const moduleRef = await Test.createTestingModule({
11
+ providers: [{ provide: RECORD_POLICY, useFactory: () => isRecord }],
12
+ }).compile()
13
+ subject = moduleRef.get<typeof isRecord>(RECORD_POLICY)
14
+ })
15
+
4
16
  it("narrows non-null objects before provider fields are read", () => {
5
- expect(isRecord({ status: 200 })).toBe(true)
6
- expect(isRecord([])).toBe(true)
7
- expect(isRecord(null)).toBe(false)
8
- expect(isRecord(undefined)).toBe(false)
9
- expect(isRecord("response")).toBe(false)
10
- expect(isRecord(200)).toBe(false)
17
+ expect(subject({ status: 200 })).toBe(true)
18
+ expect(subject([])).toBe(true)
19
+ expect(subject(null)).toBe(false)
20
+ expect(subject(undefined)).toBe(false)
21
+ expect(subject("response")).toBe(false)
22
+ expect(subject(200)).toBe(false)
11
23
  })
12
24
  })
@@ -5,4 +5,3 @@ export type { DatabaseConnectionOptions } from "./database.options"
5
5
  export { PRIMARY_DATA_SOURCE } from "./database.port"
6
6
  export { DATABASE_ERROR_KINDS } from "./errors/database.error"
7
7
  export { parsePrimaryDatabaseConfig } from "./primary.config"
8
- export { BATCH_ROWS } from "./database.sql"
@@ -21,7 +21,7 @@ export const SignInFormBase = (props: SignInFormBaseProps) => (
21
21
  <Input id="sign-in-email" name="email" label={props.props.emailLabel} kind="email" isRequired />
22
22
  <Input id="sign-in-password" name="password" label={props.props.passwordLabel} kind="password" isRequired />
23
23
  {props.props.failure === undefined ? null : <Text live="assertive">{props.props.failure}</Text>}
24
- <Button type="submit" variant="primary" isPending={props.state === "working"}>
24
+ <Button type="submit" intent="commit" isPending={props.state === "working"}>
25
25
  {props.props.submitLabel}
26
26
  </Button>
27
27
  </Form>
@@ -14,7 +14,7 @@ export interface SignOutButtonBaseProps {
14
14
  export const SignOutButtonBase = (props: SignOutButtonBaseProps) => (
15
15
  <>
16
16
  <Text>{props.props.principal}</Text>
17
- <Button variant="secondary" isPending={props.props.pending} onPress={props.on.press}>
17
+ <Button intent="neutral" isPending={props.props.pending} onPress={props.on.press}>
18
18
  {props.props.label}
19
19
  </Button>
20
20
  </>
@@ -21,7 +21,7 @@ export const AppGuideOverlayBase = (props: AppGuideOverlayBaseProps) => (
21
21
  title={props.props.title}
22
22
  closeLabel={props.props.closeLabel}
23
23
  trigger={
24
- <Button variant="secondary" startContent={<Icon />}>
24
+ <Button intent="neutral" startContent={<Icon />}>
25
25
  {props.props.triggerLabel}
26
26
  </Button>
27
27
  }
@@ -10,12 +10,12 @@ import type { ReactNode } from "react"
10
10
  /** The options a status toast takes: HeroUI's, minus the variant the status already names. */
11
11
  type StatusToastOptions = Parameters<typeof heroToast.success>[1]
12
12
 
13
- /** The one indicator of each status, from the app's one icon set. */
13
+ /** The one indicator of each status, from the app's one icon set; HeroUI's toast variant colours it through currentColor. */
14
14
  const INDICATOR: Record<"success" | "danger" | "warning" | "info", ReactNode> = {
15
- success: <CheckCircleIcon className="size-6 text-success-soft-foreground" />,
16
- danger: <XCircleIcon className="size-6 text-danger-soft-foreground" />,
17
- warning: <ExclamationTriangleIcon className="size-6 text-warning-soft-foreground" />,
18
- info: <InformationCircleIcon className="size-6 text-accent" />,
15
+ success: <CheckCircleIcon className="size-6" />,
16
+ danger: <XCircleIcon className="size-6" />,
17
+ warning: <ExclamationTriangleIcon className="size-6" />,
18
+ info: <InformationCircleIcon className="size-6" />,
19
19
  }
20
20
 
21
21
  /** The app's toast: HeroUI's imperative toast with the status indicator injected; the region is mounted by the providers. */
@@ -17,7 +17,7 @@ export const LandingPageBase = (props: LandingPageBaseProps) => (
17
17
  <>
18
18
  <Heading level={1}>{props.props.title}</Heading>
19
19
  <Text tone="muted">{props.props.lede}</Text>
20
- <Button href={props.props.appHref} variant="primary">
20
+ <Button href={props.props.appHref} intent="commit">
21
21
  {props.props.openApp}
22
22
  </Button>
23
23
  </>
@@ -1,4 +1,4 @@
1
- /** The shell's frame: a full-height column on the theme's page colours. */
1
+ /** The shell's frame: a full-height column; colours come from Grammar's root. */
2
2
  export const siteShellClassNames = {
3
- frame: "flex min-h-screen flex-col bg-background text-foreground",
3
+ frame: "flex min-h-screen flex-col",
4
4
  } as const
@@ -14,6 +14,6 @@ type SiteShellProps = {
14
14
 
15
15
  /**
16
16
  * The document chrome both apps share: Grammar's Common boundary, the brand header (the top bar carrying the wordmark that links
17
- * home), then the routed body in the shell's main landmark at reading measure. Colours are the theme's semantic tokens.
17
+ * home), then the routed body in the shell's main landmark at reading measure.
18
18
  */
19
19
  {{> fe/common/site-shell.tsx.partial}}
@@ -1,4 +1,4 @@
1
- /** The shell's frame: a full-height column on the theme's page colours. */
1
+ /** The shell's frame: a full-height column; colours come from Grammar's root. */
2
2
  export const siteShellClassNames = {
3
- frame: "flex min-h-screen flex-col bg-background text-foreground",
3
+ frame: "flex min-h-screen flex-col",
4
4
  } as const
@@ -1,92 +0,0 @@
1
- import { Test } from "@nestjs/testing"
2
- import { mock } from "@starci/jest-preset"
3
- import type { HttpClient } from "./http.port"
4
- import { callGraphql } from "./graphql-call.mapper"
5
-
6
- const SUBJECT: unique symbol = Symbol("http/graphql-call.mapper")
7
- const subjectFunctions = () => ({ callGraphql })
8
-
9
- const build = async () => {
10
- const moduleRef = await Test.createTestingModule({
11
- providers: [{ provide: SUBJECT, useFactory: subjectFunctions }],
12
- }).compile()
13
- return moduleRef.get<ReturnType<typeof subjectFunctions>>(SUBJECT)
14
- }
15
-
16
- describe("callGraphql", () => {
17
- it("sends the operation and reads its data and string error codes", async () => {
18
- const subject = await build()
19
-
20
- const http = mock<HttpClient>()
21
- http.request.mockResolvedValue({
22
- status: 200,
23
- body: {
24
- data: { order: { id: "order-1" } },
25
- errors: [
26
- { extensions: { code: "ORDER_STALE" } },
27
- { extensions: { code: 7 } },
28
- { extensions: null },
29
- "malformed",
30
- ],
31
- },
32
- })
33
-
34
- await expect(
35
- subject.callGraphql(http, {
36
- correlationId: "request-1",
37
- url: "https://order.test/graphql",
38
- query: "query Order($id: ID!) { order(id: $id) { id } }",
39
- variables: { id: "order-1" },
40
- headers: { authorization: "Bearer token" },
41
- timeoutMs: 2500,
42
- }),
43
- ).resolves.toEqual({ data: { order: { id: "order-1" } }, errorCodes: ["ORDER_STALE"] })
44
-
45
- expect(http.request).toHaveBeenCalledWith({
46
- correlationId: "request-1",
47
- method: "POST",
48
- url: "https://order.test/graphql",
49
- headers: { authorization: "Bearer token" },
50
- body: {
51
- query: "query Order($id: ID!) { order(id: $id) { id } }",
52
- variables: { id: "order-1" },
53
- },
54
- timeoutMs: 2500,
55
- })
56
- })
57
-
58
- it("returns an empty answer when data and errors do not have GraphQL shapes", async () => {
59
- const subject = await build()
60
-
61
- const http = mock<HttpClient>()
62
- http.request.mockResolvedValue({ status: 200, body: { data: "malformed", errors: {} } })
63
-
64
- await expect(
65
- subject.callGraphql(http, {
66
- correlationId: "request-1",
67
- url: "https://order.test/graphql",
68
- query: "query Health { health }",
69
- timeoutMs: 1000,
70
- }),
71
- ).resolves.toEqual({ data: undefined, errorCodes: [] })
72
- })
73
-
74
- it.each([undefined, null, "not a response"])(
75
- "returns null when the response body is not an object (%p)",
76
- async (body) => {
77
- const subject = await build()
78
-
79
- const http = mock<HttpClient>()
80
- http.request.mockResolvedValue({ status: 502, body })
81
-
82
- await expect(
83
- subject.callGraphql(http, {
84
- correlationId: "request-1",
85
- url: "https://order.test/graphql",
86
- query: "query Health { health }",
87
- timeoutMs: 1000,
88
- }),
89
- ).resolves.toBeNull()
90
- },
91
- )
92
- })
@@ -1,52 +0,0 @@
1
- import { isRecord } from "@modules/platform/primitives"
2
- import type { HttpClient } from "./http.port"
3
-
4
- /** One GraphQL operation sent to another service. */
5
- export interface GraphqlCall {
6
- /** The correlation id of the request or delivery causing this operation. */
7
- readonly correlationId: string
8
- /** The GraphQL endpoint URL. */
9
- readonly url: string
10
- /** The operation text. */
11
- readonly query: string
12
- /** The operation variables. */
13
- readonly variables?: Readonly<Record<string, unknown>>
14
- /** Extra request headers, such as the bearer token to forward. */
15
- readonly headers?: Readonly<Record<string, string>>
16
- /** How long to wait for the answer. */
17
- readonly timeoutMs: number
18
- }
19
-
20
- /** What a GraphQL service answered: the data object and the codes of the errors it reported. */
21
- export interface GraphqlAnswer {
22
- /** The `data` object, undefined when the operation failed before producing one. */
23
- readonly data: Readonly<Record<string, unknown>> | undefined
24
- /** The `extensions.code` of every reported error. */
25
- readonly errorCodes: ReadonlyArray<string>
26
- }
27
-
28
- /** What a GraphQL call yields: the answer, or null when the body is not a GraphQL response at all. */
29
- export type GraphqlCallResult = GraphqlAnswer | null
30
-
31
- const codesOf = (errors: unknown): Array<string> =>
32
- Array.isArray(errors)
33
- ? errors.flatMap((error): Array<string> => {
34
- const code: unknown = isRecord(error) && isRecord(error.extensions) ? error.extensions.code : undefined
35
- return typeof code === "string" ? [code] : []
36
- })
37
- : []
38
-
39
- /** Sends a GraphQL operation and reads the answer; null when the body is not a GraphQL response at all. */
40
- export const callGraphql = async (http: HttpClient, call: GraphqlCall): Promise<GraphqlCallResult> => {
41
- const response = await http.request({
42
- correlationId: call.correlationId,
43
- method: "POST",
44
- url: call.url,
45
- ...(call.headers === undefined ? {} : { headers: call.headers }),
46
- body: { query: call.query, variables: call.variables },
47
- timeoutMs: call.timeoutMs,
48
- })
49
- const body = response.body
50
- if (!isRecord(body)) return null
51
- return { data: isRecord(body.data) ? body.data : undefined, errorCodes: codesOf(body.errors) }
52
- }