@starci/hfs 4.0.10 → 4.2.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 (275) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +65 -24
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +194 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +222 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/release-config.mjs +28 -0
  14. package/runtime/engine/removed-vocabulary.mjs +28 -0
  15. package/runtime/engine/resources-config.mjs +52 -0
  16. package/runtime/engine/runtime-root.mjs +17 -0
  17. package/runtime/engine/secrets.mjs +35 -31
  18. package/runtime/engine/sonar-config.mjs +21 -0
  19. package/runtime/engine/temp-root.mjs +32 -0
  20. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +161 -65
  23. package/runtime/knowledge/hfs/slots.yaml +11 -14
  24. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  25. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  26. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  28. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
  30. package/runtime/modules/models/registry.yaml +25 -359
  31. package/runtime/modules/models/runtimes.yaml +126 -265
  32. package/runtime/modules/models/tiers.yaml +66 -0
  33. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  34. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  35. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  36. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  37. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  38. package/runtime/scripts/api/git/lib.mjs +34 -1
  39. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  40. package/runtime/scripts/api/process/run-program.mjs +2 -1
  41. package/runtime/scripts/api/sops/lib.mjs +198 -120
  42. package/runtime/scripts/hfs/allows.mjs +3 -3
  43. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  44. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  45. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  46. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  47. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  48. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  49. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  50. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  51. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  52. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  53. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  54. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  55. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  56. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  57. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  58. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  59. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  60. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  61. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  62. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  63. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  64. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  65. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  66. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  67. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  68. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  69. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  70. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  71. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  72. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  74. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  75. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  76. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  77. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  78. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  79. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  80. package/runtime/scripts/hfs/architecture/index.mjs +164 -104
  81. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  82. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  83. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  84. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  85. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  86. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  87. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  88. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  89. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  90. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  91. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  92. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  93. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  94. package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
  95. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  96. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  97. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  98. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  99. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  100. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  101. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  102. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  103. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  104. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  105. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  106. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  107. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  108. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  109. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  110. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  111. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  112. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  113. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  114. package/runtime/scripts/hfs/architecture.mjs +3 -4
  115. package/runtime/scripts/hfs/catalog-once.mjs +15 -0
  116. package/runtime/scripts/hfs/check.mjs +63 -71
  117. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  118. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  119. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  120. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  121. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  122. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  123. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  124. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  125. package/runtime/scripts/hfs/repo-identity.mjs +21 -4
  126. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  127. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  128. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  129. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  130. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  131. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  132. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  133. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  134. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  135. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  136. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  137. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  138. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  139. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  140. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  141. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  142. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  143. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  144. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  145. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  146. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  147. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  148. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  149. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  150. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  151. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  152. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  153. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  154. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  155. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  156. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  157. package/runtime/scripts/hfs/secret.mjs +59 -48
  158. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  159. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  160. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  161. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  162. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  163. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  164. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  165. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  166. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  167. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  168. package/runtime/scripts/hfs/slots.mjs +74 -695
  169. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  170. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  171. package/runtime/scripts/hfs/tree.mjs +19 -14
  172. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  173. package/runtime/scripts/hfs/view.mjs +1 -1
  174. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  175. package/runtime/scripts/lib/env.mjs +10 -0
  176. package/runtime/scripts/lib/event-contract.mjs +17 -13
  177. package/runtime/scripts/lib/git.mjs +2 -2
  178. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  179. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  180. package/runtime/scripts/lib/i18n.mjs +3 -2
  181. package/runtime/scripts/lib/in-order.mjs +71 -0
  182. package/runtime/scripts/lib/language.mjs +3 -3
  183. package/runtime/scripts/lib/list.mjs +8 -1
  184. package/runtime/scripts/lib/path-key.mjs +38 -5
  185. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  186. package/runtime/scripts/lib/regex.mjs +1 -1
  187. package/runtime/scripts/lib/same-text.mjs +1 -1
  188. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  189. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  190. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  191. package/runtime/scripts/lib/stack-services.mjs +1 -2
  192. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  193. package/runtime/scripts/lib/walk.mjs +6 -1
  194. package/runtime/scripts/lib/yaml-cached.mjs +14 -0
  195. package/scaffold/add-cli-lite.mjs +2 -1
  196. package/scaffold/add-table.mjs +2 -2
  197. package/scaffold/add.mjs +3 -2
  198. package/scaffold/app.mjs +4 -2
  199. package/scaffold/edition-gate.mjs +25 -25
  200. package/scaffold/lite-exports.mjs +2 -1
  201. package/scaffold/service.mjs +5 -4
  202. package/sync/hygiene.mjs +15 -9
  203. package/sync/index.mjs +7 -3
  204. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  205. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  206. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  207. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  208. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  209. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  210. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  211. package/templates/app/starciwork.gitignore +1 -1
  212. package/templates/be/image/api/Dockerfile +1 -1
  213. package/templates/be/image/cli/Dockerfile +1 -1
  214. package/templates/be/image/worker/Dockerfile +1 -1
  215. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  216. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  217. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  218. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  219. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  220. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  221. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  222. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  223. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  224. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  225. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  226. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  228. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  229. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  230. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  231. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  232. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  233. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  234. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  235. package/templates/fe/image/next/Dockerfile +2 -1
  236. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  238. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  239. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  242. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  243. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  245. package/upgrade/index.mjs +9 -2
  246. package/runtime/engine/admission.mjs +0 -308
  247. package/runtime/engine/canonical-json.mjs +0 -10
  248. package/runtime/engine/db/blob.mjs +0 -315
  249. package/runtime/engine/db/ledger-paths.mjs +0 -83
  250. package/runtime/engine/db/ledger.mjs +0 -1118
  251. package/runtime/engine/db/machine-connection.mjs +0 -135
  252. package/runtime/engine/db/machine-schema.mjs +0 -84
  253. package/runtime/engine/db/machine.mjs +0 -1367
  254. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  255. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  256. package/runtime/engine/db/provider-reservations.mjs +0 -101
  257. package/runtime/engine/digest.mjs +0 -16
  258. package/runtime/engine/refuse.mjs +0 -11
  259. package/runtime/modules/ops/_labels.yaml +0 -53
  260. package/runtime/scripts/api/node/lib.mjs +0 -14
  261. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  262. package/runtime/scripts/api/process/lib.mjs +0 -111
  263. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  264. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  265. package/runtime/scripts/connectors/lib.mjs +0 -488
  266. package/runtime/scripts/lib/clip.mjs +0 -19
  267. package/runtime/scripts/lib/display-names.mjs +0 -259
  268. package/runtime/scripts/lib/example-refs.mjs +0 -158
  269. package/runtime/scripts/lib/json-schema.mjs +0 -52
  270. package/runtime/scripts/lib/process-identity.mjs +0 -6
  271. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  272. package/runtime/scripts/lib/redact.mjs +0 -161
  273. package/runtime/scripts/lib/sleep.mjs +0 -7
  274. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  275. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -13,18 +13,38 @@
