@starci/hfs 4.0.10 → 4.1.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 (271) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +3 -2
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +187 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +221 -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/removed-vocabulary.mjs +28 -0
  14. package/runtime/engine/resources-config.mjs +52 -0
  15. package/runtime/engine/runtime-root.mjs +17 -0
  16. package/runtime/engine/secrets.mjs +35 -31
  17. package/runtime/engine/sonar-config.mjs +21 -0
  18. package/runtime/engine/temp-root.mjs +32 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  20. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  21. package/runtime/knowledge/hfs/rules.yaml +98 -65
  22. package/runtime/knowledge/hfs/slots.yaml +11 -14
  23. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  24. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  25. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  26. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  27. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +92 -256
  31. package/runtime/modules/models/tiers.yaml +65 -0
  32. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  33. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  34. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  35. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  36. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  37. package/runtime/scripts/api/git/lib.mjs +34 -1
  38. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  39. package/runtime/scripts/api/process/run-program.mjs +2 -1
  40. package/runtime/scripts/api/sops/lib.mjs +198 -120
  41. package/runtime/scripts/hfs/allows.mjs +3 -3
  42. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  43. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  44. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  45. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  47. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  48. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  49. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  50. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  51. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  52. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  53. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  54. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  55. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  56. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  57. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  58. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  59. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  60. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  61. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  62. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  63. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  65. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  66. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  67. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  69. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  70. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  71. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  72. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  74. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  75. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  76. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  77. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  78. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  79. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  80. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  81. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  82. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  83. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  84. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  85. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  86. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  87. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  88. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  89. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  90. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  91. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  92. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  93. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  94. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  95. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  96. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  97. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  98. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  99. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  100. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  101. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  102. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  103. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  104. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  105. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  113. package/runtime/scripts/hfs/architecture.mjs +3 -4
  114. package/runtime/scripts/hfs/check.mjs +61 -70
  115. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  116. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  117. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  118. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  119. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  120. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  121. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  122. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  123. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  124. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  125. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  126. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  127. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  128. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  129. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  130. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  131. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  132. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  133. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  134. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  135. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  136. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  137. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  138. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  139. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  140. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  141. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  142. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  143. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  144. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  145. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  146. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  147. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  148. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  149. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  150. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  151. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  152. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  153. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  154. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  155. package/runtime/scripts/hfs/secret.mjs +59 -48
  156. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  157. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  158. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  159. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  160. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  161. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  162. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  163. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  165. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  166. package/runtime/scripts/hfs/slots.mjs +72 -693
  167. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  168. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  169. package/runtime/scripts/hfs/tree.mjs +19 -14
  170. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  171. package/runtime/scripts/hfs/view.mjs +1 -1
  172. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  173. package/runtime/scripts/lib/event-contract.mjs +17 -13
  174. package/runtime/scripts/lib/git.mjs +2 -2
  175. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  176. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  177. package/runtime/scripts/lib/i18n.mjs +3 -2
  178. package/runtime/scripts/lib/in-order.mjs +71 -0
  179. package/runtime/scripts/lib/language.mjs +3 -3
  180. package/runtime/scripts/lib/list.mjs +8 -1
  181. package/runtime/scripts/lib/path-key.mjs +38 -5
  182. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  183. package/runtime/scripts/lib/regex.mjs +1 -1
  184. package/runtime/scripts/lib/same-text.mjs +1 -1
  185. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  186. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  187. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  188. package/runtime/scripts/lib/stack-services.mjs +1 -2
  189. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  190. package/runtime/scripts/lib/walk.mjs +6 -1
  191. package/scaffold/add-cli-lite.mjs +2 -1
  192. package/scaffold/add-table.mjs +2 -2
  193. package/scaffold/add.mjs +3 -2
  194. package/scaffold/app.mjs +4 -2
  195. package/scaffold/edition-gate.mjs +25 -25
  196. package/scaffold/lite-exports.mjs +2 -1
  197. package/scaffold/service.mjs +5 -4
  198. package/sync/hygiene.mjs +15 -9
  199. package/sync/index.mjs +7 -3
  200. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  201. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  202. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  203. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  204. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  205. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  206. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  207. package/templates/app/starciwork.gitignore +1 -1
  208. package/templates/be/image/api/Dockerfile +1 -1
  209. package/templates/be/image/cli/Dockerfile +1 -1
  210. package/templates/be/image/worker/Dockerfile +1 -1
  211. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  212. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  213. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  214. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  215. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  216. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  217. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  218. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  219. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  220. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  221. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  222. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  223. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  224. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  225. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  226. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  227. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  228. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  229. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  230. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  231. package/templates/fe/image/next/Dockerfile +2 -1
  232. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  233. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  234. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  235. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  236. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  238. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  239. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  241. package/upgrade/index.mjs +9 -2
  242. package/runtime/engine/admission.mjs +0 -308
  243. package/runtime/engine/canonical-json.mjs +0 -10
  244. package/runtime/engine/db/blob.mjs +0 -315
  245. package/runtime/engine/db/ledger-paths.mjs +0 -83
  246. package/runtime/engine/db/ledger.mjs +0 -1118
  247. package/runtime/engine/db/machine-connection.mjs +0 -135
  248. package/runtime/engine/db/machine-schema.mjs +0 -84
  249. package/runtime/engine/db/machine.mjs +0 -1367
  250. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  251. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  252. package/runtime/engine/db/provider-reservations.mjs +0 -101
  253. package/runtime/engine/digest.mjs +0 -16
  254. package/runtime/engine/refuse.mjs +0 -11
  255. package/runtime/modules/ops/_labels.yaml +0 -53
  256. package/runtime/scripts/api/node/lib.mjs +0 -14
  257. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  258. package/runtime/scripts/api/process/lib.mjs +0 -111
  259. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  260. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  261. package/runtime/scripts/connectors/lib.mjs +0 -488
  262. package/runtime/scripts/lib/clip.mjs +0 -19
  263. package/runtime/scripts/lib/display-names.mjs +0 -259
  264. package/runtime/scripts/lib/example-refs.mjs +0 -158
  265. package/runtime/scripts/lib/json-schema.mjs +0 -52
  266. package/runtime/scripts/lib/process-identity.mjs +0 -6
  267. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  268. package/runtime/scripts/lib/redact.mjs +0 -161
  269. package/runtime/scripts/lib/sleep.mjs +0 -7
  270. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  271. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -0,0 +1,65 @@
