@starci/hfs 4.0.10 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +3 -2
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +187 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +221 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/removed-vocabulary.mjs +28 -0
  14. package/runtime/engine/resources-config.mjs +52 -0
  15. package/runtime/engine/runtime-root.mjs +17 -0
  16. package/runtime/engine/secrets.mjs +35 -31
  17. package/runtime/engine/sonar-config.mjs +21 -0
  18. package/runtime/engine/temp-root.mjs +32 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  20. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  21. package/runtime/knowledge/hfs/rules.yaml +98 -65
  22. package/runtime/knowledge/hfs/slots.yaml +11 -14
  23. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  24. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  25. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  26. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  27. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +92 -256
  31. package/runtime/modules/models/tiers.yaml +65 -0
  32. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  33. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  34. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  35. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  36. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  37. package/runtime/scripts/api/git/lib.mjs +34 -1
  38. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  39. package/runtime/scripts/api/process/run-program.mjs +2 -1
  40. package/runtime/scripts/api/sops/lib.mjs +198 -120
  41. package/runtime/scripts/hfs/allows.mjs +3 -3
  42. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  43. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  44. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  45. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  47. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  48. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  49. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  50. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  51. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  52. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  53. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  54. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  55. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  56. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  57. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  58. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  59. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  60. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  61. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  62. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  63. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  65. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  66. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  67. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  69. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  70. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  71. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  72. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  74. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  75. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  76. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  77. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  78. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  79. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  80. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  81. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  82. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  83. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  84. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  85. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  86. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  87. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  88. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  89. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  90. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  91. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  92. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  93. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  94. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  95. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  96. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  97. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  98. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  99. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  100. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  101. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  102. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  103. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  104. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  105. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  113. package/runtime/scripts/hfs/architecture.mjs +3 -4
  114. package/runtime/scripts/hfs/check.mjs +61 -70
  115. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  116. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  117. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  118. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  119. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  120. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  121. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  122. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  123. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  124. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  125. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  126. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  127. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  128. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  129. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  130. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  131. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  132. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  133. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  134. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  135. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  136. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  137. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  138. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  139. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  140. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  141. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  142. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  143. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  144. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  145. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  146. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  147. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  148. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  149. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  150. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  151. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  152. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  153. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  154. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  155. package/runtime/scripts/hfs/secret.mjs +59 -48
  156. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  157. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  158. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  159. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  160. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  161. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  162. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  163. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  165. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  166. package/runtime/scripts/hfs/slots.mjs +72 -693
  167. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  168. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  169. package/runtime/scripts/hfs/tree.mjs +19 -14
  170. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  171. package/runtime/scripts/hfs/view.mjs +1 -1
  172. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  173. package/runtime/scripts/lib/event-contract.mjs +17 -13
  174. package/runtime/scripts/lib/git.mjs +2 -2
  175. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  176. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  177. package/runtime/scripts/lib/i18n.mjs +3 -2
  178. package/runtime/scripts/lib/in-order.mjs +71 -0
  179. package/runtime/scripts/lib/language.mjs +3 -3
  180. package/runtime/scripts/lib/list.mjs +8 -1
  181. package/runtime/scripts/lib/path-key.mjs +38 -5
  182. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  183. package/runtime/scripts/lib/regex.mjs +1 -1
  184. package/runtime/scripts/lib/same-text.mjs +1 -1
  185. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  186. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  187. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  188. package/runtime/scripts/lib/stack-services.mjs +1 -2
  189. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  190. package/runtime/scripts/lib/walk.mjs +6 -1
  191. package/scaffold/add-cli-lite.mjs +2 -1
  192. package/scaffold/add-table.mjs +2 -2
  193. package/scaffold/add.mjs +3 -2
  194. package/scaffold/app.mjs +4 -2
  195. package/scaffold/edition-gate.mjs +25 -25
  196. package/scaffold/lite-exports.mjs +2 -1
  197. package/scaffold/service.mjs +5 -4
  198. package/sync/hygiene.mjs +15 -9
  199. package/sync/index.mjs +7 -3
  200. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  201. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  202. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  203. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  204. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  205. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  206. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  207. package/templates/app/starciwork.gitignore +1 -1
  208. package/templates/be/image/api/Dockerfile +1 -1
  209. package/templates/be/image/cli/Dockerfile +1 -1
  210. package/templates/be/image/worker/Dockerfile +1 -1
  211. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  212. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  213. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  214. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  215. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  216. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  217. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  218. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  219. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  220. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  221. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  222. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  223. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  224. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  225. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  226. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  227. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  228. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  229. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  230. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  231. package/templates/fe/image/next/Dockerfile +2 -1
  232. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  233. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  234. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  235. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  236. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  238. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  239. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  241. package/upgrade/index.mjs +9 -2
  242. package/runtime/engine/admission.mjs +0 -308
  243. package/runtime/engine/canonical-json.mjs +0 -10
  244. package/runtime/engine/db/blob.mjs +0 -315
  245. package/runtime/engine/db/ledger-paths.mjs +0 -83
  246. package/runtime/engine/db/ledger.mjs +0 -1118
  247. package/runtime/engine/db/machine-connection.mjs +0 -135
  248. package/runtime/engine/db/machine-schema.mjs +0 -84
  249. package/runtime/engine/db/machine.mjs +0 -1367
  250. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  251. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  252. package/runtime/engine/db/provider-reservations.mjs +0 -101
  253. package/runtime/engine/digest.mjs +0 -16
  254. package/runtime/engine/refuse.mjs +0 -11
  255. package/runtime/modules/ops/_labels.yaml +0 -53
  256. package/runtime/scripts/api/node/lib.mjs +0 -14
  257. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  258. package/runtime/scripts/api/process/lib.mjs +0 -111
  259. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  260. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  261. package/runtime/scripts/connectors/lib.mjs +0 -488
  262. package/runtime/scripts/lib/clip.mjs +0 -19
  263. package/runtime/scripts/lib/display-names.mjs +0 -259
  264. package/runtime/scripts/lib/example-refs.mjs +0 -158
  265. package/runtime/scripts/lib/json-schema.mjs +0 -52
  266. package/runtime/scripts/lib/process-identity.mjs +0 -6
  267. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  268. package/runtime/scripts/lib/redact.mjs +0 -161
  269. package/runtime/scripts/lib/sleep.mjs +0 -7
  270. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  271. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -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,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 };