@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
@@ -25,7 +25,12 @@ export function testSecretPaths(name, { repo, stack = 'dev' } = {}) {
25
25
  export const setCommand = (name = '<name>', stack = 'dev') => `node scripts/stack-secret.mjs set ${stack}/secrets/test/${name}`;
26
26
 
27
27
  /** sops' format for a plaintext path, as the repository's stack-secret.mjs formatFor decides it. */
28
- export const sopsFormatFor = (file) => (file.endsWith('.env') ? 'dotenv' : /\.json$/.test(file) ? 'json' : /\.(ya?ml|kubeconfig)$/.test(file) ? 'yaml' : 'binary');
28
+ export const sopsFormatFor = (file) => {
29
+ if (file.endsWith('.env')) return 'dotenv';
30
+ if (file.endsWith('.json')) return 'json';
31
+ if (/\.(ya?ml|kubeconfig)$/.test(file)) return 'yaml';
32
+ return 'binary';
33
+ };
29
34
 
30
35
  /** Every leaf outside the `sops` block is an ENC[...] value (or null); the block itself carries a mac. */
31
36
  const allEncrypted = (node) => {
@@ -48,7 +53,7 @@ export function isSopsEnvelope(text) {
48
53
  if (!src.trim()) return false;
49
54
  try { return sopsTree(JSON.parse(src)); } catch { /* not JSON */ }
50
55
  const lines = src.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#/.test(l));
51
- if (lines.length && lines.every((l) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(l))) {
56
+ if (lines.length && lines.every((l) => /^[A-Za-z_]\w*=/.test(l))) {
52
57
  const pairs = lines.map((l) => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)]);
53
58
  const meta = pairs.filter(([k]) => k.startsWith('sops_'));
54
59
  const vals = pairs.filter(([k]) => !k.startsWith('sops_'));
@@ -58,55 +63,97 @@ export function isSopsEnvelope(text) {
58
63
  }
59
64
 
60
65
  /** Admit one flat age envelope for the native selected-identity invocation; this is shape admission, never cryptographic proof. */
61
- export function selectedAgeEnvelope(text, format, recipient) {
62
- const refuse = reason => ({ ok: false, reason });
63
- if (!['binary', 'json', 'yaml', 'dotenv'].includes(format)) return refuse('unsupported-format');
64
- let meta;
66
+ const refusal = (reason) => ({ ok: false, reason });
67
+
68
+ function dotenvMetadataValue(meta, age, key, value) {
69
+ if (key === 'age__list_0__map_recipient') age.recipient = value;
70
+ else if (key === 'age__list_0__map_enc') age.enc = value;
71
+ else if (key === 'mac_only_encrypted') {
72
+ if (value !== 'false') return 'partial-mac';
73
+ meta[key] = false;
74
+ } else if (key.includes('__')) return 'unsupported-key-group';
75
+ else meta[key] = value;
76
+ return null;
77
+ }
78
+
79
+ function readDotenvMetadata(text) {
80
+ const pairs = new Map();
81
+ for (const line of String(text).split('\n')) {
82
+ if (!line || line.startsWith('#')) continue;
83
+ if (line.includes('\r')) return { refusal: 'unsupported-dotenv' };
84
+ const at = line.indexOf('=');
85
+ if (at < 1) return { refusal: 'unsupported-dotenv' };
86
+ const name = line.slice(0, at);
87
+ if (pairs.has(name)) return { refusal: 'duplicate-key' };
88
+ pairs.set(name, line.slice(at + 1).replaceAll(String.raw`\n`, '\n'));
89
+ }
90
+ const meta = {};
91
+ const age = {};
92
+ for (const [name, value] of pairs) {
93
+ if (!name.startsWith('sops_')) continue;
94
+ const reason = dotenvMetadataValue(meta, age, name.slice('sops_'.length), value);
95
+ if (reason) return { refusal: reason };
96
+ }
97
+ meta.age = [age];
98
+ return { meta };
99
+ }
100
+
101
+ function readStructuredMetadata(text, format) {
102
+ const doc = parseYaml(String(text));
103
+ if (format !== 'yaml') JSON.parse(String(text));
104
+ if (format === 'binary' && (!doc || Object.keys(doc).length !== 2 || !Object.hasOwn(doc, 'data') || !Object.hasOwn(doc, 'sops') || typeof doc.data !== 'string'))
105
+ return { refusal: 'unsupported-binary-shape' };
106
+ return { meta: doc?.sops };
107
+ }
108
+
109
+ function readAgeMetadata(text, format) {
65
110
  try {
66
- if (format === 'dotenv') {
67
- const pairs = new Map();
68
- for (const line of String(text).split('\n')) {
69
- if (!line || line.startsWith('#')) continue;
70
- if (line.includes('\r')) return refuse('unsupported-dotenv');
71
- const at = line.indexOf('=');
72
- if (at < 1) return refuse('unsupported-dotenv');
73
- const name = line.slice(0, at);
74
- if (pairs.has(name)) return refuse('duplicate-key');
75
- pairs.set(name, line.slice(at + 1).replaceAll('\\n', '\n'));
76
- }
77
- meta = {};
78
- const age = {};
79
- for (const [name, value] of pairs) {
80
- if (!name.startsWith('sops_')) continue;
81
- const key = name.slice('sops_'.length);
82
- if (key === 'age__list_0__map_recipient') age.recipient = value;
83
- else if (key === 'age__list_0__map_enc') age.enc = value;
84
- else if (key === 'mac_only_encrypted') {
85
- if (value !== 'false') return refuse('partial-mac');
86
- meta[key] = false;
87
- } else if (key.includes('__')) return refuse('unsupported-key-group');
88
- else meta[key] = value;
89
- }
90
- meta.age = [age];
91
- } else {
92
- const doc = parseYaml(String(text));
93
- if (format !== 'yaml') JSON.parse(String(text));
94
- if (format === 'binary' && (!doc || Object.keys(doc).length !== 2 || !Object.hasOwn(doc, 'data') || !Object.hasOwn(doc, 'sops') || typeof doc.data !== 'string')) return refuse('unsupported-binary-shape');
95
- meta = doc?.sops;
96
- }
97
- } catch { return refuse('invalid-envelope'); }
98
- if (!meta || typeof meta !== 'object' || Array.isArray(meta) || !isSopsEnvelope(text)) return refuse('invalid-envelope');
99
- if (Object.hasOwn(meta, 'key_groups') || Object.hasOwn(meta, 'shamir_threshold')) return refuse('unsupported-key-group');
111
+ return format === 'dotenv' ? readDotenvMetadata(text) : readStructuredMetadata(text, format);
112
+ } catch { return { refusal: 'invalid-envelope' }; }
113
+ }
114
+
115
+ function hasNonAgeProvider(meta, providers) {
116
+ for (const name of providers) {
117
+ if (Object.hasOwn(meta, name) && (!Array.isArray(meta[name]) || meta[name].length)) return true;
118
+ }
119
+ return false;
120
+ }
121
+
122
+ function isAgeEntry(entry) {
123
+ return !!entry && typeof entry === 'object' && !Array.isArray(entry)
124
+ && !Object.keys(entry).some(name => !['recipient', 'enc'].includes(name));
125
+ }
126
+
127
+ function hasValidAgePayload(entry) {
128
+ return typeof entry.enc === 'string' && entry.enc.startsWith('-----BEGIN AGE ENCRYPTED FILE-----\n')
129
+ && entry.enc.includes('\n-----END AGE ENCRYPTED FILE-----');
130
+ }
131
+
132
+ function hasValidEnvelopeVersion(meta) {
133
+ return typeof meta.version === 'string' && /^\d+\.\d+\.\d+$/.test(meta.version)
134
+ && typeof meta.lastmodified === 'string' && Number.isFinite(Date.parse(meta.lastmodified));
135
+ }
136
+
137
+ function validateAgeMetadata(meta, text, recipient) {
138
+ if (!meta || typeof meta !== 'object' || Array.isArray(meta) || !isSopsEnvelope(text)) return refusal('invalid-envelope');
139
+ if (Object.hasOwn(meta, 'key_groups') || Object.hasOwn(meta, 'shamir_threshold')) return refusal('unsupported-key-group');
100
140
  const providers = ['kms', 'gcp_kms', 'hckms', 'azure_kv', 'hc_vault', 'pgp'];
101
- for (const name of providers) if (Object.hasOwn(meta, name) && (!Array.isArray(meta[name]) || meta[name].length)) return refuse('non-age-provider');
141
+ if (hasNonAgeProvider(meta, providers)) return refusal('non-age-provider');
102
142
  const fields = new Set([...providers, 'age', 'lastmodified', 'mac', 'version', 'unencrypted_suffix', 'encrypted_suffix', 'unencrypted_regex', 'encrypted_regex', 'unencrypted_comment_regex', 'encrypted_comment_regex', 'mac_only_encrypted']);
103
- if (Object.keys(meta).some(name => !fields.has(name))) return refuse('unknown-metadata');
104
- if (Object.hasOwn(meta, 'mac_only_encrypted') && meta.mac_only_encrypted !== false) return refuse('partial-mac');
105
- if (!Array.isArray(meta.age) || meta.age.length !== 1) return refuse('unsupported-age-set');
143
+ if (Object.keys(meta).some(name => !fields.has(name))) return refusal('unknown-metadata');
144
+ if (Object.hasOwn(meta, 'mac_only_encrypted') && meta.mac_only_encrypted !== false) return refusal('partial-mac');
145
+ if (!Array.isArray(meta.age) || meta.age.length !== 1) return refusal('unsupported-age-set');
106
146
  const entry = meta.age[0];
107
- if (!entry || typeof entry !== 'object' || Array.isArray(entry) || Object.keys(entry).some(name => !['recipient', 'enc'].includes(name))) return refuse('invalid-age-entry');
108
- if (typeof entry.recipient !== 'string' || entry.recipient !== recipient) return refuse('recipient-mismatch');
109
- if (typeof entry.enc !== 'string' || !entry.enc.startsWith('-----BEGIN AGE ENCRYPTED FILE-----\n') || !entry.enc.includes('\n-----END AGE ENCRYPTED FILE-----')) return refuse('invalid-age-entry');
110
- if (typeof meta.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(meta.version) || typeof meta.lastmodified !== 'string' || !Number.isFinite(Date.parse(meta.lastmodified))) return refuse('invalid-envelope');
147
+ if (!isAgeEntry(entry)) return refusal('invalid-age-entry');
148
+ if (typeof entry.recipient !== 'string' || entry.recipient !== recipient) return refusal('recipient-mismatch');
149
+ if (!hasValidAgePayload(entry)) return refusal('invalid-age-entry');
150
+ if (!hasValidEnvelopeVersion(meta)) return refusal('invalid-envelope');
111
151
  return { ok: true };
112
152
  }
153
+
154
+ export function selectedAgeEnvelope(text, format, recipient) {
155
+ if (!['binary', 'json', 'yaml', 'dotenv'].includes(format)) return refusal('unsupported-format');
156
+ const parsed = readAgeMetadata(text, format);
157
+ if (parsed.refusal) return refusal(parsed.refusal);
158
+ return validateAgeMetadata(parsed.meta, text, recipient);
159
+ }
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { parseYaml } from '../../engine/yaml.mjs';
4
4
  import { words } from './dockerfile.mjs';
5
+ import { asList } from './list.mjs';
5
6
 
6
7
  /**
7
8
  * The services of a repository's own stack definition, read from `.starcistacks/application-stacks.yaml` and the compose
@@ -74,8 +75,6 @@ const imagePathOf = image => {
74
75
  /** True when the image reference carries a tag or digest (an external image, not a placeholder of the repository's own build). */
75
76
  const isTagged = image => image.includes('@') || image.slice(image.lastIndexOf('/') + 1).includes(':');
76
77
 
77
- const asList = value => (Array.isArray(value) ? value : value === undefined || value === null ? [] : [value]);
78
-
79
78
  /** A compose `environment` (map or `KEY=VALUE` list) as a plain object; a `*_FILE` variable becomes the plain variable with the test value. */
80
79
  const environmentOf = value => {
81
80
  const entries = Array.isArray(value)
@@ -2,6 +2,15 @@
2
2
  // literal text, a node's 1-based position, a named property's initializer text. `ts` is the caller's
3
3
  // already-resolved compiler (see package-at.mjs loadTypescript); nothing here resolves a package.
4
4
 
5
+ /** Whether the single-bit `flag` (a ts.NodeFlags member) is set in `flags`: the bit read as arithmetic, no bitwise operator in the condition. */
6
+ export const hasFlag = (flags, flag) => Math.floor(flags / flag) % 2 === 1;
7
+
8
+ /** Whether any bit of `mask` (a ts flag or an OR of flags) is set in `flags` (absent flags read as none): an explicit comparison of the masked value. */
9
+ export const hasAnyFlag = (flags, mask) => ((flags ?? 0) & mask) !== 0;
10
+
11
+ /** Whether the variable `declaration` belongs to a `const` list: the TypeScript node-flag test, an explicit comparison of the masked flags. */
12
+ export const isConstVariable = (ts, declaration) => (ts.getCombinedNodeFlags(declaration.parent) & ts.NodeFlags.Const) !== 0;
13
+
5
14
  /** The text of an identifier or a string-literal-like `node`, or null (property names, literal specifiers). */
6
15
  export const nameText = (ts, node) => (node && (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) ? node.text : null);
7
16
 
@@ -20,7 +20,12 @@ export function walkFiles(dir, {filter = () => true, sorted = false, exclude = (
20
20
  if (depth > maxDepth) return;
21
21
  let entries;
22
22
  try { entries = fs.readdirSync(current, {withFileTypes: true}); }
23
- catch (error) { if (ignoreReadErrors) return; throw error; }
23
+ catch (error) {
24
+ if (ignoreReadErrors) {
25
+ return;
26
+ }
27
+ throw error;
28
+ }
24
29
  if (sorted) entries = entries.sort((a, b) => a.name.localeCompare(b.name));
25
30
  for (const entry of entries) {
26
31
  const full = path.join(current, entry.name);
@@ -6,6 +6,7 @@ import { appScripts, imageFiles, TEMPLATES_DIR } from "../sync/index.mjs";
6
6
  import { ScaffoldError } from "./service.mjs";
7
7
  import { jsonText, packageJsonText } from "./app.mjs";
8
8
  import { registerLiteExports } from "./lite-exports.mjs";
9
+ import { byCodeUnit } from "../report/order.mjs";
9
10
 
10
11
  const read = (relative) =>
11
12
  fs
@@ -204,7 +205,7 @@ export function ensureLiteCli({ root, manifest, repo }) {
204
205
  declaration.sides.be.apps.push({ name: "cli", kind: "cli" });
205
206
  declaration.sides.be.kinds = [
206
207
  ...new Set([...(declaration.sides.be.kinds ?? []), "cli"]),
207
- ].sort();
208
+ ].sort(byCodeUnit);
208
209
  const resolved = resolveRepoDeclaration(manifest, declaration);
209
210
  const image = imageFiles(resolved).find(
210
211
  (file) => file.path === "be/apps/cli/Dockerfile",
@@ -316,7 +316,7 @@ export function addTable({
316
316
  fs.writeFileSync(target, file.body);
317
317
  created.push(target);
318
318
  }
319
- if (emitTypes !== false) {
319
+ if (typeof emitTypes === "function") {
320
320
  const generated = emitTypes({ root });
321
321
  if (typeof generated === "string") {
322
322
  const target = path.join(root, ...dbTypesPath.split("/"));
@@ -339,6 +339,6 @@ export function addTable({
339
339
  }
340
340
  return {
341
341
  created: planned.map((file) => file.relative),
342
- types: emitTypes === false ? "skipped" : dbTypesPath,
342
+ types: typeof emitTypes === "function" ? dbTypesPath : "skipped",
343
343
  };
344
344
  }
package/scaffold/add.mjs CHANGED
@@ -26,6 +26,7 @@ import { addApp } from './add-app.mjs';
26
26
  import { ensureLiteCli, selectLiteCliEntries } from './add-cli-lite.mjs';
27
27
  import { addLiteWebhookInbox, selectLiteWebhookEntries } from './add-inbox-lite.mjs';
28
28
  import { jsonText } from './app.mjs';
29
+ import { byCodeUnit } from '../report/order.mjs';
29
30
  import { registerLiteDomainService, registerLiteExports } from './lite-exports.mjs';
30
31
 
31
32
  const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
@@ -290,8 +291,8 @@ function topicsOfPatterns(patterns) {
290
291
  function register({ declarationFile, spec }) {
291
292
  const declaration = JSON.parse(fs.readFileSync(declarationFile, 'utf8'));
292
293
  const be = declaration.sides.be;
293
- const patterns = [...new Set([...(be.patterns ?? []), ...spec.patterns])].sort();
294
- const kinds = [...new Set([...(be.kinds ?? []), ...(spec.trigger ? [spec.trigger] : [])])].sort();
294
+ const patterns = [...new Set([...(be.patterns ?? []), ...spec.patterns])].sort(byCodeUnit);
295
+ const kinds = [...new Set([...(be.kinds ?? []), ...(spec.trigger ? [spec.trigger] : [])])].sort(byCodeUnit);
295
296
  be.patterns = patterns;
296
297
  if (kinds.length) be.kinds = kinds;
297
298
  fs.writeFileSync(declarationFile, jsonText(declaration));
package/scaffold/app.mjs CHANGED
@@ -25,6 +25,7 @@ import { unlinkOnly } from '../runtime/scripts/api/fs/lib.mjs';
25
25
  import { revParseQuery } from '../runtime/scripts/api/git/rev-parse-query.mjs';
26
26
  import { lsFiles } from '../runtime/scripts/api/git/ls-files.mjs';
27
27
  import { insidePath, samePath } from '../runtime/scripts/lib/path-key.mjs';
28
+ import { byCodeUnit } from '../report/order.mjs';
28
29
 
29
30
  const NAME = /^[a-z][a-z0-9-]*$/;
30
31
  const APP_DIR = '__app__';
@@ -64,7 +65,7 @@ export const packageJsonText = value => `${JSON.stringify(value, null, 2)}\n`;
64
65
  * The apps a new app starts with: on the be side the `core` api app and the `cli` app over one `primary` connection, two Next apps
65
66
  * (landing, app) on the fe side, which read the be contracts for their codegen.
66
67
  */
67
- export const STARTER_SIDES = Object.freeze({
68
+ const STARTER_SIDES = Object.freeze({
68
69
  be: Object.freeze({ apps: [{ name: 'core', kind: 'api' }, { name: 'cli', kind: 'cli' }], kinds: ['api', 'cli'], connections: [{ name: 'primary', envPrefix: 'PRIMARY_DB', owner: 'core', isolation: 'database' }] }),
69
70
  // fe: the landing and the product app, over the shared ui and i18n packages (both opt-in slots, enabled here).
70
71
  fe: Object.freeze({ apps: [{ name: 'landing', kind: 'next' }, { name: 'app', kind: 'next' }], reads: ['be/contracts/'], optionalSlots: ['fe.package.ui', 'fe.package.i18n'] }),
@@ -277,6 +278,7 @@ const liteBaseFile = (scope, rel) => {
277
278
  && rel !== 'src/modules/platform/database/connection-source.client.ts'
278
279
  && rel !== 'src/modules/platform/database/migrate-connections.client.ts'
279
280
  && rel !== 'src/modules/platform/database/seed-connections.client.ts'
281
+ && rel !== 'src/modules/platform/primitives/sequence.policy.ts'
280
282
  && !LITE_DEFERRED_GENERATOR_FILES.has(rel)
281
283
  && !rel.startsWith('src/tests/')
282
284
  && !rel.endsWith('.spec.ts');
@@ -486,7 +488,7 @@ export function scaffoldApp({ name, into, presets, edition = 'full', manifest =
486
488
  let locked;
487
489
  try { locked = lock(root); } finally { writes.lockfile(); }
488
490
  if (!locked.ok) throw new ScaffoldError('HFS_SCAFFOLD_LOCK_FAILED', `\`${LOCK_STEP}\` could not resolve the lockfile of ${root} (${locked.detail}). Check the network and the npm registry, then run starci app scaffold ${name} again`);
489
- return { root, files: [...new Set([...files.map(file => file.path), ...targets.map(target => target.path), ...(app.edition === 'lite' ? [dbTypesPath] : []), 'package-lock.json'])].sort() };
491
+ return { root, files: [...new Set([...files.map(file => file.path), ...targets.map(target => target.path), ...(app.edition === 'lite' ? [dbTypesPath] : []), 'package-lock.json'])].sort(byCodeUnit) };
490
492
  } catch (error) {
491
493
  const held = writes.rollback();
492
494
  if (held.length) error.message += `; cleanup retained replaced or occupied entries: ${held.join(', ')}`;
@@ -1,25 +1,25 @@
1
- // The edition gate of `starci app add` and `starci app new` (design 8.6 R8): a verb whose file tree lands in a slot the app's
2
- // edition does not have - the slot's `editions` exclude it, or its `litePresence` is forbidden - is a full-edition
3
- // capability and refuses before anything is planned or written. The slot resolver of the app answers, the same
4
- // view `starci app check` reads; the noun's name is never the test, so a slot the lite manifest forbids refuses every
5
- // verb that writes into it, with no noun list kept anywhere. The refusal carries no failure code by design: the
6
- // scaffolders of packages/hfs keep their codes out of the runtime catalog (like HFS_SCAFFOLD_LOCK_FAILED).
7
-
8
- /** The refusal of a verb a non-full edition does not carry: printed verbatim by the CLI, exit 2, nothing written. */
9
- export class EditionRefusal extends Error {
10
- constructor(command) {
11
- super(`${command}: full edition only; run starci app upgrade --edition full`);
12
- this.name = 'EditionRefusal';
13
- }
14
- }
15
-
16
- /**
17
- * Refuses `command` (`add <noun>`, `new <noun>`) when one of `slotIds` - the slots the verb writes into - does not
18
- * exist for the edition of `resolver`'s repository or is forbidden under it. `resolver` is the
19
- * createSlotResolver() view of the app; every scope answers a slot id. Under full every verb slot exists and none is
20
- * forbidden, so the gate never fires. A slot id the view does not answer (an `editions` exclusion, or an unknown id) refuses
21
- * in every edition alike: the view cannot tell them apart, and the callers name only slots of the manifest.
22
- */
23
- export function refuseInEdition({ resolver, slotIds, command }) {
24
- if (slotIds.some((id) => resolver.slot(id) === null || resolver.slot(id).presence === 'forbidden')) throw new EditionRefusal(command);
25
- }
1
+ // The edition gate of `starci app add` and `starci app new` (design 8.6 R8): a verb whose file tree lands in a slot the app's
2
+ // edition does not have - the slot's `editions` exclude it, or its `litePresence` is forbidden - is a full-edition
3
+ // capability and refuses before anything is planned or written. The slot resolver of the app answers, the same
4
+ // view `starci app check` reads; the noun's name is never the test, so a slot the lite manifest forbids refuses every
5
+ // verb that writes into it, with no noun list kept anywhere. The refusal carries no failure code by design: the
6
+ // scaffolders of packages/hfs keep their codes out of the runtime catalog (like HFS_SCAFFOLD_LOCK_FAILED).
7
+
8
+ /** The refusal of a verb a non-full edition does not carry: printed verbatim by the CLI, exit 2, nothing written. */
9
+ export class EditionRefusal extends Error {
10
+ constructor(command) {
11
+ super(`${command}: full edition only; run starci app upgrade --edition full`);
12
+ this.name = 'EditionRefusal';
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Refuses `command` (`add <noun>`, `new <noun>`) when one of `slotIds` - the slots the verb writes into - does not
18
+ * exist for the edition of `resolver`'s repository or is forbidden under it. `resolver` is the
19
+ * createSlotResolver() view of the app; every scope answers a slot id. Under full every verb slot exists and none is
20
+ * forbidden, so the gate never fires. A slot id the view does not answer (an `editions` exclusion, or an unknown id) refuses
21
+ * in every edition alike: the view cannot tell them apart, and the callers name only slots of the manifest.
22
+ */
23
+ export function refuseInEdition({ resolver, slotIds, command }) {
24
+ if (slotIds.some((id) => resolver.slot(id) === null || resolver.slot(id).presence === 'forbidden')) throw new EditionRefusal(command);
25
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs"
2
2
  import path from "node:path"
3
3
  import { ScaffoldError } from "./service.mjs"
4
+ import { byCodeUnit } from "../report/order.mjs"
4
5
 
5
6
  const REGISTRATIONS = Object.freeze({
6
7
  api: [
@@ -126,7 +127,7 @@ function addExport(text, registration) {
126
127
  const line = new RegExp(`^${kind} \\{ ([^}]*) \\} from "${escape(registration.source)}"$`, "m")
127
128
  const found = line.exec(text)
128
129
  if (found) {
129
- const names = [...new Set([...found[1].split(",").map((name) => name.trim()), ...registration.names])].sort()
130
+ const names = [...new Set([...found[1].split(",").map((name) => name.trim()), ...registration.names])].sort(byCodeUnit)
130
131
  return text.replace(line, `${kind} { ${names.join(", ")} } from "${registration.source}"`)
131
132
  }
132
133
  const added = `${kind} { ${registration.names.join(", ")} } from "${registration.source}"`
@@ -17,6 +17,7 @@ import { createRequire } from 'node:module';
17
17
  import { explainPath } from '../runtime/scripts/hfs/check.mjs';
18
18
  import { createSlotResolver, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
19
19
  import { refuseInEdition } from './edition-gate.mjs';
20
+ import { byCodeUnit } from '../report/order.mjs';
20
21
 
21
22
  export class ScaffoldError extends Error {
22
23
  constructor(code, message) {
@@ -182,8 +183,8 @@ function specSkeleton({ shape, serviceFile, manifest = loadSlotManifest() }) {
182
183
  if (kit.size) lines.push(importLine([...kit].sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' })), KIT));
183
184
  const modules = [...new Set([...imports.keys(), ...typeImports.keys()])].sort(compareModules);
184
185
  for (const module of modules) {
185
- if (imports.has(module)) lines.push(importLine([...imports.get(module)].sort(), module));
186
- if (typeImports.has(module)) lines.push(importLine([...typeImports.get(module)].sort(), module, true));
186
+ if (imports.has(module)) lines.push(importLine([...imports.get(module)].sort(byCodeUnit), module));
187
+ if (typeImports.has(module)) lines.push(importLine([...typeImports.get(module)].sort(byCodeUnit), module, true));
187
188
  }
188
189
  lines.push(`import { ${shape.className} } from "${subject}"`);
189
190
  lines.push('');
@@ -265,8 +266,8 @@ function serviceSource({ name, dependencies = [] }) {
265
266
  }
266
267
  const lines = [];
267
268
  for (const module of [...new Set([...values.keys(), ...types.keys()])].sort(compareModules)) {
268
- if (values.has(module)) lines.push(importLine([...values.get(module)].sort(), module));
269
- if (types.has(module)) lines.push(importLine([...types.get(module)].sort(), module, true));
269
+ if (values.has(module)) lines.push(importLine([...values.get(module)].sort(byCodeUnit), module));
270
+ if (types.has(module)) lines.push(importLine([...types.get(module)].sort(byCodeUnit), module, true));
270
271
  }
271
272
  lines.push('', '@Injectable()', `/** The ${words(name).join(' ')} service: state what it decides in one sentence. */`);
272
273
  if (dependencies.length === 0) lines.push(`export class ${className} {}`);
package/sync/hygiene.mjs CHANGED
@@ -7,15 +7,22 @@
7
7
  // files; scripts/gates/hfs-sync.mjs judges every tracked file.
8
8
  //
9
9
  // Run inside a full runtime checkout — the repository under judgment is the checkout — it also reports the
10
- // state-root ledger findings of that checkout's scripts/housekeeping/hk-orphan-ledgers.mjs: LEDGER_ORPHAN_STATE_ROOT and
11
- // LEDGER_LEGACY_WORK_SQLITE (COOK-BRIEF F4 handover, incident 2026-09-30). Any other repository — a product repo,
10
+ // state-root ledger finding of that checkout's scripts/housekeeping/hk-orphan-ledgers.mjs: LEDGER_ORPHAN_STATE_ROOT. Any other repository — a product repo,
12
11
  // a bare repo — carries no scripts/housekeeping/ledger-hygiene.mjs at its root, so that section is silently absent:
13
12
  // never a crash, never machine-state findings blamed on a repository that does not own them.
14
- import { execFileSync } from 'node:child_process';
15
13
  import fs from 'node:fs';
16
14
  import path from 'node:path';
17
15
  import { pathToFileURL } from 'node:url';
18
16
  import { secretFileFindings } from '../runtime/scripts/hfs/rules/secrets.mjs';
17
+ import { runGit } from '../runtime/scripts/api/git/lib.mjs';
18
+
19
+ // One git call through the runtime's git owner (scripts/api/git/lib.mjs): stdout, or the failure thrown the way execFileSync threw it (error.status carries the exit code). The caller's own env is passed on unchanged: a hook runs with GIT_INDEX_FILE set, which the staged reads must keep seeing.
20
+ const git = (cwd, args, options = {}) => {
21
+ const run = runGit(args, { cwd, env: process.env, ...options });
22
+ if (run.error) throw run.error;
23
+ if (run.status !== 0) throw Object.assign(new Error(`git ${args[0]} exited ${run.status}`), { status: run.status });
24
+ return run.stdout;
25
+ };
19
26
 
20
27
  const PLAINTEXT_NAME = /(^|\/)(\.env(\..*)?|[^/]*\.(pem|key|identity|age))$/;
21
28
  /** The app root's work tree and stack tree, app-relative (`starci app hygiene` runs at the app root). */
@@ -27,7 +34,7 @@ const GUARDED = file => file.startsWith(WORK) || file.startsWith(STACKS);
27
34
  function ignoredAmong(cwd, files) {
28
35
  if (files.length === 0) return new Set();
29
36
  try {
30
- const out = execFileSync('git', ['check-ignore', '--no-index', '-z', '--stdin'], { cwd, input: files.join('\0'), encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
37
+ const out = git(cwd, ['check-ignore', '--no-index', '-z', '--stdin'], { input: files.join('\0'), maxBuffer: 64 * 1024 * 1024 });
31
38
  return new Set(out.split('\0').filter(Boolean));
32
39
  } catch (error) {
33
40
  if (error.status === 1) return new Set();
@@ -50,7 +57,7 @@ export function hygieneFindings(files, ignored) {
50
57
  return findings;
51
58
  }
52
59
 
53
- const gitList = (cwd, args) => execFileSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).split('\0').filter(Boolean);
60
+ const gitList = (cwd, args) => git(cwd, args, { maxBuffer: 64 * 1024 * 1024 }).split('\0').filter(Boolean);
54
61
 
55
62
  /** Files staged for the next commit (added, copied, modified, renamed). */
56
63
  export const stagedFiles = cwd => gitList(cwd, ['diff', '--cached', '--name-only', '--diff-filter=ACMR', '-z']);
@@ -63,7 +70,7 @@ const MAX_STAGED_BYTES = 1024 * 1024;
63
70
  /** The text of `file` as the index holds it (what the commit would record), or null when it is absent, binary or over 1 MB. */
64
71
  function stagedText(cwd, file) {
65
72
  try {
66
- const blob = execFileSync('git', ['show', `:${file}`], { cwd, maxBuffer: MAX_STAGED_BYTES * 2, stdio: ['ignore', 'pipe', 'ignore'] });
73
+ const blob = git(cwd, ['show', `:${file}`], { encoding: 'buffer', maxBuffer: MAX_STAGED_BYTES * 2 });
67
74
  return blob.length > MAX_STAGED_BYTES || blob.includes(0) ? null : blob.toString('utf8');
68
75
  } catch {
69
76
  return null;
@@ -89,7 +96,7 @@ export function judge(cwd, files) {
89
96
  // bare repo — has no such file, so the section is silently absent wherever this module happens to be installed.
90
97
  const ledgerHygieneScript = cwd => {
91
98
  try {
92
- const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' }).trim();
99
+ const root = git(cwd, ['rev-parse', '--show-toplevel']).trim();
93
100
  return path.join(root, 'scripts', 'housekeeping', 'ledger-hygiene.mjs');
94
101
  } catch {
95
102
  return null;
@@ -97,7 +104,7 @@ const ledgerHygieneScript = cwd => {
97
104
  };
98
105
 
99
106
  /**
100
- * The state-root ledger findings (LEDGER_ORPHAN_STATE_ROOT, LEDGER_LEGACY_WORK_SQLITE) of the checkout containing
107
+ * The state-root ledger findings (LEDGER_ORPHAN_STATE_ROOT) of the checkout containing
101
108
  * `cwd`, as {code, file, message} entries; [] when that checkout carries no ledger-hygiene script. Never throws: a
102
109
  * report failure is one HFS_LEDGER_HYGIENE_UNAVAILABLE finding, not a crash of `starci app hygiene`.
103
110
  */
@@ -109,7 +116,6 @@ async function ledgerHygieneFindings(cwd = process.cwd()) {
109
116
  const report = await ledgerHygieneReport({ apply: false });
110
117
  return [
111
118
  ...report.orphans.map(o => ({ code: o.code, file: o.ledgerId, message: `ledger ${o.name ?? o.ledgerId} - ${o.reason}; source roots: ${o.sourceRoots.join(', ') || '(none)'}` })),
112
- ...report.legacy.map(l => ({ code: l.code, file: l.repoRoot, message: `${l.files.length} legacy file(s) still in the repo: ${l.files.join(', ')}` })),
113
119
  ];
114
120
  } catch (error) {
115
121
  return [{ code: 'HFS_LEDGER_HYGIENE_UNAVAILABLE', file: script, message: `could not run: ${String(error?.message ?? error).slice(0, 200)}` }];
package/sync/index.mjs CHANGED
@@ -25,6 +25,7 @@ import { managedGroupOf } from '../runtime/scripts/hfs/edition-slots.mjs';
25
25
  import { feAppPackageName } from '../runtime/scripts/hfs/rules/monorepo.mjs';
26
26
  import { DEFAULT_PORT, NODE_IMAGE, dockerfilePath } from '../runtime/scripts/hfs/rules/docker.mjs';
27
27
  import { readDeclaredSonarKey } from './sonar-key.mjs';
28
+ import { byCodeUnit } from '../report/order.mjs';
28
29
 
29
30
  export const TEMPLATES_DIR = path.join(import.meta.dirname, '..', 'templates');
30
31
  const NODE_MAJOR = 22;
@@ -149,7 +150,7 @@ export function appSource(root) {
149
150
  }
150
151
  };
151
152
  for (const dir of ['apps', 'src']) walk(dir);
152
- return { files: files.sort(), read: file => fs.readFileSync(path.join(root, 'be', file), 'utf8') };
153
+ return { files: files.sort(byCodeUnit), read: file => fs.readFileSync(path.join(root, 'be', file), 'utf8') };
153
154
  }
154
155
 
155
156
  /**
@@ -241,10 +242,13 @@ export function imageFiles(app) {
241
242
  const side = app.sides.fe;
242
243
  const inputs = [...(opensPackages(side) ? ['fe/packages'] : []), ...((side.reads ?? []).includes('be/contracts/') ? ['be/contracts'] : [])];
243
244
  const copyInputs = inputs.map(folder => `COPY ${folder} ${folder}\n`).join('');
245
+ // The folders outside be/ that the back end declares it reads (the generated Supabase types of a lite back end, which an upgraded app keeps reading) are in every one of its images.
246
+ const beFolders = (app.sides.be.reads ?? []).map(folder => folder.replace(/\/$/, ''));
247
+ const copyBeInputs = beFolders.map(folder => `COPY ${folder} ${folder}\n`).join('');
244
248
  const entries = imageEntries(app).map(({ side: which, entry: item }) => ({ side: which, item }));
245
249
  return entries.map(({ side: which, item }) => {
246
250
  const template = IMAGE_TEMPLATE[item.kind];
247
- const vars = { project: app.project, app: item.name, nodeImage: NODE_IMAGE, port: DEFAULT_PORT, copyInputs };
251
+ const vars = { project: app.project, app: item.name, nodeImage: NODE_IMAGE, port: DEFAULT_PORT, copyInputs, copyBeInputs };
248
252
  return { path: dockerfilePath(which, item.name), content: render(readBundled(`${template}/Dockerfile`), vars) };
249
253
  });
250
254
  }
@@ -281,7 +285,7 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
281
285
  }
282
286
 
283
287
  // The scripts of a package.json as one comparable text: a line per script, sorted by name (order is not drift).
284
- const scriptsText = scripts => `${Object.keys(scripts).sort().map(name => `${name}: ${typeof scripts[name] === 'string' ? scripts[name] : JSON.stringify(scripts[name])}`).join('\n')}\n`;
288
+ const scriptsText = scripts => `${Object.keys(scripts).sort(byCodeUnit).map(name => `${name}: ${typeof scripts[name] === 'string' ? scripts[name] : JSON.stringify(scripts[name])}`).join('\n')}\n`;
285
289
 
286
290
  function parseScripts(text) {
287
291
  try {
@@ -24,12 +24,12 @@ jobs:
24
24
  env:
25
25
  SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
26
26
  steps:
27
- - uses: actions/checkout@v4
28
- - uses: actions/setup-node@v4
27
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
28
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
29
29
  with:
30
30
  node-version: {{nodeMajor}}
31
31
  cache: npm
32
- - run: npm ci
32
+ - run: npm ci --ignore-scripts
33
33
  - name: lint
34
34
  run: npm run lint -- --sonar reports/lint.sonar.json
35
35
  - name: format
@@ -39,7 +39,7 @@ jobs:
39
39
  - name: unit
40
40
  run: npm test -- --ci
41
41
  - name: coverage upload
42
- uses: codecov/codecov-action@v5
42
+ uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5
43
43
  if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/v') }}
44
44
  with:
45
45
  use_oidc: true
@@ -12,11 +12,11 @@ jobs:
12
12
  e2e:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@v4
16
- - uses: actions/setup-node@v4
15
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
16
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
17
17
  with:
18
18
  node-version: {{nodeMajor}}
19
19
  cache: npm
20
- - run: npm ci
20
+ - run: npm ci --ignore-scripts
21
21
  - name: e2e
22
22
  run: npm run test:e2e
@@ -25,9 +25,9 @@ jobs:
25
25
  include:
26
26
  {{imageMatrix}}
27
27
  steps:
28
- - uses: actions/checkout@v4
29
- - uses: docker/setup-buildx-action@v3
30
- - uses: docker/build-push-action@v6
28
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
29
+ - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
30
+ - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
31
31
  with:
32
32
  context: .
33
33
  file: ${{ matrix.file }}
@@ -1,8 +1,8 @@
1
- - uses: SonarSource/sonarqube-scan-action@v7
1
+ - uses: SonarSource/sonarqube-scan-action@c7ee0f9df90b7aa20e8dcf9695dcfe2e7da5b4f2 # v7.2.1
2
2
  if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/v') && env.SONAR_TOKEN != '' }}
3
3
  env:
4
4
  SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
5
- - uses: SonarSource/sonarqube-quality-gate-action@v1
5
+ - uses: SonarSource/sonarqube-quality-gate-action@7a5fffe8e523c40e0c740b6bc2712ab503e52efa # v1.2.1
6
6
  if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/v') && env.SONAR_TOKEN != '' }}
7
7
  timeout-minutes: 10
8
8
  env: