@starci/hfs 5.4.0 → 5.5.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 (127) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +4 -3
  3. package/cache/test-family.d.mts +1 -1
  4. package/cache/test-family.mjs +20 -15
  5. package/codemod/main.mjs +6 -1
  6. package/codemod/seeds-layout.d.mts +36 -0
  7. package/codemod/seeds-layout.mjs +113 -0
  8. package/codemod/sops-rules.d.mts +1 -1
  9. package/codemod/sops-rules.mjs +8 -3
  10. package/lint/run.d.mts +5 -4
  11. package/lint/run.mjs +53 -27
  12. package/package.json +1 -1
  13. package/release/lint-report.mjs +4 -3
  14. package/runtime/knowledge/hfs/canon-pins.yaml +12 -12
  15. package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
  16. package/runtime/knowledge/hfs/rules.yaml +15 -2
  17. package/runtime/knowledge/hfs/slots.yaml +15 -18
  18. package/runtime/modules/kernel/failure-codes.yaml +64 -3
  19. package/runtime/scripts/hfs/architecture/root-packages.mjs +1 -1
  20. package/runtime/scripts/hfs/check.mjs +4 -1
  21. package/runtime/scripts/hfs/coverage-scope.d.mts +15 -0
  22. package/runtime/scripts/hfs/coverage-scope.mjs +26 -0
  23. package/runtime/scripts/hfs/db-squash.mjs +9 -2
  24. package/runtime/scripts/hfs/declaration-shape.mjs +3 -1
  25. package/runtime/scripts/hfs/rules/app-root.mjs +4 -1
  26. package/runtime/scripts/hfs/rules/ci-standard.mjs +5 -4
  27. package/runtime/scripts/hfs/rules/coverage-below.mjs +12 -4
  28. package/runtime/scripts/hfs/rules/database-config.mjs +8 -7
  29. package/runtime/scripts/hfs/rules/database-constants.d.mts +2 -1
  30. package/runtime/scripts/hfs/rules/database-constants.mjs +2 -1
  31. package/runtime/scripts/hfs/rules/database.d.mts +2 -2
  32. package/runtime/scripts/hfs/rules/database.mjs +7 -6
  33. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  34. package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -1
  35. package/runtime/scripts/hfs/rules/monorepo.mjs +1 -1
  36. package/runtime/scripts/hfs/rules/seeds.d.mts +21 -0
  37. package/runtime/scripts/hfs/rules/seeds.mjs +194 -0
  38. package/runtime/scripts/hfs/rules/stacks.mjs +8 -4
  39. package/runtime/scripts/hfs/slots.d.mts +2 -0
  40. package/runtime/scripts/hfs/slots.mjs +1 -0
  41. package/runtime/scripts/hfs/stack-seeds-config.d.mts +88 -0
  42. package/runtime/scripts/hfs/stack-seeds-config.mjs +239 -0
  43. package/runtime/scripts/hfs/view.d.mts +25 -0
  44. package/runtime/scripts/hfs/view.mjs +41 -0
  45. package/scaffold/add-app.mjs +2 -2
  46. package/scaffold/add-cli-lite.mjs +41 -14
  47. package/scaffold/add-module.mjs +6 -3
  48. package/scaffold/lite-exports.mjs +1 -1
  49. package/scaffold/module-registry.mjs +10 -8
  50. package/src/main.mjs +8 -8
  51. package/stack/deploy.d.mts +3 -0
  52. package/stack/deploy.mjs +4 -1
  53. package/stack/local-up.mjs +25 -3
  54. package/stack/seeds.d.mts +29 -0
  55. package/stack/seeds.mjs +197 -0
  56. package/sync/canon-layout.d.mts +38 -0
  57. package/sync/canon-layout.mjs +62 -0
  58. package/sync/ci-commands.d.mts +2 -1
  59. package/sync/ci-commands.mjs +10 -7
  60. package/sync/index.d.mts +13 -2
  61. package/sync/index.mjs +52 -24
  62. package/templates/app/ci-workflows/github/workflows/ci.yml +2 -2
  63. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -2
  64. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +26 -0
  65. package/templates/app/skeleton/.starcistacks/dev/README.md +21 -10
  66. package/templates/app/skeleton/.starcistacks/dev/seeds/data/.gitkeep +0 -0
  67. package/templates/app/skeleton/.starcistacks/dev/seeds/files/.gitkeep +0 -0
  68. package/templates/app/skeleton/.starcistacks/dev/seeds/seeds.yaml +3 -0
  69. package/templates/app/skeleton/.starcistacks/production/seeds/data/.gitkeep +0 -0
  70. package/templates/app/skeleton/.starcistacks/production/seeds/files/.gitkeep +0 -0
  71. package/templates/app/skeleton/.starcistacks/production/seeds/seeds.yaml +2 -0
  72. package/templates/app/skeleton/.starcistacks/production/seeds/sql/.gitkeep +0 -0
  73. package/templates/app/skeleton/.starcistacks/seeds/data/.gitkeep +0 -0
  74. package/templates/app/skeleton/.starcistacks/seeds/files/.gitkeep +0 -0
  75. package/templates/app/skeleton/.starcistacks/seeds/seeds.yaml +2 -0
  76. package/templates/app/skeleton/.starcistacks/seeds/sql/.gitkeep +0 -0
  77. package/templates/app/skeleton/README.md +1 -1
  78. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  79. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/data/.gitkeep +0 -0
  80. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/files/.gitkeep +0 -0
  81. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/seeds.yaml +2 -0
  82. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/sql/.gitkeep +0 -0
  83. package/templates/app/skeleton-lite/.starcistacks/production/seeds/data/.gitkeep +0 -0
  84. package/templates/app/skeleton-lite/.starcistacks/production/seeds/files/.gitkeep +0 -0
  85. package/templates/app/skeleton-lite/.starcistacks/production/seeds/seeds.yaml +2 -0
  86. package/templates/app/skeleton-lite/.starcistacks/production/seeds/sql/.gitkeep +0 -0
  87. package/templates/app/skeleton-lite/.starcistacks/seeds/data/.gitkeep +0 -0
  88. package/templates/app/skeleton-lite/.starcistacks/seeds/files/.gitkeep +0 -0
  89. package/templates/app/skeleton-lite/.starcistacks/seeds/seeds.yaml +2 -0
  90. package/templates/app/skeleton-lite/.starcistacks/seeds/sql/.gitkeep +0 -0
  91. package/templates/app/skeleton-lite/README.md +2 -2
  92. package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +2 -0
  93. package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +6 -1
  94. package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +14 -1
  95. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +5 -1
  96. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +0 -1
  97. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +7 -0
  98. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +15 -0
  99. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +9 -3
  100. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +79 -16
  101. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +39 -9
  102. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +108 -20
  103. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +34 -7
  104. package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +28 -4
  105. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +6 -1
  106. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +9 -2
  107. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +11 -2
  108. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +59 -9
  109. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +4 -1
  110. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +25 -4
  111. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +8 -1
  112. package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +3 -1
  113. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +3 -3
  114. package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +1 -1
  115. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +3 -2
  116. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +6 -2
  117. package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +14 -9
  118. package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +3 -3
  119. package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +32 -7
  120. package/templates/be/tool-config/jest.config.js +1 -1
  121. package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +4 -1
  122. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +3 -0
  123. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +14 -6
  124. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +1 -0
  125. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +1 -0
  126. package/templates/app/module/jest.config.js.tpl +0 -8
  127. /package/templates/app/skeleton/.starcistacks/dev/seeds/{primary-notes.sql → sql/primary-notes.sql} +0 -0
