@starci/hfs 4.0.9 → 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 (247) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/LICENSE +21 -0
  3. package/emit/operations.mjs +1 -1
  4. package/emit/type-schema.mjs +2 -1
  5. package/lint/run.mjs +82 -8
  6. package/package.json +3 -2
  7. package/report/order.mjs +2 -0
  8. package/runtime/config.example.yaml +187 -0
  9. package/runtime/engine/by-code-unit.mjs +12 -0
  10. package/runtime/engine/config.mjs +247 -273
  11. package/runtime/engine/invalid-config.mjs +5 -5
  12. package/runtime/engine/model-config.mjs +90 -0
  13. package/runtime/engine/orca-config.mjs +3 -1
  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 +134 -0
  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 +18 -18
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +102 -69
  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 -1
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  30. package/runtime/modules/models/registry.yaml +25 -397
  31. package/runtime/modules/models/runtimes.yaml +92 -262
  32. package/runtime/modules/models/tiers.yaml +65 -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/lib.mjs +6 -0
  37. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  38. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  39. package/runtime/scripts/api/git/lib.mjs +36 -1
  40. package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
  41. package/runtime/scripts/api/process/run-program.mjs +8 -0
  42. package/runtime/scripts/api/sops/decrypt.mjs +9 -16
  43. package/runtime/scripts/api/sops/lib.mjs +309 -11
  44. package/runtime/scripts/api/sops/seal.mjs +8 -4
  45. package/runtime/scripts/hfs/allows.mjs +3 -3
  46. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  47. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  48. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  49. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  50. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  51. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  52. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  53. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  54. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  55. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  56. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  57. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  58. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  59. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  60. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  61. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  62. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  63. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  64. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  65. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  66. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  67. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  69. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  70. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  71. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +3 -2
  72. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  73. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  74. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  75. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  76. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  77. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  78. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  79. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  80. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  81. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  82. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  83. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  84. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  85. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  86. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  87. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  88. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  89. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  90. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  91. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  92. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  93. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  94. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  95. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  96. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  97. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  98. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  99. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  100. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  101. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  102. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  103. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  104. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  105. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  106. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  107. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  108. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  109. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  110. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  111. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  112. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  113. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  114. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  115. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  116. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  117. package/runtime/scripts/hfs/architecture.mjs +3 -4
  118. package/runtime/scripts/hfs/check.mjs +61 -70
  119. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  120. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  121. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  122. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  123. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  124. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  125. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  126. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  127. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  128. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  129. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  130. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  131. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  132. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  133. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  134. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  135. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  136. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  137. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  138. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  139. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  140. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  141. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  142. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  143. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  144. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  145. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  146. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  147. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  148. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  149. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  150. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  151. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  152. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  153. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  154. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  155. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  156. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  157. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  158. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  159. package/runtime/scripts/hfs/secret.mjs +90 -45
  160. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  161. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  162. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  163. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  165. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  166. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  167. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  168. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  170. package/runtime/scripts/hfs/slots.mjs +72 -693
  171. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  172. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  173. package/runtime/scripts/hfs/tree.mjs +19 -14
  174. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  175. package/runtime/scripts/hfs/view.mjs +1 -1
  176. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  177. package/runtime/scripts/lib/event-contract.mjs +17 -13
  178. package/runtime/scripts/lib/fs-kind.mjs +14 -4
  179. package/runtime/scripts/lib/git.mjs +2 -2
  180. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  181. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  182. package/runtime/scripts/lib/i18n.mjs +3 -2
  183. package/runtime/scripts/lib/in-order.mjs +71 -0
  184. package/runtime/scripts/lib/language.mjs +3 -3
  185. package/runtime/scripts/lib/list.mjs +8 -1
  186. package/runtime/scripts/lib/mutation-fence.mjs +15 -0
  187. package/runtime/scripts/lib/path-key.mjs +42 -9
  188. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  189. package/runtime/scripts/lib/regex.mjs +1 -1
  190. package/runtime/scripts/lib/same-text.mjs +1 -1
  191. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  192. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  193. package/runtime/scripts/lib/sops-envelope.mjs +103 -2
  194. package/runtime/scripts/lib/stack-services.mjs +1 -2
  195. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  196. package/runtime/scripts/lib/walk.mjs +6 -1
  197. package/scaffold/add-cli-lite.mjs +2 -1
  198. package/scaffold/add-table.mjs +2 -2
  199. package/scaffold/add.mjs +3 -2
  200. package/scaffold/app.mjs +4 -2
  201. package/scaffold/edition-gate.mjs +25 -25
  202. package/scaffold/lite-exports.mjs +2 -1
  203. package/scaffold/service.mjs +5 -4
  204. package/sync/hygiene.mjs +15 -9
  205. package/sync/index.mjs +7 -3
  206. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  207. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  208. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  209. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  210. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  211. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  212. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  213. package/templates/app/starciwork.gitignore +1 -1
  214. package/templates/be/image/api/Dockerfile +1 -1
  215. package/templates/be/image/cli/Dockerfile +1 -1
  216. package/templates/be/image/worker/Dockerfile +1 -1
  217. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  218. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  219. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  220. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  221. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  222. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  223. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  224. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  225. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  226. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  228. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  229. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  230. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  231. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  232. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  233. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  234. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  235. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  236. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  237. package/templates/fe/image/next/Dockerfile +2 -1
  238. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  239. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  240. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  242. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  243. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  245. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  246. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  247. package/upgrade/index.mjs +9 -2
