@starci/hfs 4.0.10 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +65 -24
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +194 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +222 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/release-config.mjs +28 -0
  14. package/runtime/engine/removed-vocabulary.mjs +28 -0
  15. package/runtime/engine/resources-config.mjs +52 -0
  16. package/runtime/engine/runtime-root.mjs +17 -0
  17. package/runtime/engine/secrets.mjs +35 -31
  18. package/runtime/engine/sonar-config.mjs +21 -0
  19. package/runtime/engine/temp-root.mjs +32 -0
  20. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +161 -65
  23. package/runtime/knowledge/hfs/slots.yaml +11 -14
  24. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  25. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  26. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  28. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
  30. package/runtime/modules/models/registry.yaml +25 -359
  31. package/runtime/modules/models/runtimes.yaml +126 -265
  32. package/runtime/modules/models/tiers.yaml +66 -0
  33. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  34. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  35. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  36. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  37. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  38. package/runtime/scripts/api/git/lib.mjs +34 -1
  39. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  40. package/runtime/scripts/api/process/run-program.mjs +2 -1
  41. package/runtime/scripts/api/sops/lib.mjs +198 -120
  42. package/runtime/scripts/hfs/allows.mjs +3 -3
  43. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  44. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  45. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  46. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  47. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  48. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  49. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  50. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  51. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  52. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  53. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  54. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  55. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  56. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  57. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  58. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  59. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  60. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  61. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  62. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  63. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  64. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  65. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  66. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  67. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  68. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  69. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  70. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  71. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  72. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  74. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  75. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  76. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  77. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  78. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  79. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  80. package/runtime/scripts/hfs/architecture/index.mjs +164 -104
  81. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  82. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  83. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  84. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  85. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  86. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  87. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  88. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  89. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  90. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  91. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  92. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  93. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  94. package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
  95. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  96. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  97. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  98. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  99. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  100. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  101. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  102. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  103. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  104. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  105. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  106. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  107. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  108. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  109. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  110. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  111. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  112. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  113. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  114. package/runtime/scripts/hfs/architecture.mjs +3 -4
  115. package/runtime/scripts/hfs/catalog-once.mjs +15 -0
  116. package/runtime/scripts/hfs/check.mjs +63 -71
  117. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  118. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  119. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  120. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  121. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  122. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  123. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  124. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  125. package/runtime/scripts/hfs/repo-identity.mjs +21 -4
  126. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  127. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  128. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  129. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  130. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  131. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  132. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  133. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  134. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  135. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  136. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  137. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  138. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  139. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  140. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  141. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  142. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  143. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  144. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  145. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  146. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  147. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  148. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  149. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  150. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  151. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  152. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  153. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  154. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  155. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  156. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  157. package/runtime/scripts/hfs/secret.mjs +59 -48
  158. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  159. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  160. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  161. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  162. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  163. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  164. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  165. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  166. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  167. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  168. package/runtime/scripts/hfs/slots.mjs +74 -695
  169. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  170. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  171. package/runtime/scripts/hfs/tree.mjs +19 -14
  172. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  173. package/runtime/scripts/hfs/view.mjs +1 -1
  174. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  175. package/runtime/scripts/lib/env.mjs +10 -0
  176. package/runtime/scripts/lib/event-contract.mjs +17 -13
  177. package/runtime/scripts/lib/git.mjs +2 -2
  178. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  179. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  180. package/runtime/scripts/lib/i18n.mjs +3 -2
  181. package/runtime/scripts/lib/in-order.mjs +71 -0
  182. package/runtime/scripts/lib/language.mjs +3 -3
  183. package/runtime/scripts/lib/list.mjs +8 -1
  184. package/runtime/scripts/lib/path-key.mjs +38 -5
  185. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  186. package/runtime/scripts/lib/regex.mjs +1 -1
  187. package/runtime/scripts/lib/same-text.mjs +1 -1
  188. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  189. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  190. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  191. package/runtime/scripts/lib/stack-services.mjs +1 -2
  192. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  193. package/runtime/scripts/lib/walk.mjs +6 -1
  194. package/runtime/scripts/lib/yaml-cached.mjs +14 -0
  195. package/scaffold/add-cli-lite.mjs +2 -1
  196. package/scaffold/add-table.mjs +2 -2
  197. package/scaffold/add.mjs +3 -2
  198. package/scaffold/app.mjs +4 -2
  199. package/scaffold/edition-gate.mjs +25 -25
  200. package/scaffold/lite-exports.mjs +2 -1
  201. package/scaffold/service.mjs +5 -4
  202. package/sync/hygiene.mjs +15 -9
  203. package/sync/index.mjs +7 -3
  204. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  205. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  206. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  207. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  208. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  209. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  210. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  211. package/templates/app/starciwork.gitignore +1 -1
  212. package/templates/be/image/api/Dockerfile +1 -1
  213. package/templates/be/image/cli/Dockerfile +1 -1
  214. package/templates/be/image/worker/Dockerfile +1 -1
  215. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  216. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  217. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  218. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  219. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  220. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  221. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  222. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  223. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  224. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  225. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  226. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  228. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  229. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  230. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  231. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  232. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  233. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  234. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  235. package/templates/fe/image/next/Dockerfile +2 -1
  236. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  238. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  239. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  242. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  243. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  245. package/upgrade/index.mjs +9 -2
  246. package/runtime/engine/admission.mjs +0 -308
  247. package/runtime/engine/canonical-json.mjs +0 -10
  248. package/runtime/engine/db/blob.mjs +0 -315
  249. package/runtime/engine/db/ledger-paths.mjs +0 -83
  250. package/runtime/engine/db/ledger.mjs +0 -1118
  251. package/runtime/engine/db/machine-connection.mjs +0 -135
  252. package/runtime/engine/db/machine-schema.mjs +0 -84
  253. package/runtime/engine/db/machine.mjs +0 -1367
  254. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  255. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  256. package/runtime/engine/db/provider-reservations.mjs +0 -101
  257. package/runtime/engine/digest.mjs +0 -16
  258. package/runtime/engine/refuse.mjs +0 -11
  259. package/runtime/modules/ops/_labels.yaml +0 -53
  260. package/runtime/scripts/api/node/lib.mjs +0 -14
  261. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  262. package/runtime/scripts/api/process/lib.mjs +0 -111
  263. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  264. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  265. package/runtime/scripts/connectors/lib.mjs +0 -488
  266. package/runtime/scripts/lib/clip.mjs +0 -19
  267. package/runtime/scripts/lib/display-names.mjs +0 -259
  268. package/runtime/scripts/lib/example-refs.mjs +0 -158
  269. package/runtime/scripts/lib/json-schema.mjs +0 -52
  270. package/runtime/scripts/lib/process-identity.mjs +0 -6
  271. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  272. package/runtime/scripts/lib/redact.mjs +0 -161
  273. package/runtime/scripts/lib/sleep.mjs +0 -7
  274. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  275. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -1,15 +1,15 @@