package/sync/index.mjs CHANGED
@@ -21,7 +21,7 @@ import fs from 'node:fs';
21
21
  import path from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
23
  import { braceVariants } from '../runtime/scripts/lib/glob.mjs';
24
- import { codecovPaths, coverageComponents, jestCoverage, sonarCoverageExclusions } from '../runtime/scripts/hfs/coverage-scope.mjs';
24
+ import { codecovPaths, coverageComponents, jestCoverage, packagesCoverage, sonarCoverageExclusions } from '../runtime/scripts/hfs/coverage-scope.mjs';
25
25
  import { APP_SCOPE, SIDES, litePresenceOf, loadSlotManifest, resolveAppDeclaration, slotInEdition } from '../runtime/scripts/hfs/slots.mjs';
26
26
  import { managedGroupOf } from '../runtime/scripts/hfs/edition-slots.mjs';
27
27
  import { declaredSlotEnabled } from '../runtime/scripts/hfs/declaration-slots.mjs';
@@ -36,6 +36,7 @@ import { byCodeUnit } from '../report/order.mjs';
36
36
  import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
37
37
  import { ciWorkflowCommands } from './ci-commands.mjs';
38
38
  import { MODULE_PREFIX, moduleNames, registryFiles } from '../scaffold/module-registry.mjs';
39
+ import { canonImport, canonList } from './canon-layout.mjs';
39
40
  import { dependabotUpdates, hasDeclaredServices, readDeclaredQuality, readDeclaredBadges, sonarHostLine } from './quality-services.mjs';
40
41
  import { qualityServices } from '../runtime/scripts/hfs/ci-declaration.mjs';
41
42
  import { grammarLintFor } from '../runtime/scripts/lib/grammar-lint.mjs';
@@ -249,16 +250,23 @@ const SOURCE_SKIPPED = new Set(['node_modules', 'dist', 'coverage']);
249
250
  * lite brand tree exists, so sync can preserve its absence. `hasImage` identifies authored optional Dockerfiles without reading
250
251
  * their content. An app with no be folder has no source files.
251
252
  */
252
- /** The folders of `packages/` that are neither a module package nor the registry. */
253
+ /**
254
+ * The packages of `packages/` that are neither a module package nor the registry: a folder that holds a `package.json` and a `src/` (an install folder
255
+ * the workspaces leave there, `packages/node_modules`, is none).
256
+ */
253
257
  function otherPackages(root) {
254
258
  try {
255
259
  return fs.readdirSync(path.join(root, 'packages'), { withFileTypes: true })
256
- .filter(entry => entry.isDirectory() && !entry.name.startsWith(MODULE_PREFIX) && entry.name !== 'modules').map(entry => entry.name).sort(byCodeUnit);
260
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith(MODULE_PREFIX) && entry.name !== 'modules'
261
+ && isFile(path.join(root, 'packages', entry.name, 'package.json'), { followLinks: false }) && fs.existsSync(path.join(root, 'packages', entry.name, 'src')))
262
+ .map(entry => entry.name).sort(byCodeUnit);
257
263
  }
258
264
  catch {
259
265
  return [];
260
266
  }
261
267
  }