1
+ // slot-classify.mjs - what slot of one scope (the app root or one side) owns a path: its status, owner unit, tier, folder kind and role.
2
+ // A scope is {manifest, repo, profile, edition, slots, byId, variants, appKind} built once by slots.mjs; every function here reads it.
3
+ import { globExpression } from '../lib/glob.mjs';
4
+ import { declaredSlotEnabled } from './declaration-slots.mjs';
5
+ import { bestMatch, fillVars, matchVariants, nearestSlot } from './slot-match.mjs';
6
+ import { cleanSlotPath } from './slot-path.mjs';
7
+
8
+ /** Whether the repository of the scope holds the slot. */
9
+ export function slotEnabled(scope, slot) {
10
+ // A provider slot is gated by its provider in every edition: litePresence may lift it to required, but it still
11
+ // exists for this repository only while a connection declares the provider.
12
+ if (slot.provider !== undefined) return declaredSlotEnabled(slot, scope.repo);
13
+ if (slot.presence !== 'opt-in') return true;
14
+ return declaredSlotEnabled(slot, scope.repo);
15
+ }
16
+
17
+ /** The folder kind of a file in its slot, from a named layer or kind folder. */
18
+ function kindOf({ variant, slot, root }, p) {
19
+ const names = [...(slot.layers ?? []), ...(slot.kinds ?? [])];
20
+ if (!names.length) return null;
21
+ const literal = variant.segments.find((segment) => names.includes(segment));
22
+ if (literal) return literal;
23
+ const below = root ? p.slice(root.length + 1) : p;
24
+ return below.split('/').slice(0, -1).find((segment) => names.includes(segment)) ?? null;
25
+ }
26
+
27
+ /** The role of a file in its slot: the entry of `roles` whose file name (variables filled from the path, `*` a wildcard inside the name) matches the file's name. */
28
+ function roleOf(slot, bindings, p) {
29
+ const name = p.split('/').pop();
30
+ for (const [role, file] of Object.entries(slot.roles ?? {})) if (globExpression(fillVars(file, bindings)).test(name)) return role;
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * status: owned | forbidden (external slot) | not-enabled (opt-in slot the repository did not declare) | ambiguous
36
+ * (two slots of equal specificity; a manifest gap) | no-slot (code HFS_SLOT_UNDECLARED, with the nearest slot).
37
+ */
38
+ export function classifyIn(scope, input) {
39
+ const p = cleanSlotPath(input);
40
+ const { hit, ambiguous } = bestMatch(matchVariants(scope, p));
41
+ if (ambiguous.length) return { path: p, status: 'ambiguous', candidates: ambiguous };
42
+ if (!hit) return { path: p, status: 'no-slot', code: 'HFS_SLOT_UNDECLARED', nearest: nearestSlot(scope.variants, p) };
43
+ const { slot, root, bindings } = hit;
44
+ const status = (slot.presence === 'forbidden' && 'forbidden') || (slotEnabled(scope, slot) ? 'owned' : 'not-enabled');
45
+ const kind = kindOf(hit, p);
46
+ const role = roleOf(slot, bindings, p);
47
+ return { path: p, status, slot: slot.id, root, bindings, ...(kind ? { kind } : {}), ...(role ? { role } : {}), presence: slot.presence, tracking: slot.tracked, ...(status === 'forbidden' ? { goesTo: slot.goesTo } : {}) };
48
+ }
49
+
50
+ /** The owner unit of `p`: the most specific owner slot instance containing it (a slot's own root when it owns nothing). */
51
+ export function ownerIn(scope, input) {
52
+ const p = cleanSlotPath(input);
53
+ const { hit } = bestMatch(matchVariants(scope, p, (s) => s.owner === true));
54
+ return hit ? { slot: hit.slot.id, root: hit.root, bindings: hit.bindings } : null;
55
+ }
56
+
57
+ /** The tier of `p` in the direction matrix: its slot's tier, the owner's for an inheriting slot, none for an untiered one; null when no slot owns it. */
58
+ export function tierIn(scope, input) {
59
+ const c = classifyIn(scope, input);
60
+ if (!c.slot) return null;
61
+ const tier = scope.byId.get(c.slot).tier;
62
+ if (tier !== 'inherit') return tier;
63
+ const owner = ownerIn(scope, input);
64
+ return owner ? scope.byId.get(owner.slot).tier : 'none';
65
+ }
@@ -0,0 +1,11 @@
1
+ // slot-errors.mjs - the shared refusal type for HFS manifest and declaration loaders.
2
+ export class HfsSlotsError extends Error {
3
+ constructor(code, message, details = {}) {
4
+ super(`${code}: ${message}`);
5
+ this.name = 'HfsSlotsError';
6
+ this.code = code;
7
+ this.details = details;
8
+ }
9
+ }
10
+
11
+ export const fail = (code, message, details) => { throw new HfsSlotsError(code, message, details); };
@@ -0,0 +1,76 @@
1
+ // slot-imports.mjs - whether one file of a scope (the app root or one side) may import another: the tier direction matrix, the
2
+ // owner units, the layer order and the public entry of an owner. Cycles are a graph property of the architecture check, not of one edge.
3
+ import { RUNTIME_KIND } from './manifest-shape.mjs';
4
+ import { classifyIn, ownerIn, tierIn } from './slot-classify.mjs';
5
+
6
+ const isEntryFile = (name) => name === 'index.ts' || name === 'index.tsx';
7
+
8
+ function unownedPathProblem(from, to) {
9
+ for (const side of [from, to]) {
10
+ if (side.status === 'no-slot') return { allowed: false, reason: 'unowned', code: side.code, path: side.path, nearest: side.nearest };
11
+ }
12
+ return null;
13
+ }
14
+
15
+ function unavailableSlotProblem(from, to) {
16
+ for (const side of [from, to]) {
17
+ if (side.status !== 'owned') return { allowed: false, reason: `slot${side.status[0].toUpperCase()}${side.status.slice(1).replace(/-(.)/g, (_, c) => c.toUpperCase())}`, path: side.path, slot: side.slot };
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function sameOwnerUnit(side, owner) {
23
+ return owner ? `${owner.slot}:${owner.root}` : `${side.slot}:${side.root}`;
24
+ }
25
+
26
+ const layerIndex = (slot, root) => (slot.layers ? slot.layers.findIndex((layer) => root.split('/').includes(layer)) : -1);
27
+
28
+ function lowerLayerProblem({ manifest, profile, byId }, from, to, fromTier, toTier, fromOwner, toOwner) {
29
+ if (manifest.tiers[profile][fromTier].lowerLayerOnly && fromTier === toTier) {
30
+ const fromSlot = byId.get((fromOwner ?? from).slot);
31
+ const a = layerIndex(fromSlot, fromOwner?.root ?? from.root);
32
+ const b = layerIndex(byId.get((toOwner ?? to).slot), toOwner?.root ?? to.root);
33
+ if (a >= 0 && b >= 0 && b <= a) return { allowed: false, reason: 'layerOrder', fromLayer: fromSlot.layers[a], toLayer: fromSlot.layers[b] };
34
+ }
35
+ return null;
36
+ }
37
+
38
+ function privateEntryProblem({ repo, byId }, to, toOwner) {
39
+ if (toOwner && repo.kind !== RUNTIME_KIND) {
40
+ const relative = to.path === toOwner.root ? '' : to.path.slice(toOwner.root.length + 1);
41
+ const ownerSlot = byId.get(toOwner.slot);
42
+ const entry = isEntryFile(relative) || (byId.get(to.slot).entries ?? []).includes(to.path.slice(to.root.length + 1)) || (ownerSlot.tier === 'package' && relative === 'src/index.ts') || (ownerSlot.tier === 'app' && relative === 'app.module.ts');
43
+ if (!entry) return { allowed: false, reason: 'notPublicEntry', owner: toOwner.root, path: to.path };
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * Whether `fromPath` may import `toPath`: {allowed, reason, ...}. Reasons: sameOwner, untiered, crossApp,
50
+ * tierDirection, layerOrder, notPublicEntry, allowed, slotForbidden, slotNotEnabled, slotAmbiguous, and unowned (HFS_SLOT_UNDECLARED for the path no slot owns).
51
+ * Cycles are a graph property and belong to the architecture check, not to one edge.
52
+ */
53
+ export function importAllowedIn(scope, fromPath, toPath) {
54
+ const from = classifyIn(scope, fromPath);
55
+ const to = classifyIn(scope, toPath);
56
+ const unowned = unownedPathProblem(from, to);
57
+ if (unowned) return unowned;
58
+ const unavailable = unavailableSlotProblem(from, to);
59
+ if (unavailable) return unavailable;
60
+ const fromTier = tierIn(scope, from.path);
61
+ const toTier = tierIn(scope, to.path);
62
+ if (fromTier === 'none' || toTier === 'none') return { allowed: true, reason: 'untiered' };
63
+ if (from.bindings.app !== undefined && to.bindings.app !== undefined && from.bindings.app !== to.bindings.app)
64
+ return { allowed: false, reason: 'crossApp', from: from.bindings.app, to: to.bindings.app };
65
+ const fromOwner = ownerIn(scope, from.path);
66
+ const toOwner = ownerIn(scope, to.path);
67
+ if (sameOwnerUnit(from, fromOwner) === sameOwnerUnit(to, toOwner)) return { allowed: true, reason: 'sameOwner' };
68
+ const { mayImport } = scope.manifest.tiers[scope.profile][fromTier] ?? {};
69
+ if (!mayImport?.includes(toTier)) return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport: mayImport ?? [] };
70
+ const layerProblem = lowerLayerProblem(scope, from, to, fromTier, toTier, fromOwner, toOwner);
71
+ if (layerProblem) return layerProblem;
72
+ // A runtime owner is imported file by file (crossOwner of knowledge/hfs/runtime-slots.yaml): no public entry.
73
+ const privateEntry = privateEntryProblem(scope, to, toOwner);
74
+ if (privateEntry) return privateEntry;
75
+ return { allowed: true, reason: 'allowed', fromTier, toTier };
76
+ }
@@ -0,0 +1,73 @@
1
+ // slot-manifest-shape.mjs - the product slot manifest's schema-shaped validation.
2
+ import { isPlainObject } from '../../engine/plain-object.mjs';
3
+ import { byCodeUnit } from '../lib/list.mjs';
4
+ import { ruleParamsProblems } from './rule-params-shape.mjs';
5
+ import { APP_KIND, EDITIONS, MANIFEST_KINDS, NAME, PRESENCE, RUNTIME_KIND, SEMVER, TESTS, TRACKED, manifestKind, runtimeShapeProblems, slotProblems, tierMapProblems } from './manifest-shape.mjs';
6
+
7
+ export const PROFILES = ['be', 'fe'];
8
+ export const APP_SCOPE = 'app';
9
+ export const SCOPES = [APP_SCOPE, ...PROFILES];
10
+
11
+ /** Shape problems of a parsed manifest, in the words of modules/schemas/hfs-slots.schema.yaml. */
12
+ function checkCoreManifestFields(m, bad) {
13
+ const allowed = new Set(['schema', 'kind', 'version', 'versioning', 'presenceValues', 'trackedValues', 'testValues', 'editions', 'sides', 'appKinds', 'triggerKinds', 'tiers', 'ruleParams', 'crossOwner', 'crossApp', 'slots', 'consumers', 'naming']);
14
+ for (const key of Object.keys(m)) if (!allowed.has(key)) bad.push(`unknown top-level key ${key}`);
15
+ if (!/^starci\/hfs-slots@\d+$/.test(String(m.schema))) bad.push('schema must be starci/hfs-slots@<major>');
16
+ if (!SEMVER.test(String(m.version))) bad.push('version must be MAJOR.MINOR.PATCH');
17
+ if (!isPlainObject(m.versioning) || !['patch', 'minor', 'major', 'pins'].every((k) => typeof m.versioning[k] === 'string')) bad.push('versioning needs patch, minor, major and pins text');
18
+ if (JSON.stringify(m.presenceValues) !== JSON.stringify(PRESENCE)) bad.push(`presenceValues must be ${PRESENCE.join(', ')}`);
19
+ if (JSON.stringify(m.trackedValues) !== JSON.stringify(TRACKED)) bad.push(`trackedValues must be ${TRACKED.join(', ')}`);
20
+ if (JSON.stringify(m.testValues) !== JSON.stringify(TESTS)) bad.push(`testValues must be ${TESTS.join(', ')}`);
21
+ if (JSON.stringify(m.editions) !== JSON.stringify(EDITIONS)) bad.push(`editions must be ${EDITIONS.join(', ')}`);
22
+ }
23
+
24
+ function checkSideShapes(m, bad) {
25
+ if (!isPlainObject(m.sides) || Object.keys(m.sides).sort(byCodeUnit).join() !== PROFILES.join()) bad.push('sides must be a map with exactly be and fe');
26
+ else for (const side of PROFILES) {
27
+ const def = m.sides[side];
28
+ if (!isPlainObject(def) || Object.keys(def).join() !== 'reads' || !Array.isArray(def.reads) || !def.reads.every((r) => typeof r === 'string' && /^[a-z][a-z0-9-]*\/([^/]+\/)+$/.test(r)) || new Set(def.reads).size !== def.reads.length) bad.push(`sides.${side} must be {reads: [unique <owner>/<dir>/ paths]}`);
29
+ }
30
+ }
31
+
32
+ function checkTriggerKinds(m, bad) {
33
+ if (m.triggerKinds !== undefined && (!Array.isArray(m.triggerKinds) || !m.triggerKinds.length || !m.triggerKinds.every((k) => NAME.test(String(k))) || new Set(m.triggerKinds).size !== m.triggerKinds.length)) bad.push('triggerKinds must be a non-empty list of unique names');
34
+ }
35
+
36
+ function checkProfileMaps(m, bad) {
37
+ for (const key of ['appKinds', 'tiers']) {
38
+ if (!isPlainObject(m[key])) { bad.push(`${key} must be a map with be and fe`); continue; }
39
+ for (const extra of Object.keys(m[key])) if (!PROFILES.includes(extra)) bad.push(`${key}.${extra} is not a profile`);
40
+ for (const profile of PROFILES) if (!(profile in m[key])) bad.push(`${key}.${profile} is missing`);
41
+ }
42
+ }
43
+
44
+ function checkAppKindAndTierShapes(m, bad) {
45
+ for (const profile of PROFILES) {
46
+ const kinds = m.appKinds?.[profile];
47
+ if (kinds !== undefined && (!Array.isArray(kinds) || !kinds.length || !kinds.every((k) => NAME.test(String(k))) || new Set(kinds).size !== kinds.length)) bad.push(`appKinds.${profile} must be a non-empty list of unique names`);
48
+ const tiers = m.tiers?.[profile];
49
+ if (tiers === undefined) continue;
50
+ bad.push(...tierMapProblems(profile, tiers));
51
+ }
52
+ }
53
+
54
+ function checkSlotShapes(m, bad) {
55
+ if (!Array.isArray(m.slots) || !m.slots.length) { bad.push('slots must be a non-empty list'); return false; }
56
+ m.slots.forEach((slot, index) => bad.push(...slotProblems(slot, index, APP_KIND, { appScope: APP_SCOPE, scopes: SCOPES })));
57
+ return true;
58
+ }
59
+
60
+ export function manifestShapeProblems(m) {
61
+ const bad = [];
62
+ if (!isPlainObject(m)) return ['the manifest is not a map'];
63
+ if (!MANIFEST_KINDS.includes(manifestKind(m))) return [`kind must be one of ${MANIFEST_KINDS.join(', ')}`];
64
+ if (manifestKind(m) === RUNTIME_KIND) return runtimeShapeProblems(m);
65
+ checkCoreManifestFields(m, bad);
66
+ checkSideShapes(m, bad);
67
+ checkTriggerKinds(m, bad);
68
+ checkProfileMaps(m, bad);
69
+ checkAppKindAndTierShapes(m, bad);
70
+ bad.push(...ruleParamsProblems(m));
71
+ if (!checkSlotShapes(m, bad)) return bad;
72
+ return bad;
73
+ }
@@ -0,0 +1,153 @@
1
+ // slot-match.mjs - the slot path patterns of the HFS manifest compiled to regular expressions, and the matching of a path against them:
2
+ // the variants that match it, the most specific one, and the nearest slot of an unknown path. Pure; slot-classify.mjs asks it.
3
+ import path from 'node:path';
4
+ import { braceVariants } from '../lib/glob.mjs';
5
+ import { captureNames } from '../lib/i18n.mjs';
6
+
7
+ const VAR = /<([A-Za-z][A-Za-z0-9-]*)>/g;
8
+ export const varsOf = (text) => captureNames(text, VAR);
9
+ const hasWildcard = (segment) => /[*?]/.test(segment);
10
+
11
+ /** `text` with the `<name>` variables filled from `bindings` (an unbound one stays as written). */
12
+ export function fillVars(text, bindings) {
13
+ const bound = (whole, name) => bindings[name] ?? whole;
14
+ return `${text}`.replace(VAR, bound);
15
+ }
16
+
17
+ /** Weight of one pattern segment: literal 4, literal mixed with a variable 3, a bare variable 2, a wildcard 1, `**` 0. */
18
+ function segmentWeight(segment) {
19
+ if (segment === '**') return 0;
20
+ if (hasWildcard(segment)) return 1;
21
+ if (/^<[^>]+>$/.test(segment)) return 2;
22
+ if (segment.includes('<')) return 3;
23
+ return 4;
24
+ }
25
+
26
+ /** The RegExp source of one pattern segment (no separators); variables capture, and are listed in `names`. */
27
+ function segmentSource(segment, names) {
28
+ let source = '', variableEnd = -1;
29
+ for (let i = 0; i < segment.length; i += 1) {
30
+ const c = segment[i];
31
+ if (i <= variableEnd) continue;
32
+ if (c === '<') {
33
+ variableEnd = segment.indexOf('>', i);
34
+ names.push(segment.slice(i + 1, variableEnd));
35
+ source += '([^/]+?)';
36
+ } else if (c === '*') source += '[^/]*';
37
+ else if (c === '?') source += '[^/]';
38
+ else source += /[.+^${}()|[\]\\]/.test(c) ? `\\${c}` : c;
39
+ }
40
+ return source;
41
+ }
42
+
43
+ /** One brace-free pattern compiled: `dir` patterns (trailing /) own everything below their root. */
44
+ export function compileVariant(slot, pattern) {
45
+ const dir = pattern.endsWith('/');
46
+ const body = dir ? pattern.slice(0, -1) : pattern;
47
+ const segments = body.split('/');
48
+ const names = [];
49
+ let source = '';
50
+ segments.forEach((segment, index) => {
51
+ const last = index === segments.length - 1;
52
+ if (segment === '**') source += last ? '.*' : '(?:.*/)?';
53
+ else source += segmentSource(segment, names) + (last ? '' : '/');
54
+ });
55
+ return {
56
+ slot,
57
+ pattern,
58
+ dir,
59
+ segments,
60
+ names,
61
+ score: segments.reduce((sum, s) => sum + segmentWeight(s), 0),
62
+ wildcards: segments.filter(hasWildcard).length,
63
+ regex: new RegExp(`^(${source})${dir ? '(?:/.*)?' : ''}$`),
64
+ segmentRegexes: segments.map((s) => (s === '**' ? null : new RegExp(`^${segmentSource(s, [])}$`))),
65
+ };
66
+ }
67
+
68
+ export const variantsOf = (slot) => braceVariants(slot.path).map((pattern) => compileVariant(slot, pattern));
69
+
70
+ const levenshtein = (a, b) => {
71
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
72
+ for (let i = 1; i <= a.length; i += 1) {
73
+ let prev = row[0];
74
+ row[0] = i;
75
+ for (let j = 1; j <= b.length; j += 1) {
76
+ const held = row[j];
77
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
78
+ prev = held;
79
+ }
80
+ }
81
+ return row[b.length];
82
+ };
83
+
84
+ /** The match of `p` against one variant: its slot, root and bindings, or null (not matching, filtered out, or an app-kind slot of another kind). */
85
+ function matchVariant(variant, p, only, appKind) {
86
+ if (only && !only(variant.slot)) return null;
87
+ const m = variant.regex.exec(p);
88
+ if (!m) return null;
89
+ const bindings = {};
90
+ variant.names.forEach((name, i) => { bindings[name] = m[i + 2]; });
91
+ if (variant.slot.appKind !== undefined && appKind.get(bindings.app) !== variant.slot.appKind) return null;
92
+ const root = variant.dir ? m[1] : path.posix.dirname(m[1]);
93
+ return { variant, slot: variant.slot, root: root === '.' ? '' : root, bindings };
94
+ }
95
+
96
+ /** Every variant matching `p`, with its root and bindings, minus app-kind slots of another kind, the most specific first. */
97
+ export function matchVariants({ variants, appKind }, p, only) {
98
+ const found = [];
99
+ for (const variant of variants) {
100
+ const hit = matchVariant(variant, p, only, appKind);
101
+ if (hit) found.push(hit);
102
+ }
103
+ return found.sort((a, b) => b.variant.score - a.variant.score || a.variant.wildcards - b.variant.wildcards);
104
+ }
105
+
106
+ /** The single most specific match of a sorted list: {hit, ambiguous}; two slots of equal specificity are ambiguous. */
107
+ export function bestMatch(found) {
108
+ if (!found.length) return { hit: null, ambiguous: [] };
109
+ const top = found.filter((f) => f.variant.score === found[0].variant.score && f.variant.wildcards === found[0].variant.wildcards);
110
+ const distinct = [...new Set(top.map((f) => f.slot.id))];
111
+ return distinct.length > 1 ? { hit: null, ambiguous: distinct } : { hit: top[0], ambiguous: [] };
112
+ }
113
+
114
+ /** How many leading segments of `parts` a variant accepts. */
115
+ function acceptedDepth(variant, parts) {
116
+ let depth = 0;
117
+ while (depth < parts.length && depth < variant.segments.length) {
118
+ const rx = variant.segmentRegexes[depth];
119
+ if (rx === null || !rx.test(parts[depth])) break;
120
+ depth += 1;
121
+ }
122
+ return depth;
123
+ }
124
+
125
+ /** Whether `cand` is nearer to the path than `winner`: deeper, then closer in shape, then in spelling, then more specific. */
126
+ function nearerCandidate(cand, winner) {
127
+ if (!winner) return true;
128
+ if (cand.depth !== winner.depth) return cand.depth > winner.depth;
129
+ if (cand.shape !== winner.shape) return cand.shape < winner.shape;
130
+ if (cand.distance !== winner.distance) return cand.distance < winner.distance;
131
+ return cand.variant.score > winner.variant.score;
132
+ }
133
+
134
+ /** The leading segments of `p` a variant accepts, and how far it got; how the nearest slot of an unknown path is found. */
135
+ export function nearestSlot(variants, p) {
136
+ const parts = p.split('/');
137
+ let winner = null;
138
+ for (const variant of variants) {
139
+ const depth = acceptedDepth(variant, parts);
140
+ const expected = variant.segments[depth] ?? '';
141
+ const distance = levenshtein(parts[depth] ?? '', expected.replace(VAR, ''));
142
+ const cand = { variant, depth, shape: Math.abs(variant.segments.length - parts.length), distance };
143
+ if (nearerCandidate(cand, winner)) winner = cand;
144
+ }
145
+ if (!winner) return null;
146
+ return {
147
+ slot: winner.variant.slot.id,
148
+ pattern: winner.variant.pattern,
149
+ matchedDepth: winner.depth,
150
+ matchedPrefix: parts.slice(0, winner.depth).join('/'),
151
+ expectedNext: winner.variant.segments[winner.depth] ?? null,
152
+ };
153
+ }
@@ -0,0 +1,8 @@
1
+ // slot-path.mjs - normalize a slot-relative path for resolver comparisons.
2
+ import { posixPath } from '../lib/path-key.mjs';
3
+
4
+ const SLASH = '/';
5
+ const END = '$';
6
+ const TRAILING_SLASHES = new RegExp(`${SLASH}+${END}`);
7
+
8
+ export const cleanSlotPath = (value) => posixPath(value).replace(TRAILING_SLASHES, '');
@@ -0,0 +1,76 @@
1
+ // slot-required.mjs - what a scope (the app root or one side) must contain, without walking it: the files an instance of a slot
2
+ // requires, and the paths and minimum instance counts of every required slot. A scope is the one slots.mjs builds.
3
+ import path from 'node:path';
4
+ import { braceVariants } from '../lib/glob.mjs';
5
+ import { classifyIn, slotEnabled } from './slot-classify.mjs';
6
+ import { fillVars, varsOf } from './slot-match.mjs';
7
+
8
+ /** One `requires` entry of a slot placed under `root`: a leading / makes it repository-relative, the variables are filled from `bindings`. */
9
+ function requiredEntry(entry, bindings, root) {
10
+ const rooted = entry.startsWith('/');
11
+ const filled = fillVars(rooted ? entry.slice(1) : entry, bindings);
12
+ return rooted || !root || root === '.' ? filled : `${root}/${filled}`;
13
+ }
14
+
15
+ /** The files and directories the instance holding `p` must contain (directories end with /); {path} entries are repo-relative. */
16
+ export function requiredFilesIn(scope, input) {
17
+ const c = classifyIn(scope, input);
18
+ if (!c.slot) return [];
19
+ const slot = scope.byId.get(c.slot);
20
+ return (slot.requires ?? []).map((entry) => requiredEntry(entry, c.bindings, c.root));
21
+ }
22
+
23
+ function growBindings(combos, name, values) {
24
+ const next = [];
25
+ for (const combo of combos) {
26
+ for (const value of values) next.push({ ...combo, [name]: value });
27
+ }
28
+ combos.splice(0, combos.length, ...next);
29
+ }
30
+
31
+ const expandApps = (repo, slot) => repo.apps.filter((a) => slot.appKind === undefined || a.kind === slot.appKind);
32
+
33
+ function appendRequiredVariantPaths(repo, slot, variant, paths) {
34
+ const names = [...new Set(varsOf(variant))];
35
+ const fixed = { ...slot.requiredInstances };
36
+ const open = names.filter((name) => name !== 'app' && !(name in fixed));
37
+ if (open.length) return;
38
+ const combos = [{}];
39
+ if (names.includes('app')) growBindings(combos, 'app', expandApps(repo, slot).map((app) => app.name));
40
+ for (const [name, values] of Object.entries(fixed)) growBindings(combos, name, values);
41
+ const isInstance = names.length > 0;
42
+ for (const bindings of combos) {
43
+ const target = fillVars(variant, bindings);
44
+ paths.push({ slot: slot.id, path: target, via: isInstance ? 'instance' : 'slot' });
45
+ const root = target.endsWith('/') ? target.slice(0, -1) : path.posix.dirname(target);
46
+ for (const entry of slot.requires ?? []) paths.push({ slot: slot.id, path: requiredEntry(entry, bindings, root), via: 'requires' });
47
+ }
48
+ }
49
+
50
+ function appendRequiredSlotPaths(scope, slot, paths, minimums) {
51
+ if (slot.presence !== 'required' || !slotEnabled(scope, slot)) return;
52
+ if (slot.requiredWhen === 'connections' && !scope.repo.connections.length) return;
53
+ if (slot.minInstances) minimums.push({ slot: slot.id, min: slot.minInstances, ...(slot.appKind ? { appKind: slot.appKind } : {}) });
54
+ for (const variant of braceVariants(slot.path)) appendRequiredVariantPaths(scope.repo, slot, variant, paths);
55
+ }
56
+
57
+ function uniqueRequiredPaths(paths) {
58
+ const seen = new Set();
59
+ return paths.filter((entry) => {
60
+ const key = `${entry.slot}|${entry.path}`;
61
+ if (seen.has(key)) return false;
62
+ seen.add(key);
63
+ return true;
64
+ });
65
+ }
66
+
67
+ /**
68
+ * What the repository must contain, without walking it: {paths: [{slot, path, via}], minimums: [{slot, min}]}.
69
+ * via is slot (a fixed path), instance (a requiredInstances or app-kind root) or requires (a file an instance needs).
70
+ */
71
+ export function requiredPathsIn(scope) {
72
+ const paths = [];
73
+ const minimums = [];
74
+ for (const slot of scope.slots) appendRequiredSlotPaths(scope, slot, paths, minimums);
75
+ return { paths: uniqueRequiredPaths(paths), minimums };
76
+ }
@@ -0,0 +1,148 @@
1
+ // slot-semantic-problems.mjs - semantic checks for the HFS slot manifest.
2
+ import { isPlainObject } from '../../engine/plain-object.mjs';
3
+ import { triggerProblems } from './declaration-slots.mjs';
4
+ import { litePresenceOf, slotInEdition } from './edition-slots.mjs';
5
+ import { manifestKind, RUNTIME_KIND, runtimeSemanticProblems } from './manifest-shape.mjs';
6
+ import { APP_SCOPE, PROFILES } from './slot-manifest-shape.mjs';
7
+
8
+ function checkAppRootSlot(slot, bad) {
9
+ for (const key of ['appKind', 'owner', 'layers', 'kinds', 'composedBy', 'requiredWhen', 'requiredInstances']) {
10
+ if (slot[key] !== undefined) bad.push(`slot ${slot.id}: an app-root slot has no ${key}`);
11
+ }
12
+ if (slot.tier !== 'none') bad.push(`slot ${slot.id}: an app-root slot has tier none`);
13
+ }
14
+
15
+ function checkSideSlot(slot, profile, manifest, bad) {
16
+ const tiers = manifest.tiers[profile];
17
+ if (slot.tier !== 'none' && slot.tier !== 'inherit' && !(slot.tier in tiers)) bad.push(`slot ${slot.id}: tier ${slot.tier} is not a ${profile} tier`);
18
+ if (slot.appKind !== undefined && !manifest.appKinds[profile].includes(slot.appKind)) bad.push(`slot ${slot.id}: app kind ${slot.appKind} is not a ${profile} kind`);
19
+ bad.push(...triggerProblems(slot, manifest.triggerKinds));
20
+ }
21
+
22
+ function claimSlotPatterns(slot, profile, claimed, bad, braceVariants) {
23
+ if (slot.appKind !== undefined) return;
24
+ for (const variant of braceVariants(slot.path)) {
25
+ const key = `${profile}:${variant}`;
26
+ if (claimed.has(key)) bad.push(`slots ${claimed.get(key)} and ${slot.id} claim the same pattern ${variant} on ${profile}`);
27
+ else claimed.set(key, slot.id);
28
+ }
29
+ }
30
+
31
+ function checkSlotProfiles(slot, manifest, claimed, bad, braceVariants) {
32
+ for (const profile of slot.profiles) {
33
+ if (profile === APP_SCOPE) checkAppRootSlot(slot, bad);
34
+ else checkSideSlot(slot, profile, manifest, bad);
35
+ claimSlotPatterns(slot, profile, claimed, bad, braceVariants);
36
+ }
37
+ }
38
+
39
+ function checkLitePath(slot, pathVars, varsOf, bad, braceVariants, compileVariant) {
40
+ if (slot.lite?.path === undefined) return;
41
+ for (const name of varsOf(slot.lite.path)) if (!pathVars.has(name)) bad.push(`slot ${slot.id}: lite.path binds <${name}>, which the path does not`);
42
+ for (const variant of braceVariants(slot.lite.path)) {
43
+ try { compileVariant(slot, variant); } catch (error) { bad.push(`slot ${slot.id}: lite.path ${variant} does not compile (${error.message})`); }
44
+ }
45
+ }
46
+
47
+ function checkNamedPathBindings(slot, pathVars, varsOf, bad) {
48
+ for (const name of Object.keys(slot.requiredInstances ?? {})) {
49
+ if (!pathVars.has(name)) bad.push(`slot ${slot.id}: requiredInstances names <${name}>, which the path does not bind`);
50
+ }
51
+ for (const file of Object.values(slot.roles ?? {})) {
52
+ for (const name of varsOf(file)) if (!pathVars.has(name)) bad.push(`slot ${slot.id}: roles names <${name}> in ${file}, which the path does not bind`);
53
+ }
54
+ for (const entry of slot.requires ?? []) {
55
+ for (const name of varsOf(entry)) if (!pathVars.has(name)) bad.push(`slot ${slot.id}: requires ${entry} uses <${name}>, which the path does not bind`);
56
+ }
57
+ }
58
+
59
+ function checkSlotPatternCompilation(slot, bad, braceVariants, compileVariant) {
60
+ for (const variant of braceVariants(slot.path)) {
61
+ try { compileVariant(slot, variant); } catch (error) { bad.push(`slot ${slot.id}: pattern ${variant} does not compile (${error.message})`); }
62
+ }
63
+ }
64
+
65
+ function checkSlotComposition(slot, manifest, bad) {
66
+ for (const kind of slot.composedBy ?? []) {
67
+ if (!slot.profiles.every((profile) => manifest.appKinds[profile].includes(kind))) bad.push(`slot ${slot.id}: composedBy names ${kind}, which is not an app kind of every profile of the slot`);
68
+ }
69
+ if (slot.layers !== undefined && slot.tier !== 'none' && !slot.profiles.every((profile) => manifest.tiers[profile][slot.tier]?.lowerLayerOnly)) bad.push(`slot ${slot.id}: layers need a lowerLayerOnly tier`);
70
+ }
71
+
72
+ function checkLiteDeclaration(slot, manifest, bad) {
73
+ if (!slotInEdition(manifest, slot, 'lite') && (slot.litePresence !== undefined || slot.lite !== undefined)) bad.push(`slot ${slot.id}: litePresence/lite on a slot lite never sees (editions)`);
74
+ if (isPlainObject(slot.litePresence) && slot.profiles.includes(APP_SCOPE)) bad.push(`slot ${slot.id}: an app-root slot takes a bare litePresence, not a per-side map`);
75
+ for (const profile of slot.profiles) {
76
+ if (litePresenceOf(slot, profile) === 'forbidden' && typeof slot.goesTo !== 'string') bad.push(`slot ${slot.id}: forbidden in lite for ${profile}, so it says where the content goes (goesTo)`);
77
+ }
78
+ }
79
+
80
+ function checkSlotSemantics(slot, manifest, claimed, bad, helpers) {
81
+ if (claimed.ids.has(slot.id)) bad.push(`slot id ${slot.id} appears twice`);
82
+ claimed.ids.add(slot.id);
83
+ const pathVars = new Set(helpers.varsOf(slot.path));
84
+ checkSlotProfiles(slot, manifest, claimed.patterns, bad, helpers.braceVariants);
85
+ if (slot.appKind !== undefined && !pathVars.has('app')) bad.push(`slot ${slot.id}: an app-kind slot binds <app> in its path`);
86
+ if (slot.requiredWhen !== undefined && slot.presence !== 'required') bad.push(`slot ${slot.id}: requiredWhen belongs to a required slot`);
87
+ checkLiteDeclaration(slot, manifest, bad);
88
+ checkLitePath(slot, pathVars, helpers.varsOf, bad, helpers.braceVariants, helpers.compileVariant);
89
+ checkNamedPathBindings(slot, pathVars, helpers.varsOf, bad);
90
+ checkSlotPatternCompilation(slot, bad, helpers.braceVariants, helpers.compileVariant);
91
+ checkSlotComposition(slot, manifest, bad);
92
+ }
93
+
94
+ /** The problem of one declared side read, or null when a slot of the side it names (or of the app root) owns the path. */
95
+ function sideReadProblem(manifest, side, read, braceVariants) {
96
+ const [owner, ...rest] = read.split('/');
97
+ if (owner === side) return `sides.${side}.reads names ${read}, which is its own side`;
98
+ const ownedBySide = PROFILES.includes(owner);
99
+ const profile = ownedBySide ? owner : APP_SCOPE;
100
+ const prefix = ownedBySide ? rest.join('/') : read;
101
+ const owned = manifest.slots.some((slot) => slot.profiles.includes(profile) && slot.presence !== 'forbidden' && braceVariants(slot.path).some((variant) => variant.startsWith(prefix)));
102
+ const ownerName = ownedBySide ? owner : 'app-root';
103
+ return owned ? null : `sides.${side}.reads names ${read}, which no ${ownerName} slot owns`;
104
+ }
105
+
106
+ function checkSideReads(manifest, bad, braceVariants) {
107
+ for (const side of PROFILES) {
108
+ for (const read of manifest.sides[side].reads) {
109
+ const problem = sideReadProblem(manifest, side, read, braceVariants);
110
+ if (problem) bad.push(problem);
111
+ }
112
+ }
113
+ }
114
+
115
+ function checkProfileTierTargets(manifest, profile, bad) {
116
+ for (const [tier, def] of Object.entries(manifest.tiers[profile])) {
117
+ for (const target of def.mayImport) {
118
+ if (!(target in manifest.tiers[profile])) bad.push(`tiers.${profile}.${tier}.mayImport names ${target}, which is not a ${profile} tier`);
119
+ }
120
+ }
121
+ }
122
+
123
+ function checkProfileAppKinds(manifest, profile, bad) {
124
+ for (const kind of manifest.appKinds[profile]) {
125
+ const owners = manifest.slots.filter((slot) => slot.profiles.includes(profile) && slot.appKind === kind);
126
+ if (owners.length !== 1) bad.push(`${profile} app kind ${kind} must have exactly one slot (found ${owners.length})`);
127
+ }
128
+ }
129
+
130
+ function checkProfileMaps(manifest, bad) {
131
+ for (const profile of PROFILES) {
132
+ checkProfileTierTargets(manifest, profile, bad);
133
+ checkProfileAppKinds(manifest, profile, bad);
134
+ }
135
+ }
136
+
137
+ /** The rules a JSON Schema cannot state: unique ids, tiers named and reachable, app kinds, variables, no duplicate pattern. */
138
+ export function manifestSemanticProblems(manifest, helpers) {
139
+ const bad = [];
140
+ const claimed = { ids: new Set(), patterns: new Map() };
141
+ for (const slot of manifest.slots) checkSlotSemantics(slot, manifest, claimed, bad, helpers);
142
+ if (manifestKind(manifest) === RUNTIME_KIND) return [...bad, ...runtimeSemanticProblems(manifest)];
143
+ checkSideReads(manifest, bad, helpers.braceVariants);
144
+ checkProfileMaps(manifest, bad);
145
+ const major = Number(manifest.version.split('.')[0]);
146
+ if (String(manifest.schema) !== `starci/hfs-slots@${major}`) bad.push(`schema ${manifest.schema} does not carry the major of version ${manifest.version}`);
147
+ return bad;
148
+ }
@@ -0,0 +1,28 @@
1
+ // slot-side-problems.mjs - declaration checks for one app side against the HFS manifest.
2
+ import { optionalSlotProblems } from './declaration-slots.mjs';
3
+
4
+ function sideAppKindProblems(manifest, side, apps) {
5
+ const bad = [];
6
+ for (const app of apps) if (!manifest.appKinds[side].includes(app.kind)) bad.push(`${side} app ${app.name} has kind ${app.kind}, which is not a ${side} kind (${manifest.appKinds[side].join(', ')})`);
7
+ return bad;
8
+ }
9
+
10
+ function requiredAppKindProblems(manifest, side, declaration) {
11
+ const bad = [];
12
+ const connections = declaration.connections ?? [];
13
+ for (const slot of manifest.slots) {
14
+ if (slot.appKind === undefined || !slot.profiles.includes(side) || slot.presence !== 'required') continue;
15
+ if (slot.requiredWhen === 'connections' && !connections.length) continue;
16
+ if (!declaration.apps.some((app) => app.kind === slot.appKind)) bad.push(`no ${side} app of kind ${slot.appKind} is declared (${slot.id} is required${slot.requiredWhen ? ' once a connection is declared' : ''})`);
17
+ }
18
+ return bad;
19
+ }
20
+
21
+ /** Problems for one side declaration, preserving the manifest's checks and their order. */
22
+ export function sideProblems(manifest, side, declaration) {
23
+ const bad = sideAppKindProblems(manifest, side, declaration.apps);
24
+ bad.push(...optionalSlotProblems(manifest, side, declaration));
25
+ for (const read of declaration.reads ?? []) if (!manifest.sides[side].reads.includes(read)) bad.push(`sides.${side}.reads names ${read}; ${side} may read only ${manifest.sides[side].reads.join(', ') || 'nothing of the other side'}`);
26
+ bad.push(...requiredAppKindProblems(manifest, side, declaration));
27
+ return bad;
28
+ }