1
1
  import path from 'node:path';
2
2
  import {isPlainObject as plain} from './plain-object.mjs';
3
3
  /** The one `Invalid config.yaml:` raiser every section validator shares: bad('<rest>') throws it. */
4
- export const invalid=section=>message=>{throw Error(`Invalid config.yaml: ${section}${message}`);};
4
+ export const invalid=section=>message=>{throw new Error(`Invalid config.yaml: ${section}${message}`);};
5
5
 
6
- /** The host roots the owner may relocate (config.yaml `roots`): the archive root and the lanes root. */
7
- const ROOT_KEYS=Object.freeze(['archive','lanes']);
8
- /** config.yaml `roots` - {archive?, lanes?}: absolute directories, or null; an absent key means <starciLocalRoot>/archive and <starciLocalRoot>/lanes. */
6
+ /** The host roots the owner may relocate (config.yaml `roots`): the archive root, the lanes root and the temp root. */
7
+ export const ROOT_KEYS=Object.freeze(['archive','lanes','temp']);
8
+ /** config.yaml `roots` - {archive?, lanes?, temp?}: absolute directories, or null; an absent key means <starciLocalRoot>/archive and the per-user lanes directory (scripts/machine/home.mjs lanesDefault). */
9
9
  export function validateRoots(roots){
10
10
  if(roots===null)return;
11
11
  const bad=invalid('roots');
12
- if(!plain(roots))bad(` must be {archive?: <absolute directory>, lanes?: <absolute directory>} or null.`);
12
+ if(!plain(roots))bad(` must be {archive?: <absolute directory>, lanes?: <absolute directory>, temp?: <absolute directory>} or null.`);
13
13
  for(const key of Object.keys(roots))if(!ROOT_KEYS.includes(key))bad(` has unknown key ${key} (allowed: ${ROOT_KEYS.join(', ')}).`);
14
14
  for(const key of ROOT_KEYS)if(roots[key]!==undefined&&roots[key]!==null&&!(typeof roots[key]==='string'&&roots[key].trim()&&path.isAbsolute(roots[key])))bad(`.${key} must be an absolute directory path or null.`);
15
15
  }