268
+ /** The app-relative folders of the root packages whose logic the back end's unit run measures: every module package and every other package, never the registry. */
269
+ export const rootPackageFolders = (source) => [...(source?.modules ?? []).map(name => `packages/${MODULE_PREFIX}${name}`), ...(source?.otherPackages ?? []).map(name => `packages/${name}`)];
262
270
  export function appSource(root, { parseYaml, notices } = {}) {
263
271
  const files = [];
264
272
  const walk = (dir) => {
@@ -283,7 +291,7 @@ export function appSource(root, { parseYaml, notices } = {}) {
283
291
  walk(dir);
284
292
  return { runtimeExample: isRuntimeExampleApp(root), files: files.sort(byCodeUnit), read: (file) => fs.readFileSync(path.join(root, 'be', file), 'utf8'),
285
293
  modules: moduleNames(root), otherPackages: otherPackages(root),
286
- workPresent: fs.existsSync(path.join(root, '.starciwork')), hasImage: (file) => isFile(path.join(root, file), { followLinks: false }),
294
+ workPresent: fs.existsSync(path.join(root, '.starciwork')), hasContracts: fs.existsSync(path.join(root, 'be', 'contracts')), hasImage: (file) => isFile(path.join(root, file), { followLinks: false }),
287
295
  stack: readDeclaredStack(root, { parseYaml }), quality: readDeclaredQuality(root, { parseYaml }),
288
296
  badges: readDeclaredBadges(root, { parseYaml, notices }),
289
297
  qualityDeclared: hasDeclaredServices(root, { parseYaml }) };
@@ -337,7 +345,7 @@ export function appScripts(app, source) {
337
345
  }),
338
346
  ...fe.map(entry => line(`start:${entry.name}`, `npm run start -w ${workspace(entry)}`)),
339
347
  ...(app.edition !== 'lite' && app.optionalSlots.includes('app.browser') ? [line('test:browser', 'playwright test -c browser/playwright.config.ts')] : []),
340
- ...(source?.modules?.length ? [line('build:modules', 'turbo run build --filter=./packages/*'), line('test:modules', 'turbo run test --filter=./packages/module-*')] : []),
348
+ ...(rootPackageFolders(source).length ? [line('build:modules', 'turbo run build --filter=./packages/*')] : []),
341
349
  line('docker:build', images.map(image => `npm run docker:build:${image.entry.name}`).join(' && ')),
342
350
  ...images.map(image => line(`docker:build:${image.entry.name}`, imageCommand(image))),
343
351
  ].join('\n ');
@@ -377,11 +385,13 @@ const IMAGE_TEMPLATE = Object.freeze({ api: 'be/image/api', worker: 'be/image/wo
377
385
  /**
378
386
  * The Dockerfile of every declared app, [{ path, content }] (app-relative): written once by starci app scaffold from templates/<side>/image/<kind>
379
387
  * and then the app's own, judged by the docker rules (R187-R191). A Next image copies the fe packages only when the fe side opts into
380
- * them and the be contracts only when the fe side reads them, so each COPY names a folder the app has.
388
+ * them and the be contracts only when the fe side reads them and the back end holds them (`source.hasContracts`: a back end that emits no
389
+ * contract has no be/contracts/, and a COPY of a missing folder fails the image build), so each COPY names a folder the app has.
381
390
  */
382
391
  export function imageFiles(app, source) {
383
392
  const side = app.sides.fe;
384
- const inputs = [...(opensPackages(side) ? ['fe/packages'] : []), ...((side.reads ?? []).includes('be/contracts/') ? ['be/contracts'] : [])];
393
+ const contracts = (side.reads ?? []).includes('be/contracts/') && source?.hasContracts === true;
394
+ const inputs = [...(opensPackages(side) ? ['fe/packages'] : []), ...(contracts ? ['be/contracts'] : [])];
385
395
  const copyInputs = inputs.map(folder => `COPY ${folder} ${folder}\n`).join('');
386
396
  // The folders outside be/ that the back end declares it reads (the generated database types of a lite back end, db/types/) are in every one of its images.
387
397
  const beFolders = (app.sides.be.reads ?? []).map(folder => folder.replace(/\/$/, ''));
@@ -394,32 +404,49 @@ export function imageFiles(app, source) {
394
404
  });
395
405
  }
396
406
  /** Every value a template of `scope` can name, derived from hfs.json and the presets. */
397
- /** What a module package keeps out of the measured scope: its workbench (drawn with Grammar, a front end has no tests) and its specs. The logic of its template and manifest is measured. */
398
- const moduleExclusions = (dirs) => dirs.flatMap(dir => [`${dir}/src/workbench/**`, `${dir}/src/**/index.ts`]);
399
- /** The managed files of the registry package for the module packages the app has (empty when it has none). */
407
+ /**
408
+ * The `packages` option of the managed be/jest.config.js (`starciJestConfig`), laid out as prettier lays it out (width 120, indent 4): the root packages whose
409
+ * specs the unit run holds, with the files measured in them; an app without one has no such option.
410
+ */
411
+ export function jestPackagesSource(packages) {
412
+ if (packages.folders.length === 0)
413
+ return '';
414
+ const list = (key, items) => {
415
+ const one = ` ${key}: [${items.map(item => JSON.stringify(item)).join(', ')}],`;
416
+ return one.length <= 120 ? one : [` ${key}: [`, ...items.map(item => ` ${JSON.stringify(item)},`), ' ],'].join('\n');
417
+ };
418
+ return ['', ' packages: {', list('folders', packages.folders), list('files', packages.files), list('excludes', packages.excludes), ' },'].join('\n');
419
+ }
420
+ /**
421
+ * The managed files of the registry package for the module packages the app has (empty when it has none). An app that keeps its modules as data
422
+ * (hfs.json `modules: "data"`) has no rendered registry: a module is a seed there, and packages/modules, if any, is the app's own.
423
+ */
400
424
  function registryTargets(app, source) {
425
+ if (app.modules === 'data')
426
+ return [];
401
427
  return registryFiles(app.project, source?.modules ?? []).map((file) => ({ path: file.path, scope: APP_SCOPE, mode: 'file', content: file.content, hash: hashOf(file.content) }));
402
428
  }
403
429
  /**
404
- * The root `eslint.config.mjs` of an app that has root workspace packages (packages/agent-sdk, packages/module-<name>, the registry): no side config
430
+ * The root `eslint.config.mjs` of an app that has root workspace packages (packages/<name>: the agent SDK, a block library, a module package, the registry): no side config
405
431
  * reaches `packages/`, so this managed file lints their source by subpath. The back-end canon judges everything but the front-end subpaths
406
432
  * (`src/workbench/` outside its contract, `src/public/`), the front-end canon and the selected Grammar family those, each over the package view of
407
433
  * the app root. An app without a root package has no such file; the family factory for packages is the family's `<name>GrammarPackagesConfig`.
434
+ * It is laid out as the canon Prettier config prints it (`canonList`, `canonImport`), so the app's format check passes it as rendered.
408
435
  */
409
436
  function packagesLintTargets(app, source) {
410
437
  if (!(source?.modules?.length || source?.otherPackages?.length))
411
438
  return [];
412
439
  const grammar = grammarLintFor(app.sides.fe.grammar);
413
- const factory = grammar.package ? grammar.factory.replace(/Config$/, 'PackagesConfig') : null;
440
+ const family = grammar.package ? { name: grammar.package, factory: grammar.factory.replace(/Config$/, 'PackagesConfig') } : null;
414
441
  const content = [
415
442
  'import { loadPackagesHfs as loadBePackages, starciBePackagesConfig } from "@starci/eslint-canon-be"',
416
443
  'import { loadPackagesHfs as loadFePackages, starciFePackagesConfig } from "@starci/eslint-canon-fe"',
417
- ...(grammar.package ? [`import { ${factory} } from "${grammar.package}"`] : []),
444
+ ...(family ? [canonImport({ names: [family.factory], from: family.name })] : []),
418
445
  '',
419
446
  'const be = loadBePackages(import.meta.url)',
420
447
  'const fe = loadFePackages(import.meta.url)',
421
448
  '',
422
- `export default [...(await starciBePackagesConfig({ hfs: be })), ...starciFePackagesConfig({ hfs: fe })${grammar.package ? `, ...${factory}({ hfs: fe })` : ''}]`,
449
+ canonList({ head: 'export default ', items: ['...(await starciBePackagesConfig({ hfs: be }))', '...starciFePackagesConfig({ hfs: fe })', ...(family ? [`...${family.factory}({ hfs: fe })`] : [])] }),
423
450
  '',
424
451
  ].join(LINE);
425
452
  return [{ path: PACKAGES_LINT_FILE, scope: APP_SCOPE, mode: 'file', content, hash: hashOf(content) }];
@@ -431,10 +458,11 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
431
458
  const lite = app.edition === 'lite';
432
459
  const providers = (app.sides.be.connections ?? []).map(connection => connection.provider);
433
460
  const quality = source.quality ?? qualityServices({});
434
- const modules = source.modules ?? [];
435
- const moduleDirs = modules.map(name => `packages/${MODULE_PREFIX}${name}`);
436
- // The module packages are built before anything imports them (typecheck, lint, the fe build and the be run all follow the registry).
437
- const modulesFirst = modules.length ? 'npm run build:modules --silent && ' : '';
461
+ // The root packages (the agent SDK, the module packages and the rest) are built before anything imports them (typecheck, lint, the fe build and the be
462
+ // run), and their logic is measured by the back end's unit run like its own (packagesCoverage, BE-TEST-13).
463
+ const rootPackages = packagesCoverage(rootPackageFolders(source));
464
+ const moduleDirs = rootPackages.folders;
465
+ const modulesFirst = moduleDirs.length ? 'npm run build:modules --silent && ' : '';
438
466
  const grammarLint = scope === 'fe' ? grammarLintFor(fe.grammar) : null;
439
467
  return {
440
468
  header: HEADER(scope),
@@ -457,9 +485,8 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
457
485
  typecheck: `${modulesFirst}npm run codegen --silent && tsc -p be/tsconfig.json && turbo run typecheck`,
458
486
  // The typed lint reads the types the fe packages declare (their dist), so the packages are built first.
459
487
  lintPrefix: `${modulesFirst}${packages ? 'npm run codegen --silent && turbo run build --filter=./fe/packages/* && ' : 'npm run codegen --silent && '}`,
460
- // The unit run, the back-end build and every CI job that imports a module package find it built (build:modules precedes them); the logic of a package is held at 100 per file by its own jest run, a step of the fast job.
488
+ // The unit run, the back-end build and every CI job that imports a root package find it built (build:modules precedes them); its specs run in the unit run and its shards.
461
489
  modulesFirst,
462
- moduleTestStep: modules.length ? ' - name: module packages\n run: npm run test:modules\n' : '',
463
490
  nodeMajor: String(NODE_MAJOR),
464
491
  // The deploy server's age identity the restore workflow writes (stack/server-key.mjs owns the path).
465
492
  serverAgeIdentity: SERVER_AGE_IDENTITY,
@@ -472,12 +499,13 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
472
499
  sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : []), ...moduleDirs.map(dir => `${dir}/src`)].join(','),
473
500
  lcovReport: LCOV_REPORT,
474
501
  cpdExclusions: SONAR_CPD_EXCLUSIONS.join(','),
475
- ...ciWorkflowCommands(source, LCOV_REPORT, source.qualityDeclared === false ? undefined : source.quality, { app, packages }),
502
+ ...ciWorkflowCommands(source, LCOV_REPORT, source.qualityDeclared === false ? undefined : source.quality, { app, packages, rootPackages: moduleDirs.length > 0 }),
476
503
  // The coverage scope is the same in both editions: lite measures exactly what full measures (ruling:R212/case-1, amended by ruling:R212/case-4).
477
504
  jestCoverage: scope === 'be' ? jestCoverageSource(jestCoverage(manifest, providers)) : '',
478
- coverageExclusions: scope === APP_SCOPE ? [...sonarCoverageExclusions(manifest, providers), ...moduleExclusions(moduleDirs)].join(',') : '',
479
- codecovExclusions: scope === APP_SCOPE ? [...sonarCoverageExclusions(manifest, providers), ...moduleExclusions(moduleDirs)].map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
480
- codecovPaths: scope === APP_SCOPE ? [...codecovPaths(manifest, providers), ...moduleDirs.flatMap(dir => [`${dir}/src/manifest/**`, `${dir}/src/template/**`])].map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
505
+ jestPackages: scope === 'be' ? jestPackagesSource(rootPackages) : '',
506
+ coverageExclusions: scope === APP_SCOPE ? [...sonarCoverageExclusions(manifest, providers), ...rootPackages.excludes].join(',') : '',
507
+ codecovExclusions: scope === APP_SCOPE ? [...sonarCoverageExclusions(manifest, providers), ...rootPackages.excludes].map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
508
+ codecovPaths: scope === APP_SCOPE ? [...codecovPaths(manifest, providers), ...moduleDirs.map(dir => `${dir}/src/**`)].map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
481
509
  codecovComponents: scope === APP_SCOPE ? componentsYaml(coverageComponents(manifest, { ...source, apps: app.sides.be.apps, providers })) : '',
482
510
  codecovTarget: quality.codecov.target,
483
511
  dependabotUpdates: dependabotUpdates(quality.dependabot),
@@ -92,7 +92,7 @@ jobs:
92
92
  run: npm run typecheck
93
93
  env:
94
94
  NODE_OPTIONS: {{nodeOptions}}
95
- {{moduleTestStep}}
95
+
96
96
  lint:
97
97
  needs: plan
98
98
  if: ${{ needs.plan.outputs.checks == 'true' }}
@@ -101,7 +101,7 @@ jobs:
101
101
  strategy:
102
102
  fail-fast: false
103
103
  matrix:
104
- side: [be, fe]
104
+ side: {{lintSides}}
105
105
  steps:
106
106
  {{> app/ci-workflows/install-steps.yml}}
107
107
  {{> app/ci-workflows/lint-base.yml}}
@@ -92,7 +92,7 @@ jobs:
92
92
  run: npm run typecheck
93
93
  env:
94
94
  NODE_OPTIONS: {{nodeOptions}}
95
- {{moduleTestStep}}
95
+
96
96
  lint:
97
97
  needs: plan
98
98
  if: ${{ needs.plan.outputs.checks == 'true' }}
@@ -101,7 +101,7 @@ jobs:
101
101
  strategy:
102
102
  fail-fast: false
103
103
  matrix:
104
- side: [be, fe]
104
+ side: {{lintSides}}
105
105
  steps:
106
106
  {{> app/ci-workflows/install-steps.yml}}
107
107
  {{> app/ci-workflows/lint-base.yml}}
@@ -40,6 +40,32 @@ environments:
40
40
  runbook: README.md
41
41
  # Custody starts empty; add real sealed members after owner provisioning.
42
42
  secrets: []
43
+ # `starci app stack up --env dev --execute` reads this block (PROJECT-STACKS-6): the compose services in dependency order, each
44
+ # awaited on its healthcheck, then readiness and the origins. The app images come from `starci app stack prepare --env dev --execute`.
45
+ local:
46
+ ports:
47
+ core: 3001
48
+ app: 3000
49
+ landing: 3002
50
+ services:
51
+ - {name: postgres, wait: healthy}
52
+ - {name: core, wait: healthy, port: core}
53
+ - {name: app, wait: healthy, port: app}
54
+ - {name: landing, wait: healthy, port: landing}
55
+ ready:
56
+ - {name: core-live, url: "http://127.0.0.1:${port.core}/health/live"}
57
+ - {name: core-ready, url: "http://127.0.0.1:${port.core}/health/ready"}
58
+ - {name: app-live, url: "http://127.0.0.1:${port.app}/health/live"}
59
+ - {name: landing-live, url: "http://127.0.0.1:${port.landing}/health/live"}
60
+ verify:
61
+ - {name: core-live, url: "http://127.0.0.1:${port.core}/health/live"}
62
+ - {name: core-ready, url: "http://127.0.0.1:${port.core}/health/ready"}
63
+ - {name: app-live, url: "http://127.0.0.1:${port.app}/health/live"}
64
+ - {name: landing-live, url: "http://127.0.0.1:${port.landing}/health/live"}
65
+ origins:
66
+ - {name: app, url: "http://127.0.0.1:${port.app}"}
67
+ - {name: landing, url: "http://127.0.0.1:${port.landing}"}
68
+ - {name: core, url: "http://127.0.0.1:${port.core}"}
43
69
  production:
44
70
  status: supported
45
71
  runtime: docker-swarm
@@ -1,16 +1,27 @@
1
1
  # Development stack
2
2
 
3
- Every development dependency runs in Docker Linux containers. This starter's Docker Compose model contains the core API, the product and landing front ends, and a pinned local PostgreSQL database. Compose is an implementation detail; declared dependencies may also use managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts. A declared native profile may run app processes locally while dependencies stay in Docker. Trust authentication is confined to this local, unpublished database.
4
- Starter release images and external secrets must be prepared before starting the container model. Local source development remains available through the app scripts.
3
+ The dev environment is one command: `starci app stack up --env dev --execute`. It checks the machine, opens the dev custody, starts the
4
+ compose services of `infra/compose/stack.yaml` in Docker Linux containers (the pinned local PostgreSQL first, then the core API and the product
5
+ and landing front ends), waits for each healthcheck and for readiness, and prints the origins. What it runs is the `local` block of
6
+ `environments.dev` in `.starcistacks/application-stacks.yaml` (PROJECT-STACKS-6 in contexts/project/stacks.md); this runbook never carries a
7
+ docker command, an agent never runs docker by hand. Trust authentication is confined to this local, unpublished database.
8
+
9
+ **A new machine.** Install what `starci app stack doctor --env dev` reports missing (it prints the exact winget, brew or apt step per tool; Node
10
+ from `.nvmrc`, npm, git, Docker with Compose v2 running Linux containers, WSL2 on Windows, sops and age), then run `prepare` and `up`.
11
+
12
+ **Custody.** The API reads its database connection from the owner-controlled file `/run/starci-secrets/{{project}}/dev/primary-db-url`, which
13
+ the compose model mounts into the API; materialize its real sealed member there before `up`. The scaffold creates no fake secret or
14
+ successful proof.
5
15
 
6
16
  | Command | Operation |
7
17
  | --- | --- |
8
- | prepare | Build the app images and provide the local database connection through sealed custody. Materialize its real sealed member to the owner-controlled file `/run/starci-secrets/{{project}}/dev/primary-db-url`; Docker Compose mounts that file into the API. |
9
- | doctor | From this directory, run `docker compose config -f infra/compose/stack.yaml`. Confirm image, secret and database availability. |
10
- | up | Run `docker compose -f infra/compose/stack.yaml up -d` from this directory after preparation. |
11
- | status | Run `docker compose -f infra/compose/stack.yaml ps`. |
12
- | logs | Run `docker compose -f infra/compose/stack.yaml logs core`; never print mounted credentials. |
13
- | down | Run `docker compose -f infra/compose/stack.yaml down`. |
14
- | verification | Confirm the API `/health/live` and `/health/ready` endpoints, then run `npm run lint` from the app root. |
18
+ | prepare | `starci app stack prepare --env dev --execute`: checks the machine, opens the custody and builds the app images. |
19
+ | doctor | `starci app stack doctor --env dev`: read-only table of host prerequisites with the owner-run install step per OS, custody, compose render and ports. |
20
+ | up | `starci app stack up --env dev --execute`: prepare, then the services in order and readiness; prints the origins. |
21
+ | status | `starci app stack status --env dev`: desired against running and every readiness probe. |
22
+ | logs | `starci app stack logs --env dev --service core` (a compose service, bounded and redacted; never prints mounted credentials). |
23
+ | down | `starci app stack down --env dev --execute`: stops the compose project; volumes and sealed values stay. |
24
+ | verification | `starci app stack verify --env dev`: the declared liveness and readiness probes after a real start, then `npm run lint` from the app root. |
15
25
 
16
- Keep real sealed members under `.starcistacks/dev/secrets/*.enc`, add them to the manifest after sealing and inventory their key names in the environment record. The scaffold creates no fake secret or successful proof.
26
+ Keep real sealed members under `.starcistacks/dev/secrets/*.enc.yaml`, add them to the manifest after sealing and inventory their key names in
27
+ `.starcistacks/dev/runtime/env/KEYS.md`.
@@ -0,0 +1,3 @@
1
+ schema: starci/seeds@1
2
+ entries:
3
+ - {id: primary-notes, kind: sql, path: sql/primary-notes.sql, target: primary, mode: upsert}
@@ -0,0 +1,2 @@
1
+ schema: starci/seeds@1
2
+ entries: []
@@ -0,0 +1,2 @@
1
+ schema: starci/seeds@1
2
+ entries: []
@@ -36,7 +36,7 @@ npm run build:fe
36
36
  The core api app reads `PORT`, `HTTP_SECURITY_ALLOWED_ORIGINS` (comma-separated origins allowed to send state-changing browser
37
37
  requests) and `PRIMARY_DB_URL` at boot; every door is closed until it is marked public or sign-in is designed. The cli app reads
38
38
  `PRIMARY_DB_URL`: `npm run migrate` applies the migrations of the primary database before the api starts, and
39
- `npm run cli -- seed run` runs the dev seeds of `.starcistacks/dev/seeds` (run both from the app root). The product app reads
39
+ `npm run cli -- seed run` runs the SQL seeds of `.starcistacks/seeds/sql/`, then those of `.starcistacks/dev/seeds/sql/` (`--env <env>` names another environment; run both from the app root). The product app reads
40
40
  `NEXT_PUBLIC_SITE_URL` and `NEXT_PUBLIC_GRAPHQL_URL` (the back end it calls through `fe/apps/app/src/modules/api`, read once the first GraphQL operation exists), the landing `NEXT_PUBLIC_APP_URL` (the product app it hands the reader over to). A missing key stops the process with an error that names it.
41
41
 
42
42
  Production browser build values live in `.starcistacks/application-stacks.yaml` under `environments.production.deploy.images.*.buildArgs.values`. Replace the placeholder origins for the product's `NEXT_PUBLIC_SITE_URL` and the landing's `NEXT_PUBLIC_APP_URL` with your product app's public origin before building deployment images.
@@ -2,7 +2,7 @@
2
2
 
3
3
  The dev environment is one command: `starci app stack up --env dev --execute`. It checks the machine, opens the dev custody, runs the setup
4
4
  commands, starts the dependencies in Docker Linux containers (the `db` service: the official postgres image plus pgvector, built from
5
- `.starcistacks/images/db/Dockerfile`; the `mail` service: Mailpit, the dev mail sink), gives the environment its own database role, migrates, runs the seed, starts the api and web
5
+ `.starcistacks/images/db/Dockerfile`; the `mail` service: Mailpit, the dev mail sink), gives the environment its own database role, migrates, applies the seed levels `.starcistacks/seeds/` then `.starcistacks/dev/seeds/`, starts the api and web
6
6
  processes natively and waits for readiness. What it runs is the `local` block of `environments.dev` in `.starcistacks/application-stacks.yaml`
7
7
  (PROJECT-STACKS-6 in contexts/project/stacks.md); this runbook never carries a docker command, an agent never runs docker by hand.
8
8
 
@@ -20,7 +20,7 @@ another environment.
20
20
  | --- | --- |
21
21
  | prepare | `starci app stack prepare --env dev --execute`: checks the machine, opens the custody, runs the setup commands and builds the images. |
22
22
  | doctor | `starci app stack doctor --env dev`: read-only table of host prerequisites with the owner-run install step per OS, custody, compose render and ports. |
23
- | up | `starci app stack up --env dev --execute`: prepare, then the services, the database role, migrations with a receipt, the seed, the processes and readiness; prints the origins. |
23
+ | up | `starci app stack up --env dev --execute`: prepare, then the services, the database role, migrations with a receipt, the seed levels `.starcistacks/seeds/` then `.starcistacks/dev/seeds/`, each entry once per content hash, the processes and readiness; prints the origins. |
24
24
  | status | `starci app stack status --env dev`: desired against running, the migration receipt and every readiness probe. |
25
25
  | logs | `starci app stack logs --env dev --service api` (a process or a compose service, bounded and redacted). |
26
26
  | down | `starci app stack down --env dev --execute`: ends the processes and stops the compose project; volumes and sealed values stay. |
@@ -0,0 +1,2 @@
1
+ schema: starci/seeds@1
2
+ entries: []
@@ -0,0 +1,2 @@
1
+ schema: starci/seeds@1
2
+ entries: []
@@ -0,0 +1,2 @@
1
+ schema: starci/seeds@1
2
+ entries: []
@@ -12,10 +12,10 @@ TypeScript everywhere, NestJS in `be/`, Next.js with next-intl in `fe/`, Postgre
12
12
 
13
13
  ## Repository layout
14
14
 
15
- - `db/`: forward-only SQL migrations, seed data, and generated database types.
15
+ - `db/`: forward-only SQL migrations and generated database types.
16
16
  - `be/`: the API, shared platform capabilities, verified identity admission, and the passport and storage integrations.
17
17
  - `fe/`: the web app; it holds no credential, signs in through `fe/apps/web/src/modules/auth` and reaches data through `fe/apps/web/src/modules/api`.
18
- - `.starcistacks/`: value-free environment metadata and encrypted-secret custody.
18
+ - `.starcistacks/`: value-free environment metadata, encrypted-secret custody and the seed levels (`seeds/` for every environment, `<env>/seeds/` for one, each a `seeds.yaml` manifest of `sql/`, `data/` and `files/`).
19
19
  - `.starciwork/`: `.gitignore`, `.starciwork/workspace.yaml` project metadata and repo roles, and optional brand drawing records, references, and assets.
20
20
 
21
21
  ## Status badges
@@ -17,6 +17,8 @@ export interface SignupData {
17
17
  readonly email: string
18
18
  /** The chosen password. */
19
19
  readonly password: string
20
+ /** The person's full name, when the request names one. */
21
+ readonly name: string | undefined
20
22
  /** The language of the confirmation mail, when the request names one. */
21
23
  readonly language: MailLanguage | undefined
22
24
  }
@@ -17,6 +17,11 @@ export class SignupHandler extends ICQRSHandler<SignupCommand, void> {
17
17
  }
18
18
 
19
19
  protected override process(command: SignupCommand): Promise<void> {
20
- return this.email.signup(command.params.request.email, command.params.request.password, command.params.request.language)
20
+ return this.email.signup(
21
+ command.params.request.email,
22
+ command.params.request.password,
23
+ command.params.request.language,
24
+ command.params.request.name,
25
+ )
21
26
  }
22
27
  }