13
13
  // and `starci app check` compares the files with this derivation. Nothing here lists a path of a product: it all comes from slots.yaml.
14
14
  import { braceVariants, globExpression } from '../lib/glob.mjs';
15
15
  import { logicRolesOf } from './manifest-shape.mjs';
16
+ import { byCodeUnit } from '../lib/list.mjs';
17
+ import { trimTrailingSlashes } from './trailing-slashes.mjs';
16
18
 
17
19
  /** The component that holds the shared infrastructure: the platform tier and every capability more than one service app composes. */
18
20
  export const PLATFORM_COMPONENT = 'platform';
19
21
  const SERVICE_APP_KINDS = new Set(['api', 'worker']);
20
- const star = (text) => String(text).replace(/<[^>]+>/g, '*');
21
- const segments = (dir) => dir.replace(/\/+$/, '').split('/').filter(Boolean);
22
+ /** `text` with every `<placeholder>` (a `<`, at least one character, the next `>`) replaced by `*`; a `<` with no closing `>` stays. */
23
+ export const star = (text) => {
24
+ const source = String(text);
25
+ let result = '';
26
+ let copied = 0;
27
+ let open = source.indexOf('<');
28
+ while (open !== -1) {
29
+ const close = source.indexOf('>', open + 1);
30
+ if (close === -1) break;
31
+ if (close === open + 1) {
32
+ open = source.indexOf('<', close);
33
+ continue;
34
+ }
35
+ result += `${source.slice(copied, open)}*`;
36
+ copied = close + 1;
37
+ open = source.indexOf('<', copied);
38
+ }
39
+ return result + source.slice(copied);
40
+ };
41
+ export const segments = (dir, trimSlashes = false) => (trimSlashes ? trimTrailingSlashes(dir) : dir).split('/').filter(Boolean);
22
42
  const isDir = (path) => path.endsWith('/');