@@ -0,0 +1,90 @@
1
+ import fs from 'node:fs';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {parseYaml} from './yaml.mjs';
4
+ import {isPlainObject as plain} from './plain-object.mjs';
5
+ import {invalid} from './invalid-config.mjs';
6
+ import {byCodeUnit} from './by-code-unit.mjs';
7
+ import {removedOfKind} from './removed-vocabulary.mjs';
8
+
9
+ /** The effort vocabulary, ordered weakest to strongest — the only list of it. */
10
+ export const EFFORT_LEVELS=new Set(['none','minimal','low','medium','high','xhigh','max','ultra']);
11
+ const DIFFICULTIES=['easy','medium','hard','insane'];
12
+ export const MODELS_KEYS=['tiers','seats','balance','usage'];
13
+ const known=names=>[...names].sort(byCodeUnit).join(', ');
14
+
15
+ const hasPath=(root,dotted)=>dotted.split('.').reduce((node,key)=>(plain(node)&&Object.hasOwn(node,key)?node[key]:undefined),root)!==undefined;
16
+
17
+ /** Refuse an old-shape config (the removed keys of modules/kernel/removed-vocabulary.yaml), naming every removed key and where its meaning lives now. */
18
+ export function refuseRemovedKeys(config){
19
+ const found=removedOfKind('config-key').filter(({name})=>hasPath(config,name));
20
+ if(!found.length)return;
21
+ const removed=found.map(({name,use})=>`${name} is removed (${use})`).join('; ');
22
+ throw new Error(`Invalid config.yaml: ${removed}.`);
23
+ }
24
+
25
+ let shippedCache=null;
26
+ /** modules/models/tiers.yaml as shipped, parsed once per file version. */
27
+ export function shippedTiers(){
28
+ const file=fileURLToPath(new URL('../modules/models/tiers.yaml',import.meta.url));
29
+ let stat;try{stat=fs.statSync(file);}catch{throw new Error('Missing modules/models/tiers.yaml');}
30
+ const version=`${stat.mtimeMs}:${stat.size}`;
31
+ if(shippedCache?.version!==version)shippedCache={version,doc:parseYaml(fs.readFileSync(file,'utf8'))};
32
+ return structuredClone(shippedCache.doc);
33
+ }
34
+
35
+ function validateMember(bad,where,member,profile){
36
+ if(!plain(member)||Object.keys(member).some(key=>!['agent','model','effort'].includes(key))||typeof member.agent!=='string'||typeof member.model!=='string')
37
+ bad(`${where} must be {agent, model, effort?}.`);
38
+ if(profile.models?.[member.model]?.provider!==member.agent)bad(`${where}: model ${member.model} is not declared by agent ${member.agent} in modules/models/registry.yaml models.`);
39
+ if(member.effort!==undefined&&!EFFORT_LEVELS.has(member.effort))bad(`${where}.effort must use the effort vocabulary.`);
40
+ }
41
+
42
+ function validateChains(bad,tiers,profile){
43
+ for(const [name,chain] of Object.entries(tiers)){
44
+ if(!Array.isArray(chain)||!chain.length)bad(`.${name} must be a non-empty ordered list of {agent, model, effort?}.`);
45
+ chain.forEach((member,index)=>validateMember(bad,`.${name}[${index}]`,member,profile));
46
+ if(new Set(chain.map(member=>`${member.agent}/${member.model}`)).size!==chain.length)bad(`.${name} names each member once.`);
47
+ }
48
+ }
49
+
50
+ /** A call tier is taken by a headless call only: no seat, difficulty, kind, order or caller reference may name it, and every call names one. */
51
+ function validateUse(bad,{doc,names,refs}){
52
+ const callTiers=new Set(Object.entries(doc.tierUse).filter(([,use])=>use==='call').map(([tier])=>tier));
53
+ for(const tier of Object.keys(doc.tierUse))if(!names.has(tier))bad(`: tierUse names tier ${tier} (known: ${known(names)}).`);
54
+ for(const [where,tier] of refs)if(callTiers.has(tier))bad(`: ${where} names tier ${tier}, a call tier (tierUse): a headless call is made on it, no seat or op is seated on it.`);
55
+ for(const [call,spec] of Object.entries(doc.calls))if(!callTiers.has(spec.tier))bad(`: calls.${call}.tier ${spec.tier} must be a tier whose tierUse is call.`);
56
+ }
57
+
58
+ /** The tier document the picker reads: the shipped tiers.yaml with config.yaml `models` laid over it. Validated. */
59
+ export function effectiveTiers(config,profile){
60
+ const doc=shippedTiers(),models=plain(config?.models)?config.models:{};
61
+ const bad=invalid('models');
62
+ const tiers={...doc.tiers,...models.tiers};
63
+ validateChains(bad,tiers,profile);
64
+ const seats={...doc.seats,...models.seats};
65
+ const balance={...doc.balance,...models.balance};
66
+ const usage={...doc.usage,...models.usage};
67
+ const names=new Set(Object.keys(tiers));
68
+ const refs=[...Object.entries(seats).map(([seat,tier])=>[`.seats.${seat}`,tier]),...DIFFICULTIES.map(level=>[`difficulty.${level}`,doc.difficulty[level]]),
69
+ ...Object.entries(doc.kindTiers).map(([kind,tier])=>[`kindTiers.${kind}`,tier]),...doc.tierOrder.map(tier=>['tierOrder',tier])];
70
+ for(const [where,tier] of refs)if(!names.has(tier))bad(`: ${where} names tier ${tier} (known: ${known(names)}).`);
71
+ validateUse(bad,{doc,names,refs});
72
+ if(!(Number.isInteger(balance.maxStreak)&&balance.maxStreak>=1))bad('.balance.maxStreak must be an integer >= 1.');
73
+ if(!(typeof balance.maxSharePercent==='number'&&balance.maxSharePercent>0&&balance.maxSharePercent<=100))bad('.balance.maxSharePercent must be a number in (0, 100].');
74
+ const {reservePercent,biasPercent,exhaustedPercent}=usage;
75
+ if(![reservePercent,biasPercent,exhaustedPercent].every(value=>typeof value==='number'&&value>=0&&value<=100)||!(reservePercent<=biasPercent&&biasPercent<exhaustedPercent))
76
+ bad('.usage must hold numbers with reservePercent <= biasPercent < exhaustedPercent, each within 0..100.');
77
+ return {...doc,tiers,seats,balance,usage};
78
+ }
79
+
80
+ /** config.yaml `models` — {tiers?, seats?, balance?, usage?}; the shipped defaults make every key optional. */
81
+ export function validateModelsBlock(config,profile){
82
+ refuseRemovedKeys(config);
83
+ const models=config?.models;
84
+ if(models===undefined||models===null)return;
85
+ const bad=invalid('models');
86
+ if(!plain(models)||Object.keys(models).some(key=>!MODELS_KEYS.includes(key)))bad(` must be {${MODELS_KEYS.map(key=>key+'?').join(', ')}} or null (unknown keys are refused).`);
87
+ if(models.tiers!==undefined&&!plain(models.tiers))bad('.tiers must map tier names to ordered member lists.');
88
+ for(const key of ['seats','balance','usage'])if(models[key]!==undefined&&!plain(models[key]))bad(`.${key} must be a mapping.`);
89
+ effectiveTiers(config,profile);
90
+ }
@@ -10,12 +10,14 @@ import {invalid} from './invalid-config.mjs';
10
10
  * a measured probe (scripts/agent/depth-probe.mjs).
11
11
  */
12
12
  export const ORCA_DEFAULTS=Object.freeze({maxWorkerDepth:4});
13
+ /** The keys of the orca block. */
14
+ export const ORCA_KEYS=Object.freeze(Object.keys(ORCA_DEFAULTS));
13
15
  export const MAX_WORKER_DEPTH_CEILING=16;
14
16
  export function validateOrca(orca){
15
17
  if(orca===null)return;
16
18
  const bad=invalid('orca');
17
19
  if(!plain(orca))bad(' must be {maxWorkerDepth?} or null.');
18
- for(const key of Object.keys(orca))if(key!=='maxWorkerDepth')bad(` has unknown key ${key} (allowed: maxWorkerDepth).`);
20
+ for(const key of Object.keys(orca))if(!ORCA_KEYS.includes(key))bad(` has unknown key ${key} (allowed: ${ORCA_KEYS.join(', ')}).`);
19
21
  const v=orca.maxWorkerDepth;
20
22
  if(v!==undefined&&v!==null&&!(Number.isInteger(v)&&v>=1&&v<=MAX_WORKER_DEPTH_CEILING))bad(`.maxWorkerDepth must be an integer from 1 to ${MAX_WORKER_DEPTH_CEILING} equal to the Orca app's worker depth setting (default ${ORCA_DEFAULTS.maxWorkerDepth}), or null.`);
21
23
  }
