@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
@@ -37,8 +37,11 @@ export async function parsePlpgsqlBody(body, parameters = [], returnKind = 'void
37
37
  let tag = `$hfs_probe_${i}$`;
38
38
  while (body.includes(tag)) { i += 1; tag = `$hfs_probe_${i}$`; }
39
39
  const args = parameters.map((parameter) => parameter?.FunctionParameter?.name).filter(Boolean)
40
- .map((name) => `"${name.replace(/"/g, '""')}" text`).join(', ');
41
- const returns = returnKind === 'setof' ? 'setof text' : returnKind === 'trigger' ? 'trigger' : returnKind === 'scalar' ? 'text' : 'void';
40
+ .map((name) => `"${name.replaceAll('"', '""')}" text`).join(', ');
41
+ let returns = 'void';
42
+ if (returnKind === 'setof') returns = 'setof text';
43
+ else if (returnKind === 'trigger') returns = 'trigger';
44
+ else if (returnKind === 'scalar') returns = 'text';
42
45
  const result = await (await loadPgParser()).parsePlPgSQLSync(
43
46
  `create function hfs_probe(${args}) returns ${returns} language plpgsql as ${tag}${body}${tag};`,
44
47
  );
@@ -0,0 +1,8 @@
1
+ // Trailing-slash trimming for slot paths and generated roots, linear in the length of the text.
2
+
3
+ /** `text` without its trailing `/` characters. */
4
+ export function trimTrailingSlashes(text) {
5
+ let end = text.length;
6
+ while (end > 0 && text.codePointAt(end - 1) === 47) end -= 1;
7
+ return text.slice(0, end);
8
+ }
@@ -49,6 +49,24 @@ export function readTree(repoRoot, { isIgnored }) {
49
49
  const holdsNoFile = (node) => node.files === 0 && node.dirs.every(holdsNoFile);
50
50
  const countDirs = (node) => node.dirs.reduce((sum, child) => sum + 1 + countDirs(child), 0);
51
51
 
52
+ function collectGhosts(node, ghosts) {
53
+ for (const [i, a] of node.dirs.entries()) {
54
+ for (const b of node.dirs.slice(i + 1)) {
55
+ if (editDistance(a.name, b.name) > GHOST_DISTANCE) continue;
56
+ if (holdsNoFile(a)) ghosts.push({ path: a.rel, of: b.rel, distance: editDistance(a.name, b.name) });
57
+ if (holdsNoFile(b)) ghosts.push({ path: b.rel, of: a.rel, distance: editDistance(a.name, b.name) });
58
+ }
59
+ }
60
+ }
61
+
62
+ function collectTreeFacts(node, empty, ghosts) {
63
+ for (const child of node.dirs) {
64
+ if (holdsNoFile(child)) empty.push({ path: child.rel, below: countDirs(child) });
65
+ else collectTreeFacts(child, empty, ghosts);
66
+ }
67
+ collectGhosts(node, ghosts);
68
+ }
69
+
52
70
  /**
53
71
  * The tree's own findings: `empty` are the topmost directories with no file anywhere below them (each with the number of
54
72
  * directories it holds); `ghosts` are pairs of sibling directories within GHOST_DISTANCE edits where `ghost` has no file
@@ -57,20 +75,7 @@ const countDirs = (node) => node.dirs.reduce((sum, child) => sum + 1 + countDirs
57
75
  export function treeFacts(tree) {
58
76
  const empty = [];
59
77
  const ghosts = [];
60
- const visit = (node) => {
61
- for (const child of node.dirs) {
62
- if (holdsNoFile(child)) empty.push({ path: child.rel, below: countDirs(child) });
63
- else visit(child);
64
- }
65
- for (const [i, a] of node.dirs.entries()) {
66
- for (const b of node.dirs.slice(i + 1)) {
67
- if (editDistance(a.name, b.name) > GHOST_DISTANCE) continue;
68
- if (holdsNoFile(a)) ghosts.push({ path: a.rel, of: b.rel, distance: editDistance(a.name, b.name) });
69
- if (holdsNoFile(b)) ghosts.push({ path: b.rel, of: a.rel, distance: editDistance(a.name, b.name) });
70
- }
71
- }
72
- };
73
- visit(tree);
78
+ collectTreeFacts(tree, empty, ghosts);
74
79
  return { empty, ghosts };
75
80
  }
76
81
 
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
- import { createRequire } from 'node:module';
3
2
  import path from 'node:path';
3
+ import { byCodeUnit } from '../lib/list.mjs';
4
4
 
5
5
  // One TypeScript program per (compiler, root names, compiler options, project references) inside a program run, and
6
6
  // one value per (kind, compiler, input) for what callers derive from those programs (the architecture context).
@@ -27,7 +27,7 @@ function keyOf(value) {
27
27
  }
28
28
  if (typeof value !== 'object' || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) return null;
29
29
  const entries = [];
30
- for (const name of Object.keys(value).sort()) {
30
+ for (const name of Object.keys(value).sort(byCodeUnit)) {
31
31
  const item = keyOf(value[name]);
32
32
  if (item === null) return null;
33
33
  entries.push(`${JSON.stringify(name)}:${item}`);
@@ -44,7 +44,10 @@ export function sharedInProgramRun(kind, ts, input, build) {
44
44
  const key = values ? keyOf(input) : null;
45
45
  if (key === null) return build();
46
46
  let byKey = values.get(ts);
47
- if (!byKey) values.set(ts, byKey = new Map());
47
+ if (!byKey) {
48
+ byKey = new Map();
49
+ values.set(ts, byKey);
50
+ }
48
51
  const id = `${kind}\0${key}`;
49
52
  if (!byKey.has(id)) byKey.set(id, build());
50
53
  return byKey.get(id);
@@ -74,4 +77,3 @@ export function typeScriptProjectReferencePath(ts, reference) {
74
77
  export function resolveTypeScriptModule(ts, specifier, from, options, host = ts.sys) {
75
78
  return ts.resolveModuleName(specifier, from, options, host).resolvedModule?.resolvedFileName;
76
79
  }
77
-
@@ -6,7 +6,7 @@ import path from 'node:path';
6
6
  import { allowsFile } from './allows.mjs';
7
7
  import { openHfs } from './slots.mjs';
8
8
 
9
- const posix = (p) => String(p).replace(/\\/g, '/');
9
+ const posix = (p) => String(p).replaceAll('\\', '/');
10
10
  const isAbsoluteAny = (text) => path.isAbsolute(text) || /^[A-Za-z]:\//.test(text);
11
11
 
12
12
  /**
@@ -58,8 +58,8 @@ export function shellCommands(body) {
58
58
  } else if (char === '"' || char === "'") {
59
59
  quote = char;
60
60
  current += char;
61
- } else if (char === ';' || char === '|' || (char === '&' && body[i + 1] === '&')) {
62
- if (char !== ';' && body[i + 1] === char) i += 1;
61
+ } else if (isCommandSeparator(body, i, char)) {
62
+ i += commandSeparatorAdvance(body, i, char);
63
63
  flush();
64
64
  } else if (char === '\\' && body[i + 1] === '\n') {
65
65
  i += 1;
@@ -71,43 +71,17 @@ export function shellCommands(body) {
71
71
  return commands;
72
72
  }
73
73
 
74
+ const isCommandSeparator = (body, index, char) => char === ';' || char === '|' || (char === '&' && body[index + 1] === '&');
75
+ const commandSeparatorAdvance = (body, index, char) => char !== ';' && body[index + 1] === char ? 1 : 0;
76
+
74
77
  /**
75
78
  * Parse a Dockerfile: { instructions: [{ keyword, text, line, stage }], stages: [{ index, name, image, line, instructions }],
76
79
  * comments: [{ text, line }], preamble: [instruction before the first FROM (ARG)] }. `keyword` is upper-case; `text` is the
77
80
  * argument text of the logical line; `stage` is the index of the stage the instruction belongs to (-1 before the first FROM).
78
81
  */
79
82
  export function parseDockerfile(source) {
80
- const lines = String(source).replace(/\r\n/g, '\n').split('\n');
81
- const instructions = [];
82
- const comments = [];
83
- let escape = '\\';
84
- let directives = true;
85
- for (let i = 0; i < lines.length; i += 1) {
86
- const raw = lines[i];
87
- const trimmed = raw.trim();
88
- if (trimmed === '') { directives = false; continue; }
89
- if (trimmed.startsWith('#')) {
90
- const body = trimmed.slice(1).trim();
91
- const directive = directives ? /^(syntax|escape|check)\s*=\s*(\S+)$/i.exec(body) : null;
92
- if (directive) { if (directive[1].toLowerCase() === 'escape') escape = directive[2]; continue; }
93
- directives = false;
94
- comments.push({ text: body, line: i + 1 });
95
- continue;
96
- }
97
- directives = false;
98
- const start = i + 1;
99
- let logical = trimmed;
100
- while (logical.endsWith(escape) && i + 1 < lines.length) {
101
- logical = logical.slice(0, -1).trimEnd();
102
- i += 1;
103
- while (i < lines.length && lines[i].trim().startsWith('#')) i += 1;
104
- if (i >= lines.length) break;
105
- logical = `${logical} ${lines[i].trim()}`;
106
- }
107
- const split = logical.search(/\s/);
108
- const keyword = (split < 0 ? logical : logical.slice(0, split)).toUpperCase();
109
- instructions.push({ keyword, text: split < 0 ? '' : logical.slice(split).trim(), line: start, stage: -1 });
110
- }
83
+ const lines = String(source).replaceAll('\r\n', '\n').split('\n');
84
+ const { instructions, comments } = parseInstructions(lines);
111
85
  const stages = [];
112
86
  const preamble = [];
113
87
  for (const instruction of instructions) {
@@ -122,3 +96,52 @@ export function parseDockerfile(source) {
122
96
  }
123
97
  return { instructions, stages, comments, preamble };
124
98
  }
99
+
100
+ function parseInstructions(lines) {
101
+ const instructions = [];
102
+ const comments = [];
103
+ const state = { escape: '\\', directives: true };
104
+ let i = 0;
105
+ while (i < lines.length) {
106
+ const parsed = parseInstructionLine(lines, i, state);
107
+ if (parsed.comment) comments.push(parsed.comment);
108
+ if (parsed.instruction) instructions.push(parsed.instruction);
109
+ i = parsed.nextLine + 1;
110
+ }
111
+ return { instructions, comments };
112
+ }
113
+
114
+ function parseInstructionLine(lines, index, state) {
115
+ const trimmed = lines[index].trim();
116
+ if (trimmed === '') { state.directives = false; return { nextLine: index }; }
117
+ if (trimmed.startsWith('#')) return parseCommentLine(trimmed, index, state);
118
+ state.directives = false;
119
+ const { logical, nextLine } = logicalInstructionOf(lines, index, state.escape);
120
+ const split = logical.search(/\s/);
121
+ const keyword = (split < 0 ? logical : logical.slice(0, split)).toUpperCase();
122
+ return { nextLine, instruction: { keyword, text: split < 0 ? '' : logical.slice(split).trim(), line: index + 1, stage: -1 } };
123
+ }
124
+
125
+ function parseCommentLine(trimmed, index, state) {
126
+ const body = trimmed.slice(1).trim();
127
+ const directive = state.directives ? /^(syntax|escape|check)\s*=\s*(\S+)$/i.exec(body) : null;
128
+ if (directive) {
129
+ if (directive[1].toLowerCase() === 'escape') state.escape = directive[2];
130
+ return { nextLine: index };
131
+ }
132
+ state.directives = false;
133
+ return { nextLine: index, comment: { text: body, line: index + 1 } };
134
+ }
135
+
136
+ function logicalInstructionOf(lines, index, escape) {
137
+ let logical = lines[index].trim();
138
+ let nextLine = index;
139
+ while (logical.endsWith(escape) && nextLine + 1 < lines.length) {
140
+ logical = logical.slice(0, -1).trimEnd();
141
+ nextLine += 1;
142
+ while (nextLine < lines.length && lines[nextLine].trim().startsWith('#')) nextLine += 1;
143
+ if (nextLine >= lines.length) break;
144
+ logical = `${logical} ${lines[nextLine].trim()}`;
145
+ }
146
+ return { logical, nextLine };
147
+ }
@@ -8,3 +8,13 @@ export const readEnv = (name, env = process.env) => env[name];
8
8
 
9
9
  /** True when `env` (default: the process environment) belongs to a spec run (`node --test` marks its children with NODE_TEST_CONTEXT). */
10
10
  export const isSpecRun = (env = process.env) => Boolean(env.NODE_TEST_CONTEXT);
11
+
12
+ /**
13
+ * The environment for a child that is itself a test runner: `env` without the mark of an enclosing spec run. A `node --test` that inherits NODE_TEST_CONTEXT believes it is a
14
+ * subtest of that run: it reports to its parent and exits 0 whatever its tests did, so a verb that runs specs as a child (starci test affected, the land gate, the release cut, the
15
+ * suite) would pass red specs from inside a test process. The runner marks the spec files it starts itself, so isSpecRun stays true inside them.
16
+ */
17
+ export function withoutTestRunner(env = process.env) {
18
+ const { NODE_TEST_CONTEXT: _enclosing, ...rest } = env;
19
+ return rest;
20
+ }
@@ -8,7 +8,8 @@
8
8
  // }
9
9
  // `starci app emit` writes `be/contracts/<service>/events.json` from them (name, version, compensates, and the payload fields of the
10
10
  // interface `create` takes); `starci app check` compares the committed file with the same reader. Nothing is executed: both walk the TypeScript
11
- // syntax tree. The functions take the `ts` module the caller loaded (the repository's own, else the runtime's); this file imports nothing.
11
+ // syntax tree. The functions take the `ts` module the caller loaded (the repository's own, else the runtime's); this file imports only the code-unit comparator.
12
+ import { byCodeUnit } from './list.mjs';
12
13
 
13
14
  const EVENT_CONTRACT_SCHEMA = 'starci/event-contract@1';
14
15
  const FIELD_TYPE = /^(?:string|number|boolean|string\[\]|number\[\])\??$/;
@@ -37,6 +38,18 @@ function fieldType(ts, node) {
37
38
  return null;
38
39
  }
39
40
 
41
+ function appendPayloadMember(ts, member, typeName, problems, where, payload) {
42
+ if (!ts.isPropertySignature(member) || !ts.isIdentifier(member.name) || member.type === undefined) {
43
+ problems.push(`${where}: the payload ${typeName} holds a member that is not \`name: type\``);
44
+ return;
45
+ }
46
+ const type = fieldType(ts, member.type);
47
+ let written = null;
48
+ if (type !== null) written = `${type}${member.questionToken ? '?' : ''}`;
49
+ if (written === null || !FIELD_TYPE.test(written)) problems.push(`${where}: payload field ${member.name.text} must be string, number, boolean, string[] or number[]`);
50
+ else payload[member.name.text] = written;
51
+ }
52
+
40
53
  /** The payload fields of the interface or type literal named `typeName` in the file: `{ <field>: <type> }`, optional fields with a `?`. */
41
54
  function payloadOf(ts, sourceFile, typeName, problems, where) {
42
55
  let members = null;
@@ -49,16 +62,7 @@ function payloadOf(ts, sourceFile, typeName, problems, where) {
49
62
  return {};
50
63
  }
51
64
  const payload = {};
52
- for (const member of members) {
53
- if (!ts.isPropertySignature(member) || !ts.isIdentifier(member.name) || member.type === undefined) {
54
- problems.push(`${where}: the payload ${typeName} holds a member that is not \`name: type\``);
55
- continue;
56
- }
57
- const type = fieldType(ts, member.type);
58
- const written = type === null ? null : `${type}${member.questionToken ? '?' : ''}`;
59
- if (written === null || !FIELD_TYPE.test(written)) problems.push(`${where}: payload field ${member.name.text} must be string, number, boolean, string[] or number[]`);
60
- else payload[member.name.text] = written;
61
- }
65
+ for (const member of members) appendPayloadMember(ts, member, typeName, problems, where, payload);
62
66
  return payload;
63
67
  }
64
68
 
@@ -91,7 +95,7 @@ export function readEventClasses(ts, text, file = 'event.ts') {
91
95
  }
92
96
 
93
97
  /** The sorted object: keys in code-point order, recursively (what the snapshot prints). */
94
- const sorted = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, sorted(value[key])])) : value);
98
+ const sorted = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? Object.fromEntries(Object.keys(value).sort(byCodeUnit).map((key) => [key, sorted(value[key])])) : value);
95
99
 