23
43
 
24
44
  /** True when directory pattern `inner` lies inside `outer` (an outer `*` segment matches any one inner segment) or is the same. */
25
- function nested(inner, outer) {
26
- const a = segments(inner);
27
- const b = segments(outer);
45
+ export function nested(inner, outer) {
46
+ const a = segments(inner, true);
47
+ const b = segments(outer, true);
28
48
  return a.length >= b.length && b.every((segment, index) => segment === '*' ? true : segment === a[index]);
29
49
  }
30
50
 
@@ -37,6 +57,24 @@ const variantsOf = (slot) => braceVariants(slot.path).map(star);
37
57
  */
38
58
  const beSlots = (manifest, providers) => manifest.slots.filter((slot) => slot.profiles.includes('be') && slot.tracked === 'tracked' && (slot.provider === undefined || providers.includes(slot.provider)));
39
59
 
60
+ function addRequiredPatterns(sonar, required, suffixes) {
61
+ const escapedSuffixes = suffixes.map((suffix) => suffix.replaceAll('-', String.raw`\-`)).join('|');
62
+ const isRole = new RegExp(String.raw`\.(?:${escapedSuffixes})\.ts$`);
63
+ for (const slot of required) {
64
+ for (const entry of [...(slot.requires ?? []), ...(slot.allows ?? [])]) {
65
+ addEntryPatterns(sonar, slot, entry, isRole);
66
+ }
67
+ }
68
+ }
69
+
70
+ function addEntryPatterns(sonar, slot, entry, isRole) {
71
+ if (!entry.endsWith('.ts') || entry.includes('<role>')) return;
72
+ for (const name of braceVariants(entry)) {
73
+ if (isRole.test(star(name).replaceAll('*', 'x'))) continue;
74
+ for (const dir of variantsOf(slot)) sonar.add(`be/${dir}${star(name)}`.replace(/\/\/+/g, '/'));
75
+ }
76
+ }
77
+
40
78
  /** The directory a path pattern lives in (itself when it is one). */
41
79
  const dirOf = (path) => (isDir(path) ? path : path.slice(0, path.lastIndexOf('/') + 1));
42
80
 
@@ -64,31 +102,26 @@ export function coverageScope(manifest, providers = []) {
64
102
  if (!isDir(dir)) throw new Error(`slot ${slot.id} is coverage: required but its path ${slot.path} is not a directory`);
65
103
  return dir;
66
104
  }));
67
- const roots = minimal(requiredDirs).sort();
105
+ const roots = minimal(requiredDirs).sort(byCodeUnit);
68
106
  const nonePaths = none.flatMap((slot) => variantsOf(slot));
69
107
  for (const dir of nonePaths.filter(isDir)) if (requiredDirs.some((other) => nested(other, dir))) throw new Error(`a coverage: none directory ${dir} contains a coverage: required slot`);
70
108
  const noneUnique = [...new Set(nonePaths)];
71
109
  const noneMinimal = noneUnique.filter((path) => !noneUnique.some((other) => other !== path && isDir(other) && nested(dirOf(path), other)));
72
- const excludes = noneMinimal.filter((path) => isDir(path) && roots.some((root) => nested(path, root))).sort();
110
+ const excludes = noneMinimal.filter((path) => isDir(path) && roots.some((root) => nested(path, root))).sort(byCodeUnit);
73
111
  const suffixes = manifest.ruleParams.be.suffixes;
74
- const isRole = new RegExp(`\\.(?:${suffixes.map((s) => s.replace(/-/g, '\\-')).join('|')})\\.ts$`);
75
112
  const sonar = new Set(noneMinimal.map((path) => `be/${path}${isDir(path) ? '**' : ''}`));
76
113
  for (const role of suffixes.filter((suffix) => !roles.includes(suffix))) sonar.add(`be/**/*.${role}.ts`);