@@ -0,0 +1,28 @@
1
+ import {isPlainObject as plain} from './plain-object.mjs';
2
+ import {invalid} from './invalid-config.mjs';
3
+
4
+ /** The keys of the release block. */
5
+ export const RELEASE_KEYS=Object.freeze(['suite']);
6
+ /** Where the full test suite of a release is judged: `local` (the release cut runs it, the shipped default) or `ci` (the GitHub workflow runs it after the push; the cut runs only the affected specs of the release and the rows CI cannot run). */
7
+ export const SUITE_MODES=Object.freeze(['local','ci']);
8
+ export const RELEASE_DEFAULTS=Object.freeze({suite:'local'});
9
+
10
+ /**
11
+ * config.yaml `release` - {suite?: local | ci}: who judges the full suite of a release (docs/releasing.md "Where the suite runs"). `local` is the shipped
12
+ * default: `starci release cut` runs the root suite and the Linux parity container before it pushes. `ci` is the owner's recorded choice (2026-10-09) that
13
+ * the full suite runs on GitHub only: the cut runs no root suite and no Linux container, and the CI workflow's verdict is read afterwards (`starci release ci-status`).
14
+ * `none` is refused: the suite does not vanish, it moves to CI, and the value says so.
15
+ */
16
+ export function validateRelease(release){
17
+ if(release===null)return;
18
+ const bad=invalid('release');
19
+ if(!plain(release))bad(` must be {suite?: ${SUITE_MODES.join(' | ')}} or null.`);
20
+ for(const key of Object.keys(release))if(!RELEASE_KEYS.includes(key))bad(` has unknown key ${key} (allowed: ${RELEASE_KEYS.join(', ')}).`);
21
+ const value=release.suite;
22
+ if(value===undefined||value===null)return;
23
+ if(value==='none')bad('.suite: "none" is not a mode: write "ci" (no local full suite, CI judges) or "local".');
24
+ if(!SUITE_MODES.includes(value))bad(`.suite must be ${SUITE_MODES.join(' or ')}, or null.`);
25
+ }
26
+
27
+ /** The suite mode a validated owner config names; an absent block or key is the shipped default. */
28
+ export const releaseSuiteMode=config=>config?.release?.suite??RELEASE_DEFAULTS.suite;
@@ -0,0 +1,28 @@
1
+ import fs from 'node:fs';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {parseYaml} from './yaml.mjs';
4
+
5
+ const FILE=fileURLToPath(new URL('../modules/kernel/removed-vocabulary.yaml',import.meta.url));
6
+
7
+ let cache=null;
8
+ function loaded(){
9
+ let stat;try{stat=fs.statSync(FILE);}catch{throw new Error('Missing modules/kernel/removed-vocabulary.yaml');}
10
+ const version=`${stat.mtimeMs}:${stat.size}`;
11
+ if(cache?.version!==version)cache={version,doc:parseYaml(fs.readFileSync(FILE,'utf8'))};
12
+ return cache.doc;
13
+ }
14
+
15
+ /** The kinds a removed spelling has. */
16
+ export const removedKinds=()=>loaded().kinds;
17
+
18
+ /** modules/kernel/removed-vocabulary.yaml: the one list of spellings the runtime refuses, each with its replacement and the release that removed it. */
19
+ export const removedVocabulary=()=>loaded().removed;
20
+
21
+ /** The removed spellings of one kind. */
22
+ export const removedOfKind=kind=>removedVocabulary().filter(entry=>entry.kind===kind);
23
+
24
+ /** `<name> is removed (<replacement>)` for a removed spelling of the kind, or undefined when the name is not one. */
25
+ export function removedNotice(kind,name){
26
+ const entry=removedOfKind(kind).find(candidate=>candidate.name===name);
27
+ return entry?`${entry.name} is removed (${entry.use})`:undefined;
28
+ }
@@ -0,0 +1,52 @@
1
+ import {isPlainObject as plain} from './plain-object.mjs';
2
+ import {invalid} from './invalid-config.mjs';
3
+
4
+ /**
5
+ * config.yaml `resources` — the owner's host-capacity floors, which override the shipped numbers of
6
+ * modules/models/runtimes.yaml `allocation.resources`. minFreeDiskGb: the free space a drive holding the temp root or the
7
+ * repository must keep before `starci kernel dispatch --spawn` launches another worker. minFreeDiskPct: the same floor as a
8
+ * percentage of the drive's size; when both are set the larger requirement applies. minFreeRamPct: the free-RAM floor below
9
+ * which no new heavy op starts. A null or absent key leaves the shipped number.
10
+ */
11
+ const FLOOR_LIMITS=Object.freeze({minFreeDiskGb:Infinity,minFreeDiskPct:100,minFreeRamPct:100});
12
+ const inRange=(value,limit)=>typeof value==='number'&&Number.isFinite(value)&&value>0&&value<=limit;
13
+
14
+ /** The keys of the resources block. */
15
+ export const RESOURCE_KEYS=Object.freeze(Object.keys(FLOOR_LIMITS));
16
+
17
+ export function validateResources(resources){
18
+ if(resources===null)return;
19
+ const bad=invalid('resources');
20
+ const keys=RESOURCE_KEYS;
21
+ const shape=keys.map(key=>`${key}?`).join(', ');
22
+ if(!plain(resources))bad(` must be {${shape}} or null.`);
23
+ for(const key of Object.keys(resources))if(!keys.includes(key))bad(` has unknown key ${key} (allowed: ${keys.join(', ')}).`);
24
+ for(const key of keys){
25
+ const value=resources[key];
26
+ if(value!==undefined&&value!==null&&!inRange(value,FLOOR_LIMITS[key])){
27
+ const cap=FLOOR_LIMITS[key]===Infinity?'':` and at most ${FLOOR_LIMITS[key]}`;
28
+ bad(`.${key} must be a number above 0${cap}, or null.`);
29
+ }
30
+ }
31
+ }
32
+
33
+ const declared=(block,key,source)=>{
34
+ const value=block?.[key];
35
+ if(value===undefined||value===null)return null;
36
+ if(!inRange(value,FLOOR_LIMITS[key]))throw new Error(`${source}.${key} must be a number above 0 (at most ${FLOOR_LIMITS[key]})`);
37
+ return value;
38
+ };
39
+
40
+ /**
41
+ * The floors in force: the owner's `resources` (config.yaml) over the shipped `allocation.resources`.
42
+ * {minFreeDiskGb, minFreeDiskPct (null when unset), minFreeRamPct}. The shipped policy owns the default numbers: when it
43
+ * omits or misspells minFreeDiskGb or minFreeRamPct this refuses, there is no fallback constant.
44
+ */
45
+ export function hostFloors(shipped,owner=null){
46
+ const source='modules/models/runtimes.yaml allocation.resources';
47
+ const base={minFreeDiskGb:declared(shipped,'minFreeDiskGb',source),minFreeDiskPct:declared(shipped,'minFreeDiskPct',source),minFreeRamPct:declared(shipped,'minFreeRamPct',source)};
48
+ for(const key of ['minFreeDiskGb','minFreeRamPct'])if(base[key]===null)throw new Error(`${source}.${key} must declare a positive number`);
49
+ if(owner!==null&&owner!==undefined)validateResources(owner);
50
+ const pick=key=>declared(owner,key,'config.yaml resources')??base[key];
51
+ return {minFreeDiskGb:pick('minFreeDiskGb'),minFreeDiskPct:pick('minFreeDiskPct'),minFreeRamPct:pick('minFreeRamPct')};
52
+ }
@@ -22,6 +22,23 @@ export const skillRoot = moduleRoot;
22
22
  */