96
100
  /** The text of the snapshot of a service from its event classes: 2-space JSON, sorted keys, one final newline. */
97
101
  export function snapshotText(service, events) {
@@ -100,4 +104,4 @@ export function snapshotText(service, events) {
100
104
  }
101
105
 
102
106
  /** The snapshot's text with line endings folded, for comparing a committed file to a fresh one. */
103
- export const folded = (text) => text.replace(/\r\n/g, '\n');
107
+ export const folded = (text) => text.replaceAll('\r\n', '\n');
@@ -7,13 +7,13 @@
7
7
  */
8
8
  export const unquoteDiffPath = (value) =>
9
9
  /^".*"$/.test(value)
10
- ? JSON.parse(value.replace(/\\([0-7]{3})/g, (_, octal) => `\\u00${Number.parseInt(octal, 8).toString(16).padStart(2, '0')}`))
10
+ ? JSON.parse(value.replace(/\\([0-7]{3})/g, (_, octal) => String.raw`\u00${Number.parseInt(octal, 8).toString(16).padStart(2, '0')}`))
11
11
  : value;
12
12
 
13
13
  // git's repository-local variables (git rev-parse --local-env-vars). A hook or alias run in a linked worktree exports
14
14
  // GIT_DIR=<main>/.git/worktrees/<wt>; a spec inheriting it pointed every fixture git at the LIVE .claude repo and re-inited
15
15
  // it core.bare=true (2026-09-29, a6f60352c). tests/setup/isolated-registry.mjs drops the same list.
16
- export const GIT_LOCAL_ENV_VARS = ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_IMPLICIT_WORK_TREE', 'GIT_PREFIX', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_COUNT', 'GIT_GRAFT_FILE', 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_SHALLOW_FILE'];
16
+ export const GIT_LOCAL_ENV_VARS = ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_IMPLICIT_WORK_TREE', 'GIT_PREFIX', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_COUNT', 'GIT_GRAFT_FILE', 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_SHALLOW_FILE', 'GIT_INTERNAL_SUPER_PREFIX'];
17
17
  /** `parent` (the caller's env) without git's repository-local variables (GIT_CONFIG_KEY_n/VALUE_n go with GIT_CONFIG_COUNT). Pure. */
18
18
  export function withoutGitLocalEnv(parent) {
19
19
  const env = { ...parent };