77
- for (const slot of required) {
78
- for (const entry of [...(slot.requires ?? []), ...(slot.allows ?? [])]) {
79
- if (!/\.ts$/.test(entry) || entry.includes('<role>')) continue;
80
- for (const name of braceVariants(entry)) {
81
- if (isRole.test(star(name).replace(/\*/g, 'x'))) continue;
82
- for (const dir of variantsOf(slot)) sonar.add(`be/${dir}${star(name)}`.replace(/\/\/+/g, '/'));
83
- }
84
- }
85
- }
114
+ addRequiredPatterns(sonar, required, suffixes);
86
115
  sonar.add('fe/**');
87
- return { roles, roots, excludes, none: noneMinimal.sort(), sonar: [...sonar].sort(), codecovPaths: roots.map((root) => `be/${root}**`) };
116
+ noneMinimal.sort(byCodeUnit);
117
+ return { roles, roots, excludes, none: noneMinimal, sonar: [...sonar].sort(byCodeUnit), codecovPaths: roots.map((root) => `be/${root}**`) };
88
118
  }
89
119
 
90
120
  /** The glob a root measures: every file of a logic role below it (`src/modules/domain/*\/**\/*.{service,policy}.ts`). */
91
- export const rootGlob = (root, roles) => `${root}**/*.${roles.length === 1 ? roles[0] : `{${roles.join(',')}}`}.ts`;
121
+ export const rootGlob = (root, roles) => {
122
+ const roleGlob = roles.length === 1 ? roles[0] : `{${roles.join(',')}}`;
123
+ return `${root}**/*.${roleGlob}.ts`;
124
+ };
92
125
 
93
126
  /** The jest coverage options of the rendered be/jest.config.js: { roots, roles, excludes }. */