23
23
  export const starciSourceRoot = (env = process.env) => (env.STARCI_SOURCE_ROOT ? path.resolve(env.STARCI_SOURCE_ROOT) : path.dirname(skillRoot));
24
24
 
25
+ /** Overrides the per-host state base wholesale (debug probes, spec trees); the one seam starciLocalRoot reads. */
26
+ export const LOCAL_ROOT_ENV = 'STARCI_LOCAL_ROOT';
27
+ /** The host-data directory of a runtime root. Git-ignored and never packed; the example fixtures under examples/.runtimes are a separate, public exception. */
28
+ export const RUNTIME_STATE_DIR = '.runtime';
29
+ /**
30
+ * <root>/.runtime. The root is the `.claude` of the host Source: the checkout, or the copy `starci runtime install` places at
31
+ * <app>/.claude. ~/.starci/runtime/node_modules/starci is only the CLI's download cache (it supplies the installer and hosts no
32
+ * state), and `starci runtime link` merely points the launcher at a root, so the state follows the code's own location.
33
+ */
34
+ export const runtimeStateDir = (root = skillRoot) => path.join(root, RUNTIME_STATE_DIR);
35
+ /**
36
+ * The per-host state base, the one owner of every host-state location: machine.sqlite, projects/<ledger_id>/runtime.sqlite, archive/,
37
+ * artifacts/ (the blob store), guards/, host-lock/ and the other process state sit under it. Default <runtime root>/.runtime;
38
+ * LOCAL_ROOT_ENV replaces it wholesale. Nothing is read from or moved out of the earlier %LOCALAPPDATA%/StarCi and ~/.starci locations.
39
+ */
40
+ export const starciLocalRoot = (env = process.env, root = skillRoot) => (env[LOCAL_ROOT_ENV] ? path.resolve(env[LOCAL_ROOT_ENV]) : runtimeStateDir(root));
41
+
25
42
  /**
26
43
  * Read a runtime contract document. `parts` are path segments under `skillRoot`
27
44
  * (e.g. readModuleJson('modules', 'models', 'kinds.yaml')).
@@ -37,7 +37,7 @@ export function readSecretBytes(file){
37
37
  if(!stat.isFile()||stat.dev!==before.dev||stat.ino!==before.ino)throw new Error('credential file changed during resolution');
38
38
  bytes=Buffer.alloc(CREDENTIAL_FILE_MAX_BYTES+1);
39
39
  let count=0;
40
- while(count<bytes.length){const n=fs.readSync(fd,bytes,count,bytes.length-count,null);if(n===0)break;count+=n;}
40
+ while(count<bytes.length){const n=fs.readSync(fd,bytes,count,bytes.length-count,null);if(n===0){break;}count+=n;}
41
41
  if(count>CREDENTIAL_FILE_MAX_BYTES)throw new Error('credential file exceeds the byte budget');
42
42
  return bytes.subarray(0,count);
43
43
  }catch(error){bytes?.fill(0);throw error;}finally{fs.closeSync(fd);}
@@ -45,15 +45,18 @@ export function readSecretBytes(file){
45
45
 
46
46
  const readCredentialFile = file => { const bytes=readSecretBytes(file); try{return bytes.toString('utf8');}finally{bytes.fill(0);} };
47
47
 
48
+ const DOTENV_LINE_PARTS = [String.raw`^\s*`, String.raw`(?:export\s+)?`, String.raw`([A-Za-z_]\w*)`, String.raw`\s*=\s*`, String.raw`([^\r\n\u2028\u2029]*)`, String.raw`\s*$`];
49
+ const DOTENV_LINE = new RegExp(DOTENV_LINE_PARTS.join(''));
50
+
48
51
  /** Parse a dotenv file (KEY=VALUE lines, # comments, optional export/quotes). An absent file is {}. */