@@ -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 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')).
@@ -0,0 +1,134 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /** The host-local credential filename, resolved only against a verified runtime main root. */
5
+ export const SECRET_ENV_FILE='secret.env';
6
+ /** The configured credential variable grammar shared by validation and secret resolution. */
7
+ export const ENV_NAME=/^[A-Z_][A-Z0-9_]{0,63}$/;
8
+ export const CREDENTIAL_FILE_MAX_BYTES=1024*1024;
9
+ const resolvedValues = new Set();
10
+ const remember = value => {
11
+ if (typeof value === 'string' && value.length >= 6) resolvedValues.add(value);
12
+ return value;
13
+ };
14
+
15
+ /** Filter credentials resolved by this process; the values never leave their resolution owner. */
16
+ export function redactResolvedSecrets(text) {
17
+ let clean = text;
18
+ for (const value of resolvedValues) if (clean.includes(value)) clean = clean.split(value).join('[redacted:resolved-secret]');
19
+ return clean;
20
+ }
21
+
22
+ /** Check lexical availability only; blank and conventional template markers never prove provider validity. */
23
+ export function credentialPresent(value){
24
+ if(typeof value!=='string'||!value.trim())return false;
25
+ return !/^(?:CHANGE_ME|REPLACE_ME|PLACEHOLDER|TODO|<[^>]*>|YOUR_[A-Z0-9_]+)$/i.test(value.trim());
26
+ }
27
+
28
+ /** Read one configured credential file with a finite byte budget; no effects beyond the read. */
29
+ export function readSecretBytes(file){
30
+ const before=fs.lstatSync(file);
31
+ if(!before.isFile()||before.isSymbolicLink())throw new Error('credential file must be a regular file');
32
+ if(before.size>CREDENTIAL_FILE_MAX_BYTES)throw new Error('credential file exceeds the byte budget');
33
+ const fd=fs.openSync(file,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0));
34
+ let bytes;
35
+ try{
36
+ const stat=fs.fstatSync(fd);
37
+ if(!stat.isFile()||stat.dev!==before.dev||stat.ino!==before.ino)throw new Error('credential file changed during resolution');
38
+ bytes=Buffer.alloc(CREDENTIAL_FILE_MAX_BYTES+1);
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;}
41
+ if(count>CREDENTIAL_FILE_MAX_BYTES)throw new Error('credential file exceeds the byte budget');
42
+ return bytes.subarray(0,count);
43
+ }catch(error){bytes?.fill(0);throw error;}finally{fs.closeSync(fd);}
44
+ }
45
+
46
+ const readCredentialFile = file => { const bytes=readSecretBytes(file); try{return bytes.toString('utf8');}finally{bytes.fill(0);} };
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
+
51
+ /** Parse a dotenv file (KEY=VALUE lines, # comments, optional export/quotes). An absent file is {}. */
52
+ export function readDotenv(file){
53
+ let text='';try{text=readCredentialFile(file);}catch(error){if(error?.code==='ENOENT'){return {};}throw error;}
54
+ const out={};
55
+ for(const line of text.split(/\r?\n/)){
56
+ const m=DOTENV_LINE.exec(line);
57
+ if(!m)continue;
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);
60
+ out[m[1]]=value;
61
+ }
62
+ return out;
63
+ }
64
+
65
+ /**
66
+ * Resolve a configured variable or its supported NAME_FILE custody pointer. An explicitly supplied
67
+ * blank variable disables the value; it must not resurrect a file value. Never print the result.
68
+ */
69
+ export function connectorSecret(name,env=process.env){
70
+ if(typeof name!=='string'||!ENV_NAME.test(name))return null;
71
+ if(Object.hasOwn(env,name))return typeof env[name]==='string'?remember(env[name].trim()||null):null;
72
+ const pointer=typeof env[`${name}_FILE`]==='string'?env[`${name}_FILE`].trim():'';
73
+ if(!pointer)return null;
74
+ try{const value=readCredentialFile(pointer).trim();return remember(value||null);}catch{return null;}
75
+ }
76
+
77
+ /**
78
+ * Load the verified runtime main's local credentials below the actual environment without mutating
79
+ * either input. The caller owns repository identity; this base-tier reader never infers a root from cwd.
80
+ * Absence is allowed so the action-specific gate can name only the credentials that action requires.
81
+ */
82
+ export function secretEnv(verifiedRuntimeRoot,env=process.env){
83
+ if(typeof verifiedRuntimeRoot!=='string'||!path.isAbsolute(verifiedRuntimeRoot))
84
+ throw new TypeError('secretEnv requires an absolute verified runtime root');
85
+ if(env===null||typeof env!=='object'||Array.isArray(env))throw new TypeError('secretEnv requires an environment mapping');
86
+ const root=fs.lstatSync(verifiedRuntimeRoot);
87
+ if(!root.isDirectory()||root.isSymbolicLink())throw new Error('secretEnv runtime root must be a regular directory');
88
+ const file=path.join(verifiedRuntimeRoot,SECRET_ENV_FILE);
89
+ let stat;
90
+ try{stat=fs.lstatSync(file);}catch(error){if(error?.code==='ENOENT'){return {...env};}throw error;}
91
+ if(!stat.isFile()||stat.isSymbolicLink())throw new Error('secret.env must be a regular file');
92
+ const local = readDotenv(file);
93
+ const resolved = {...local,...env};
94
+ for (const name of Object.keys(local)) if (typeof resolved[name] === 'string') remember(resolved[name]);
95
+ return resolved;
96
+ }
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
+ };
117
+ /** Select a supplied age identity without inventing custody or running a key tool. Inline execution awaits its owned isolation capability. */
118
+ export function sopsIdentityEnv(env,{identity=null,required=false,platform=process.platform}={}){
119
+ if(!env||typeof env!=='object'||Array.isArray(env))return sopsRefuse('invalid-environment','an environment mapping is required');
120
+ const names=name=>Object.keys(env).filter(key=>platform==='win32'?key.toUpperCase()===name:key===name);
121
+ const inline=names('SOPS_AGE_KEY'),files=names('SOPS_AGE_KEY_FILE');
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]);
124
+ let file=identity;
125
+ if(files.length){
126
+ const value=env[files[0]];
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');
129
+ file=value;
130
+ }
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');
133
+ return {mode:'public-recipient',env:{...env},error:null};
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) });
@@ -22,73 +22,73 @@ pins:
22
22
  source: packages/heroicons/package.json
23
23
  why: 'StarCi custom Heroicons-compatible cuts; eslint-canon-fe (icon.mjs) admits @starci/heroicons/24/outline and /16/solid as icon sources, so an app that uses them gets this exact version.'
24
24
  '@starci/grammar':
25
- version: 0.8.2
25
+ version: 0.8.3
26
26
  group: starci
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.9
32
+ version: 3.1.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.9
39
+ version: 8.1.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.4
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
- version: 2.0.2
53
+ version: 2.0.3
54
54
  group: starci
55
55
  install: registry
56
56
  side: both
57
57
  source: packages/tsconfig/package.json
58
58
  '@starci/prettier-config':
59
- version: 1.0.2
59
+ version: 1.0.3
60
60
  group: starci
61
61
  install: registry
62
62
  side: both
63
63
  source: packages/prettier-config/package.json
64
64
  '@starci/jest-preset':
65
- version: 2.2.4
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
- version: 1.2.0
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.0
79
+ version: 1.1.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.9
86
+ version: 4.1.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