94
127
  export function jestCoverage(manifest, providers = []) {
@@ -107,13 +140,17 @@ export const codecovPaths = (manifest, providers = []) => coverageScope(manifest
107
140
  * (`src/modules/domain/*\/` and `src/modules/domain/order/order.service.ts` give `order`).
108
141
  */
109
142
  function capabilityOf(root, file) {
110
- const parts = segments(root);
143
+ const parts = segments(root, true);
111
144
  const at = parts.length - 1;
112
145
  if (parts[at] !== '*') return null;
113
146
  const own = file.split('/');
114
147
  return parts.slice(0, at).every((part, index) => part === own[index]) && own.length > at + 1 ? own[at] : null;
115
148
  }
116
149
 
150
+ function escapeImportPart(part) {
151
+ return part.replace(/[/.]/g, String.raw`\$&`);
152
+ }
153
+
117
154
  /**
118
155
  * The Codecov components of an app, [{ id, name, paths }] (paths repository-relative from the app root): one per service app (api or worker)
119
156
  * that composes the capabilities nothing else composes, then `platform`. A capability of a root whose last segment is a placeholder
@@ -130,28 +167,37 @@ export function coverageComponents(manifest, { files, read, apps, providers = []
130
167
  const imports = new Map(services.map((app) => [app.name, files.filter((file) => file.startsWith(`apps/${app.name}/`) && /\.[cm]?ts$/.test(file)).map((file) => read(file)).join('\n')]));
131
168
  const platformPaths = [];
132
169
  const ownedBy = new Map(services.map((app) => [app.name, []]));
133
- for (const root of roots) {
134
- const module = /^src\/modules\/(.+)\/\*\/$/.exec(root)?.[1] ?? null;
135
- const capabilities = [...new Set(files.map((file) => capabilityOf(root, file)).filter(Boolean))].sort();
136
- const base = root.slice(0, -2);
137
- if (module === null || module === 'platform' || !capabilities.length) {
138
- platformPaths.push(`be/${root}**`);
139
- continue;
140
- }
141
- for (const capability of capabilities) {
142
- const spec = new RegExp(`from\\s+["']@modules/${module.replace(/[/.]/g, '\\$&')}/${capability.replace(/[/.]/g, '\\$&')}["']`);
143
- const importers = services.filter((app) => spec.test(imports.get(app.name)));
144
- const owners = importers.length === 1 ? importers : importers.filter((app) => app.name === capability);
145
- if (owners.length === 1) ownedBy.get(owners[0].name).push(`be/${base}${capability}/**`);
146
- else platformPaths.push(`be/${base}${capability}/**`);
147
- }
148
- }
170
+ for (const root of roots) assignRoot({ root, files, services, imports, platformPaths, ownedBy });
171
+ const components = services.filter((app) => ownedBy.get(app.name).length).map((app) => {
172
+ const paths = ownedBy.get(app.name);
173
+ paths.sort(byCodeUnit);
174
+ return { id: app.name, name: app.name, paths };
175
+ });
176
+ if (platformPaths.length) platformPaths.sort(byCodeUnit);
149
177
  return [
150
- ...services.filter((app) => ownedBy.get(app.name).length).map((app) => ({ id: app.name, name: app.name, paths: ownedBy.get(app.name).sort() })),
151
- ...(platformPaths.length ? [{ id: PLATFORM_COMPONENT, name: PLATFORM_COMPONENT, paths: platformPaths.sort() }] : []),
178
+ ...components,
179
+ ...(platformPaths.length ? [{ id: PLATFORM_COMPONENT, name: PLATFORM_COMPONENT, paths: platformPaths }] : []),
152
180
  ];
153
181
  }
154
182
 
183
+ /** Puts the measured root, or each capability below it, into the platform paths or the paths of the one service app that composes it. */
184
+ function assignRoot({ root, files, services, imports, platformPaths, ownedBy }) {
185
+ const module = /^src\/modules\/(.+)\/\*\/$/.exec(root)?.[1] ?? null;
186
+ const capabilities = [...new Set(files.map((file) => capabilityOf(root, file)).filter(Boolean))].sort(byCodeUnit);
187
+ const base = root.slice(0, -2);
188
+ if (module === null || module === 'platform' || !capabilities.length) {
189
+ platformPaths.push(`be/${root}**`);
190
+ return;
191
+ }
192
+ for (const capability of capabilities) {
193
+ const spec = new RegExp(String.raw`from\s+["']@modules/${escapeImportPart(module)}/${escapeImportPart(capability)}["']`);
194
+ const importers = services.filter((app) => spec.test(imports.get(app.name)));
195
+ const owners = importers.length === 1 ? importers : importers.filter((app) => app.name === capability);
196
+ if (owners.length === 1) ownedBy.get(owners[0].name).push(`be/${base}${capability}/**`);
197
+ else platformPaths.push(`be/${base}${capability}/**`);
198
+ }
199
+ }
200
+
155
201
  /** True when the be-relative `file` is measured: it is `<name>.<role>.ts` of a logic role below a measured root and inside no none directory of it. */
156
202
  export function isMeasured(manifest, file) {
157
203
  const { roots, roles, excludes } = coverageScope(manifest);
@@ -3,17 +3,88 @@
3
3
  import { isPlainObject } from '../../engine/plain-object.mjs';
4
4
  import { APP_KIND, connectionShapeProblems, NAME, RUNTIME_KIND, SLOT_ID } from './manifest-shape.mjs';
5
5
  import { kindShapeProblems, patternShapeProblems } from './declaration-slots.mjs';
6
+ import { byCodeUnit } from '../lib/list.mjs';
6
7
 
7
8
  /** Shape problems of the optional hfs.json `supabase` block whose auth posture DB_CONFIG_POLICY checks. */
9
+ const isSupabaseStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string' && entry.trim());
10
+
11
+ function supabaseValueProblems(block) {
12
+ const bad = [];
13
+ if (block.enableSignup !== undefined && typeof block.enableSignup !== 'boolean') bad.push('supabase.enableSignup must be true or false');
14
+ if (block.jwtExpiry !== undefined && !(Number.isInteger(block.jwtExpiry) && block.jwtExpiry > 0 && block.jwtExpiry <= 3600)) bad.push('supabase.jwtExpiry must be an integer of seconds, 3600 at most');
15
+ if (block.siteUrl !== undefined && (typeof block.siteUrl !== 'string' || !block.siteUrl.trim())) bad.push('supabase.siteUrl must be a URL string');
16
+ for (const key of ['redirectUrls', 'forceRls']) if (block[key] !== undefined && !isSupabaseStringList(block[key])) bad.push(`supabase.${key} must be a list of strings`);
17
+ return bad;
18
+ }
19
+
8
20
  function supabaseBlockProblems(block) {
9
21
  if (block === undefined) return [];
10
22
  if (!isPlainObject(block)) return ['supabase must be an object'];
11
23
  const bad = [];
12
24
  for (const key of Object.keys(block)) if (!['enableSignup', 'jwtExpiry', 'siteUrl', 'redirectUrls', 'forceRls'].includes(key)) bad.push(`supabase has unknown key ${key}`);
13
- if (block.enableSignup !== undefined && typeof block.enableSignup !== 'boolean') bad.push('supabase.enableSignup must be true or false');
14
- if (block.jwtExpiry !== undefined && !(Number.isInteger(block.jwtExpiry) && block.jwtExpiry > 0 && block.jwtExpiry <= 3600)) bad.push('supabase.jwtExpiry must be an integer of seconds, 3600 at most');
15
- if (block.siteUrl !== undefined && (typeof block.siteUrl !== 'string' || !block.siteUrl.trim())) bad.push('supabase.siteUrl must be a URL string');
16
- for (const key of ['redirectUrls', 'forceRls']) if (block[key] !== undefined && !(Array.isArray(block[key]) && block[key].every((value) => typeof value === 'string' && value.trim()))) bad.push(`supabase.${key} must be a list of strings`);
25
+ bad.push(...supabaseValueProblems(block));
26
+ return bad;
27
+ }
28
+
29
+ function appShapeProblems(apps, at, side, names) {
30
+ const bad = [];
31
+ if (!Array.isArray(apps) || !apps.length) {
32
+ bad.push(`${at}.apps must list every ${side}/apps/<name> with its kind`);
33
+ return bad;
34
+ }
35
+ apps.forEach((app, index) => {
36
+ if (!isPlainObject(app) || !NAME.test(String(app.name)) || !NAME.test(String(app.kind)) || Object.keys(app).some((key) => key !== 'name' && key !== 'kind')) bad.push(`${at}.apps[${index}] must be {name, kind}`);
37
+ else if (names.has(app.name)) bad.push(`app ${app.name} is declared twice (${names.get(app.name)} and ${side})`);
38
+ else names.set(app.name, side);
39
+ });
40
+ return bad;
41
+ }
42
+
43
+ function connectionProblems(side, value) {
44
+ const bad = [];
45
+ if (side !== 'be') {
46
+ bad.push('connections belong to the be side');
47
+ return bad;
48
+ }
49
+ const list = Array.isArray(value.connections) ? value.connections : null;
50
+ const shape = connectionShapeProblems(list, value.apps);
51
+ if (shape.length) {
52
+ bad.push(...shape);
53
+ return bad;
54
+ }
55
+ if (new Set(list.map((connection) => connection.name)).size !== list.length) bad.push('connections names must be unique');
56
+ for (const a of list) for (const b of list) if (a !== b && `${b.envPrefix}_`.startsWith(`${a.envPrefix}_`)) bad.push(`connections ${a.name} and ${b.name} share env keys (${a.envPrefix}_ covers ${b.envPrefix}_)`);
57
+ return bad;
58
+ }
59
+
60
+ /** Whether a declared list is present but not a list of distinct entries that all satisfy `valid`. */
61
+ const isBadUniqueList = (list, valid) => list !== undefined && (!Array.isArray(list) || !list.every((entry) => valid(entry)) || new Set(list).size !== list.length);
62
+
63
+ function declarationSideProblems(side, value, names) {
64
+ const bad = [];
65
+ const at = `sides.${side}`;
66
+ if (!isPlainObject(value)) return [`${at} must be an object`];
67
+ for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads'].includes(key)) bad.push(`${at} has unknown key ${key}`);
68
+ bad.push(...appShapeProblems(value.apps, at, side, names));
69
+ if (isBadUniqueList(value.optionalSlots, (slot) => SLOT_ID.test(String(slot)))) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
70
+ bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
71
+ if (isBadUniqueList(value.reads, (read) => typeof read === 'string' && read.length > 0)) bad.push(`${at}.reads must be a unique list of paths`);
72
+ if (value.connections !== undefined) bad.push(...connectionProblems(side, value));
73
+ return bad;
74
+ }
75
+
76
+ const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'supabase', 'sides', 'browser']);
77
+ const RUNTIME_DECLARATION_KEYS = new Set(['hfs', 'kind', 'project']);
78
+
79
+ function productDeclarationProblems(declaration, profiles) {
80
+ const bad = [];
81
+ for (const key of Object.keys(declaration)) if (!DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key}`);
82
+ bad.push(...supabaseBlockProblems(declaration.supabase));
83
+ if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
84
+ if (declaration.kind !== APP_KIND) bad.push(`kind must be ${APP_KIND} (a product is one app repository with a be and an fe side) or ${RUNTIME_KIND} (the StarCi runtime repository)`);
85
+ if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort(byCodeUnit).join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
86
+ const names = new Map();
87
+ for (const side of profiles) bad.push(...declarationSideProblems(side, declaration.sides[side], names));
17
88
  return bad;
18
89
  }
19
90
 
@@ -24,39 +95,9 @@ export function declarationShapeProblems(declaration, profiles) {
24
95
  if (!(Number.isInteger(declaration.hfs) && declaration.hfs >= 1)) bad.push('hfs must be the pinned manifest major (an integer, 1 or more)');
25
96
  if (!NAME.test(String(declaration.project))) bad.push('project must be a project name');
26
97
  if (declaration.kind === RUNTIME_KIND) {
27
- for (const key of Object.keys(declaration)) if (!['hfs', 'kind', 'project'].includes(key)) bad.push(`unknown key ${key} (a runtime declaration is {hfs, kind, project})`);
98
+ for (const key of Object.keys(declaration)) if (!RUNTIME_DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key} (a runtime declaration is {hfs, kind, project})`);
28
99
  return bad;
29
100
  }