@@ -15,7 +15,20 @@ import { ResetHandler } from "./application/reset.handler"
15
15
  import { ConfigurableModuleClass } from "./auth.module-definition"
16
16
 
17
17
  @Module({
18
- providers: [LoginHandler, RefreshHandler, SessionHandler, LogoutHandler, GoogleBeginHandler, GoogleCompleteHandler, GetJwksHandler, SignupHandler, ResendHandler, ConfirmHandler, ForgotHandler, ResetHandler],
18
+ providers: [
19
+ LoginHandler,
20
+ RefreshHandler,
21
+ SessionHandler,
22
+ LogoutHandler,
23
+ GoogleBeginHandler,
24
+ GoogleCompleteHandler,
25
+ GetJwksHandler,
26
+ SignupHandler,
27
+ ResendHandler,
28
+ ConfirmHandler,
29
+ ForgotHandler,
30
+ ResetHandler,
31
+ ],
19
32
  })
20
33
  /** The auth feature: the handlers behind the sign-in doors. */
21
34
  export class AuthApiModule extends ConfigurableModuleClass {}
@@ -96,7 +96,11 @@ export class AuthController {
96
96
  @Public({ reason: PublicReason.AuthHandshake })
97
97
  @RateLimit(RateTier.Strict)
98
98
  signup(@Body() body: SignupRequest): Promise<void> {
99
- return this.commandBus.execute(new SignupCommand({ request: { email: body.email, password: body.password, language: body.language } }))
99
+ return this.commandBus.execute(
100
+ new SignupCommand({
101
+ request: { email: body.email, password: body.password, name: body.name, language: body.language },
102
+ }),
103
+ )
100
104
  }
101
105
 
102
106
  /** Mails a new confirmation link; always answers 204. */
@@ -14,5 +14,4 @@ export class ResetRequest {
14
14
  @MinLength(1)
15
15
  @MaxLength(128)
16
16
  password!: string
17
-
18
17
  }
@@ -15,6 +15,13 @@ export class SignupRequest {
15
15
  @MaxLength(128)
16
16
  password!: string
17
17
 
18
+ /** The person's full name, kept on the account's profile keys; left out, the account has none. */
19
+ @IsOptional()
20
+ @IsString()
21
+ @MinLength(1)
22
+ @MaxLength(200)
23
+ name?: string
24
+
18
25
  /** The language of the mail; the app's default when left out. */
19
26
  @IsOptional()
20
27
  @IsIn(["vi", "en"])
@@ -32,6 +32,21 @@ describe("IdentityAccountDirectoryService.find", () => {
32
32
  expect(await addressless.directory.find("u-1")).toEqual({ id: "u-1", email: null, disabled: false })
33
33
  })
34
34
 
35
+ it("answers the full name the profile keys hold, and none for an empty or a missing one", async () => {
36
+ const named = await build([accountByIdRow("named")])
37
+ const blank = await build([{ ...accountByIdRow(), meta: { full_name: "" } }])
38
+ const raw = await build([{ ...accountByIdRow(), meta: null }])
39
+
40
+ expect(await named.directory.find("u-1")).toEqual({
41
+ id: "u-1",
42
+ email: "a@example.test",
43
+ disabled: false,
44
+ name: "Ada Lovelace",
45
+ })
46
+ expect((await blank.directory.find("u-1"))?.name).toBeUndefined()
47
+ expect((await raw.directory.find("u-1"))?.name).toBeUndefined()
48
+ })
49
+
35
50
  it("answers undefined for an account that does not exist", async () => {
36
51
  const { directory } = await build([])
37
52
 
@@ -3,10 +3,10 @@ import { Injectable } from "@nestjs/common"
3
3
  import type { EntityManager } from "typeorm"
4
4
  import type { AccountDirectory, AccountStanding } from "@modules/integrations/passport"
5
5
  import { InjectPrimaryEntityManager } from "@modules/platform/database"
6
- import { accountDoors } from "./persistence/account-doors.port"
6
+ import { accountDoors, fullNameOf } from "./persistence/account-doors.port"
7
7
 
8
8
  @Injectable()
9
- /** The package's account directory over the account doors: whether an account still stands, for a refresh to go on. */
9
+ /** The package's account directory over the account doors: whether an account still stands, for a refresh to go on, and the person's full name. */
10
10
  export class IdentityAccountDirectoryService implements AccountDirectory {
11
11
  constructor(@InjectPrimaryEntityManager() private readonly entityManager: EntityManager) {}
12
12
 
@@ -14,6 +14,12 @@ export class IdentityAccountDirectoryService implements AccountDirectory {
14
14
  async find(userId: string): Promise<AccountStanding | undefined> {
15
15
  const account = await accountDoors.userById(this.entityManager, userId)
16
16
  if (account === undefined) return undefined
17
- return { id: account.id, email: account.email, disabled: account.disabled }
17
+ const name = fullNameOf(account.meta)
18
+ return {
19
+ id: account.id,
20
+ email: account.email,
21
+ disabled: account.disabled,
22
+ ...(name === undefined ? {} : { name }),
23
+ }
18
24
  }
19
25
  }