49
52
  export function readDotenv(file){
50
- let text='';try{text=readCredentialFile(file);}catch(error){if(error?.code==='ENOENT')return {};throw error;}
53
+ let text='';try{text=readCredentialFile(file);}catch(error){if(error?.code==='ENOENT'){return {};}throw error;}
51
54
  const out={};
52
55
  for(const line of text.split(/\r?\n/)){
53
- const m=line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
56
+ const m=DOTENV_LINE.exec(line);
54
57
  if(!m)continue;
55
- let value=m[2];
56
- if(value.length>=2&&(value[0]==='"'||value[0]==="'")&&value.at(-1)===value[0])value=value.slice(1,-1);
58
+ let value=m[2].trimEnd();
59
+ if(value.length>=2&&(value.startsWith('"')||value.startsWith("'"))&&value.at(-1)===value[0])value=value.slice(1,-1);
57
60
  out[m[1]]=value;
58
61
  }
59
62
  return out;
@@ -84,7 +87,7 @@ export function secretEnv(verifiedRuntimeRoot,env=process.env){
84
87
  if(!root.isDirectory()||root.isSymbolicLink())throw new Error('secretEnv runtime root must be a regular directory');
85
88
  const file=path.join(verifiedRuntimeRoot,SECRET_ENV_FILE);
86
89
  let stat;
87
- try{stat=fs.lstatSync(file);}catch(error){if(error?.code==='ENOENT')return {...env};throw error;}
90
+ try{stat=fs.lstatSync(file);}catch(error){if(error?.code==='ENOENT'){return {...env};}throw error;}
88
91
  if(!stat.isFile()||stat.isSymbolicLink())throw new Error('secret.env must be a regular file');
89
92
  const local = readDotenv(file);
90
93
  const resolved = {...local,...env};
@@ -92,39 +95,40 @@ export function secretEnv(verifiedRuntimeRoot,env=process.env){
92
95
  return resolved;
93
96
  }
94
97
 
98
+ const sopsRefuse=(reason,message)=>{
99
+ const error=new Error(`SOPS identity [${reason}]: ${message}`);
100
+ error.name='SopsIdentityRefusal';error.identityRefusal=reason;
101
+ return {mode:'refused',env:null,error};
102
+ };
103
+ const sopsInlineIdentity=(env,name)=>{
104
+ if(!credentialPresent(env[name]))return sopsRefuse('disabled-inline','SOPS_AGE_KEY is explicitly empty or invalid');
105
+ remember(env[name].trim());
106
+ return {...sopsRefuse('inline-context-unqualified','the supplied SOPS_AGE_KEY awaits an owned selected-identity isolation capability; no SOPS process was launched'),inlineName:name};
107
+ };
108
+ const sopsFileIdentity=(env,file)=>{
109
+ if(!credentialPresent(file))return sopsRefuse('invalid-file','the original identity file selection is empty or invalid');
110
+ if(!path.isAbsolute(file))return sopsRefuse('identity-file-location-unqualified','select the original identity by its absolute path; caller and SOPS working directories may differ');
111
+ try{
112
+ const stat=fs.lstatSync(file);
113
+ if(!stat.isFile()||stat.isSymbolicLink())return sopsRefuse('identity-file-unavailable','the explicitly selected original identity must be a regular non-symlink file');
114
+ }catch{return sopsRefuse('identity-file-unavailable','the explicitly selected original identity file is unavailable');}
115
+ return {mode:'file',env:{...env,SOPS_AGE_KEY_FILE:file},error:null};
116
+ };
95
117
  /** Select a supplied age identity without inventing custody or running a key tool. Inline execution awaits its owned isolation capability. */
96
118
  export function sopsIdentityEnv(env,{identity=null,required=false,platform=process.platform}={}){
97
- const refuse=(reason,message)=>{
98
- const error=new Error(`SOPS identity [${reason}]: ${message}`);
99
- error.name='SopsIdentityRefusal';error.identityRefusal=reason;
100
- return {mode:'refused',env:null,error};
101
- };
102
- if(!env||typeof env!=='object'||Array.isArray(env))return refuse('invalid-environment','an environment mapping is required');
119
+ if(!env||typeof env!=='object'||Array.isArray(env))return sopsRefuse('invalid-environment','an environment mapping is required');
103
120
  const names=name=>Object.keys(env).filter(key=>platform==='win32'?key.toUpperCase()===name:key===name);
104
121
  const inline=names('SOPS_AGE_KEY'),files=names('SOPS_AGE_KEY_FILE');
105
- if(inline.length>1||files.length>1)return refuse('ambiguous-environment','multiple Windows aliases select an age identity');
106
- if(inline.length){
107
- const name=inline[0];
108
- if(!credentialPresent(env[name]))return refuse('disabled-inline','SOPS_AGE_KEY is explicitly empty or invalid');
109
- remember(env[name].trim());
110
- return {...refuse('inline-context-unqualified','the supplied SOPS_AGE_KEY awaits an owned selected-identity isolation capability; no SOPS process was launched'),inlineName:name};
111
- }
122
+ if(inline.length>1||files.length>1)return sopsRefuse('ambiguous-environment','multiple Windows aliases select an age identity');
123
+ if(inline.length)return sopsInlineIdentity(env,inline[0]);
112
124
  let file=identity;
113
125
  if(files.length){
114
126
  const value=env[files[0]];
115
- if(!credentialPresent(value))return refuse('disabled-file','SOPS_AGE_KEY_FILE is explicitly empty or invalid');
116
- if(identity!==null&&identity!==undefined&&identity!==value)return refuse('conflicting-files','two different original identity files were explicitly selected');
127
+ if(!credentialPresent(value))return sopsRefuse('disabled-file','SOPS_AGE_KEY_FILE is explicitly empty or invalid');
128
+ if(identity!==null&&identity!==undefined&&identity!==value)return sopsRefuse('conflicting-files','two different original identity files were explicitly selected');
117
129
  file=value;
118
130
  }
119
- if(file!==null&&file!==undefined){
120
- if(!credentialPresent(file))return refuse('invalid-file','the original identity file selection is empty or invalid');
121
- if(!path.isAbsolute(file))return refuse('identity-file-location-unqualified','select the original identity by its absolute path; caller and SOPS working directories may differ');
122
- try{
123
- const stat=fs.lstatSync(file);
124
- if(!stat.isFile()||stat.isSymbolicLink())return refuse('identity-file-unavailable','the explicitly selected original identity must be a regular non-symlink file');
125
- }catch{return refuse('identity-file-unavailable','the explicitly selected original identity file is unavailable');}
126
- return {mode:'file',env:{...env,SOPS_AGE_KEY_FILE:file},error:null};
127
- }
128
- if(required)return refuse('identity-not-supplied','supply SOPS_AGE_KEY locally or select the original SOPS_AGE_KEY_FILE; no home default was inferred');
131
+ if(file!==null&&file!==undefined)return sopsFileIdentity(env,file);
132
+ if(required)return sopsRefuse('identity-not-supplied','supply SOPS_AGE_KEY locally or select the original SOPS_AGE_KEY_FILE; no home default was inferred');
129
133
  return {mode:'public-recipient',env:{...env},error:null};
130
134
  }
@@ -0,0 +1,21 @@
1
+ import {isPlainObject as plain} from './plain-object.mjs';
2
+ import {invalid} from './invalid-config.mjs';
3
+
4
+ /** The keys of the sonar block. */
5
+ const SONAR_KEYS=Object.freeze(['organization']);
6
+ /** A SonarCloud organization key: lowercase letters, digits, hyphens and underscores, starting with a letter or a digit. */
7
+ const ORGANIZATION=/^[a-z0-9][a-z0-9_-]*$/;
8
+
9
+ /**
10
+ * config.yaml `sonar` - {organization?}: the key of the SonarCloud organization the release's Sonar proof of the example apps analyses in
11
+ * (the project key is <organization>_<key the example declares>). It is configuration, not a secret; the environment variable
12
+ * SONAR_ORGANIZATION wins over it, the repository variable of the same name is what CI reads. Null or absent means "not set yet".
13
+ */
14
+ export function validateSonar(sonar){
15
+ if(sonar===null)return;
16
+ const bad=invalid('sonar');
17
+ if(!plain(sonar))bad(' must be {organization?: <SonarCloud organization key>} or null.');
18
+ for(const key of Object.keys(sonar))if(!SONAR_KEYS.includes(key))bad(` has unknown key ${key} (allowed: ${SONAR_KEYS.join(', ')}).`);
19
+ const value=sonar.organization;
20
+ if(value!==undefined&&value!==null&&!(typeof value==='string'&&ORGANIZATION.test(value)))bad('.organization must be a SonarCloud organization key (lowercase letters, digits, - and _) or null.');
21
+ }
@@ -0,0 +1,32 @@
1
+ // temp-root.mjs — the one owner of where the runtime puts its temporary files.
2
+ //
3
+ // Every temp directory or file the runtime creates (spec fixtures, land scratch, gate staging, dispatch prompts, scan work
4
+ // dirs) lives under tempRoot(): env STARCI_TEMP_ROOT (a spec, a one-off run), then the owner config `roots.temp`
5
+ // (config.yaml, gitignored), else the OS temp directory of the process (TEMP / TMP / TMPDIR, then os.tmpdir()). The base tier
6
+ // only resolves; the directory is made by scripts/api/fs/make-temp-dir.mjs, temp-path.mjs and ensure-temp-root.mjs. The
7
+ // children the runtime starts get the same directory as TEMP / TMP / TMPDIR (tempChildEnv, applied by withTempEnv in every scripts/api/ spawn wrapper).
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { loadConfig } from './config.mjs';
11
+
12
+ export const TEMP_ROOT_ENV = 'STARCI_TEMP_ROOT';
13
+
14
+ /** The owner's `roots.temp` (config.yaml, validated by engine/invalid-config.mjs), or null. `config` is the owner config (default: loadConfig(), whose error for an invalid config.yaml propagates: the OS directory never replaces a root the owner set wrongly). */
15
+ const ownerTemp = (config) => (config === undefined ? loadConfig() : config)?.roots?.temp ?? null;
16
+
17
+ /** The operating-system temp directory of `env`: TEMP, TMP, TMPDIR, else os.tmpdir(). */
18
+ export const osTempDir = (env = process.env) => path.resolve(String(env?.TEMP || env?.TMP || env?.TMPDIR || os.tmpdir()));
19
+
20
+ /** The one temp root: env STARCI_TEMP_ROOT, then the owner config `roots.temp`, else the OS temp directory. Resolved, not created. */
21
+ export function tempRoot({ env = process.env, config = undefined } = {}) {
22
+ return path.resolve(String(env?.[TEMP_ROOT_ENV] || ownerTemp(config) || osTempDir(env)));
23
+ }
24
+
25
+ /** `env` (default: the process environment) as a copy whose TEMP, TMP and TMPDIR name the temp root. */
26
+ export function tempChildEnv(env = process.env) {
27
+ const root = tempRoot({ env });
28
+ return { ...env, TEMP: root, TMP: root, TMPDIR: root };
29
+ }
30
+
31
+ /** The spawn options with `env` replaced by tempChildEnv of the options' own env (default: the process environment). */
32
+ export const withTempEnv = (options = {}) => ({ ...options, env: tempChildEnv(options.env ?? process.env) });
@@ -27,28 +27,28 @@ pins:
27
27
  install: registry
28
28
  side: fe
29
29
  source: packages/grammar/package.json
30
- why: the product front ends pinned older versions (0.4.11, 0.5.0, 0.5.1); the runtime source is 0.8.0 and wins for a @starci package (the brand layer sets `--font-sans` and `--font-mono`; the grammar reads them; 0.7.2 added the Input tel kind and IconButton disclosure props).
30
+ why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind and the IconButton disclosure props. The runtime source version wins for a @starci package.'
31
31
  '@starci/eslint-canon-be':
32
- version: 3.0.10
32
+ version: 3.2.0
33
33
  group: starci
34
34
  install: registry
35
35
  side: be
36
36
  source: packages/eslint/be/package.json
37
- why: '3.0.3: its bundled canon-pins copy pins stylelint-canon 2.0.2 and hfs 4.0.3; no rule changed. 3.0.2: its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root (app.starcistacks, app.sops) and pin hfs 4.0.2. 3.0.1: its bundled canon-pins copy pins hfs 4.0.1. 3.0.0: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side; the project graph is built per side. 2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws.'
37
+ why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
38
38
  '@starci/eslint-canon-fe':
39
- version: 8.0.10
39
+ version: 8.2.0
40
40
  group: starci
41
41
  install: registry
42
42
  side: fe
43
43
  source: packages/eslint/fe/package.json
44
- why: '8.0.3: its bundled canon-pins copy pins stylelint-canon 2.0.2 and hfs 4.0.3; no rule changed. 8.0.2: its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root (app.starcistacks, app.sops) and pin hfs 4.0.2. 8.0.1: its bundled canon-pins copy pins hfs 4.0.1. 8.0.0: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side; the project graph is built per side. 7.0.0: the front end has no tests (FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
44
+ why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
45
45
  '@starci/stylelint-canon':
46
- version: 2.0.5
46
+ version: 2.0.6
47
47
  group: starci
48
48
  install: registry
49
49
  side: fe
50
50
  source: packages/stylelint/package.json
51
- why: '2.0.2: no-class-selector refuses a compound class selector (`div.card`), which the 2.0.1 pattern let through. 2.0.1 accepts `--font-sans` and `--font-mono` in the brand layer (the vocabulary follows the grammar 0.8.0). 2.0.0 adds status-contrast (HeroUI soft pairs measured per theme), fixes brand-layer-shape on the shared :root,.light,.dark block and the info soft pair, and derives appTokens with loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
51
+ why: 'the front-end stylelint canon: no-class-selector refuses a compound class selector (`div.card`); the brand layer accepts `--font-sans` and `--font-mono`; status-contrast measures the HeroUI soft pairs per theme; brand-layer-shape reads the shared :root,.light,.dark block and the info soft pair; appTokens come from loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
52
52
  '@starci/tsconfig':
53
53
  version: 2.0.3
54
54
  group: starci
@@ -62,33 +62,33 @@ pins:
62
62
  side: both
63
63
  source: packages/prettier-config/package.json
64
64
  '@starci/jest-preset':
65
- version: 2.2.5
65
+ version: 2.2.6
66
66
  group: starci
67
67
  install: registry
68
68
  side: be
69
69
  source: packages/jest-preset/package.json
70
- why: '2.2.4: the world runner runs up to min(--maxWorkers, slots) world files at once, each in its own process bound to one data slot of the test world (STARCI_TEST_WORLD_SLOT), and reads test-world state protocol 2 only: pinned together with @starci/test-world 1.1.0, any other state file is JEST_PRESET_WORLD_PAIR_MISMATCH. 2.2.3: republished after the move (no preset change). 2.2.2: the unit run writes the lcov Sonar and Codecov import (services only); 2.2.0: the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; 2.1.0: four projects on the test world, the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
70
+ why: 'the Jest preset of a back end: the world runner runs up to min(--maxWorkers, slots) world files at once, each in its own process bound to one data slot of the test world (STARCI_TEST_WORLD_SLOT), and reads test-world state protocol 2 only: it pairs exactly with @starci/test-world, and any other state file is JEST_PRESET_WORLD_PAIR_MISMATCH. The unit run writes the lcov Sonar and Codecov import (services only); the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; it ships the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
71
71
  '@starci/test-world':
72
72
  version: 1.2.1
73
73
  group: starci
74
74
  install: registry
75
75
  side: be
76
76
  source: packages/test-world/package.json
77
- why: '1.2.0: Kafka as real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and schema-per-context Postgres (a schema and a login role per context in a shared database); its CLI main is exported at ./cli for starci app stack, and the former starci app stack bin is removed. 1.1.0: per-worker data slots: a run provisions min(jest workers, workers cap, default 2) slots, each its own namespace (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock, so world files run in parallel; state file protocol 2, paired exactly with @starci/jest-preset 2.2.4 (TEST_WORLD_PAIR_MISMATCH otherwise). 1.0.6: republished after the move (no library change). 1.0.5: a modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, and w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client. 1.0.4: world.resolve and scope.resolve take any Nest token (class, string or symbol). 1.0.3: the globalSetup registers the path aliases as TypeScript resolves them (the extends chain, paths from the config that declares them, the effective baseUrl), so a tests tsconfig that only extends the side config loads the declaration. 1.0.2: every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json), where .starcistacks lives, never from the be side. The shared e2e library of every back end (R47, R48): the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock; a devDependency of every back end. 1.0.1: world.keycloak.events/sessions (the user events of the realm and live sessions through the admin API) and world.infra.postgresql.connection(name) (the database of one connection down while the others serve, under the outage lock).'
77
+ why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
78
78
  '@starci/cli':
79
- version: 1.0.1
79
+ version: 1.2.0
80
80
  group: starci
81
81
  install: registry
82
82
  side: both
83
83
  source: packages/cli/package.json
84
- why: '1.0.0: the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
84
+ why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
85
85
  '@starci/hfs':
86
- version: 4.0.10
86
+ version: 4.2.0
87
87
  group: starci
88
88
  install: registry
89
89
  side: both
90
90
  source: packages/hfs/package.json
91
- why: '4.0.9: the transitive app implementation used by @starci/cli; it carries the generated runtime slice read by starci app commands.'
91
+ why: 'the transitive app implementation used by @starci/cli; it carries the generated runtime slice read by starci app commands.'
92
92
  # --- tooling
93
93
  typescript:
94
94
  version: 5.9.3
@@ -2,9 +2,8 @@ schema: starci/hfs-peer-integrations@1
2
2
  purpose: >-
3
3
  The runtime peers a driver integration needs and npm does not install for it: "if the app depends on every package of
4
4
  `when` (at the named major, when one is named), it must declare `requires` in its dependencies". A missing peer is
5
- invisible to the type checker and the unit specs and only fails when the api boots (ecommerce-app,
6
- 2026-10-01: the old backend did not declare @as-integrations/express5, so the Apollo GraphQL api could not boot under
7
- Express 5 and nothing noticed before e2e). R111 HFS_PEER_INTEGRATION_MISSING judges the app root package.json, the one
5
+ invisible to the type checker and the unit specs and only fails when the api boots (a backend that does not declare
6
+ @as-integrations/express5 cannot boot its Apollo GraphQL api under Express 5, and nothing notices before e2e). R111 HFS_PEER_INTEGRATION_MISSING judges the app root package.json, the one
8
7
  manifest of both sides, with this catalog; an entry is added the day a boot failure of the same kind is found.
9
8
  pairs:
10
9
  - id: nestjs-apollo-express5