30
- for (const key of Object.keys(declaration)) if (!['hfs', 'kind', 'project', 'edition', 'supabase', 'sides', 'browser'].includes(key)) bad.push(`unknown key ${key}`);
31
- bad.push(...supabaseBlockProblems(declaration.supabase));
32
- if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
33
- if (declaration.kind !== APP_KIND) bad.push(`kind must be ${APP_KIND} (a product is one app repository with a be and an fe side) or ${RUNTIME_KIND} (the StarCi runtime repository)`);
34
- if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort().join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
35
- const names = new Map();
36
- for (const side of profiles) {
37
- const value = declaration.sides[side];
38
- const at = `sides.${side}`;
39
- if (!isPlainObject(value)) { bad.push(`${at} must be an object`); continue; }
40
- for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads'].includes(key)) bad.push(`${at} has unknown key ${key}`);
41
- if (!Array.isArray(value.apps) || !value.apps.length) bad.push(`${at}.apps must list every ${side}/apps/<name> with its kind`);
42
- else value.apps.forEach((app, index) => {
43
- if (!isPlainObject(app) || !NAME.test(String(app.name)) || !NAME.test(String(app.kind)) || Object.keys(app).some((key) => key !== 'name' && key !== 'kind')) bad.push(`${at}.apps[${index}] must be {name, kind}`);
44
- else if (names.has(app.name)) bad.push(`app ${app.name} is declared twice (${names.get(app.name)} and ${side})`);
45
- else names.set(app.name, side);
46
- });
47
- if (value.optionalSlots !== undefined && (!Array.isArray(value.optionalSlots) || !value.optionalSlots.every((slot) => SLOT_ID.test(String(slot))) || new Set(value.optionalSlots).size !== value.optionalSlots.length)) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
48
- bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
49
- if (value.reads !== undefined && (!Array.isArray(value.reads) || !value.reads.every((read) => typeof read === 'string' && read.length > 0) || new Set(value.reads).size !== value.reads.length)) bad.push(`${at}.reads must be a unique list of paths`);
50
- if (value.connections !== undefined) {
51
- if (side !== 'be') { bad.push('connections belong to the be side'); continue; }
52
- const list = Array.isArray(value.connections) ? value.connections : null;
53
- const shape = connectionShapeProblems(list, value.apps);
54
- if (shape.length) bad.push(...shape);
55
- else {
56
- if (new Set(list.map((connection) => connection.name)).size !== list.length) bad.push('connections names must be unique');
57
- for (const a of list) for (const b of list) if (a !== b && `${b.envPrefix}_`.startsWith(`${a.envPrefix}_`)) bad.push(`connections ${a.name} and ${b.name} share env keys (${a.envPrefix}_ covers ${b.envPrefix}_)`);
58
- }
59
- }
60
- }
101
+ bad.push(...productDeclarationProblems(declaration, profiles));
61
102
  return bad;
62
103
  }
@@ -8,7 +8,7 @@ export function optionalSlotProblems(manifest, side, s) {
8
8
  const bad = [];
9
9
  for (const id of s.optionalSlots ?? []) {
10
10
  const slot = manifest.slots.find((candidate) => candidate.id === id);
11
- if (!slot || !slot.profiles.includes(side)) bad.push(`sides.${side}.optionalSlots names ${id}, which is not a ${side} slot`);
11
+ if (!slot?.profiles.includes(side)) bad.push(`sides.${side}.optionalSlots names ${id}, which is not a ${side} slot`);
12
12
  else if (slot.presence !== 'opt-in') bad.push(`sides.${side}.optionalSlots names ${id}, which is ${slot.presence}, not opt-in`);
13
13
  else if (slot.appKind !== undefined) bad.push(`sides.${side}.optionalSlots names ${id}; an app of kind ${slot.appKind} enables it`);
14
14
  }
@@ -20,8 +20,10 @@ export function effectiveSlot(slot, profile, edition) {
20
20
  if (edition !== 'lite') return slot;
21
21
  const { lite, litePresence, ...base } = slot;
22
22
  const presence = litePresenceOf(slot, profile);
23
- const tracked = presence === 'forbidden' ? 'external' : (slot.tracked === 'external' ? 'tracked' : slot.tracked);
24
- return { ...base, ...(lite ?? {}), presence, tracked, tests: 'none' };
23
+ let tracked = slot.tracked;
24
+ if (presence === 'forbidden') tracked = 'external';
25
+ else if (tracked === 'external') tracked = 'tracked';
26
+ return { ...base, ...lite, presence, tracked, tests: 'none' };
25
27
  }
26
28
 
27
29
  /** The template group starci app sync renders `slot` from under `edition`: `liteManagedBy` (else `managedBy`) in lite, `managedBy` otherwise; undefined when unmanaged. */
@@ -61,6 +63,6 @@ export function enforcerJudgedInEdition(rules, kind, id, edition = 'full') {
61
63
  export function editionRuleParams(params, edition) {
62
64
  const { lite, ...base } = params;
63
65
  const merged = edition === 'lite' && lite ? { ...base, ...lite } : base;
64
- const deepFreeze = (value) => { if (value && typeof value === 'object') Object.values(value).forEach(deepFreeze); return Object.freeze(value); };
66
+ const deepFreeze = (value) => { if (value && typeof value === 'object') { Object.values(value).forEach(deepFreeze); } return Object.freeze(value); };
65
67
  return deepFreeze(structuredClone(merged));
66
68
  }
@@ -0,0 +1,30 @@
1
+ // Linear-time text trimming and `<placeholder>` replacement for the HFS path and prose checks.
2
+
3
+ /** `text` without its trailing characters that are in `chars`. */
4
+ export function trimTrailingChars(text, chars) {
5
+ let last = text.length - 1;
6
+ while (last >= 0 && chars.includes(text[last])) last -= 1;
7
+ return text.slice(0, last + 1);
8
+ }
9
+
10
+ /** `text` with every `<name>` placeholder (a `<`, one or more characters other than `>`, a `>`) replaced by `replacement`. */
11
+ export function replacePlaceholders(text, replacement) {
12
+ let out = '';
13
+ let from = 0;
14
+ let open = text.indexOf('<');
15
+ while (open >= 0) {
16
+ const close = text.indexOf('>', open + 1);
17
+ if (close < 0) break;
18
+ if (close === open + 1) {
19
+ open = text.indexOf('<', close);
20
+ } else {
21
+ out += text.slice(from, open) + replacement;
22
+ from = close + 1;
23
+ open = text.indexOf('<', from);
24
+ }
25
+ }
26
+ return out + text.slice(from);
27
+ }
28
+
29
+ /** Whether `text` holds a `<name>` placeholder. */
30
+ export const hasPlaceholder = (text) => replacePlaceholders(text, '') !== text;