@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
@@ -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');
@@ -2,9 +2,19 @@
2
2
  // A missing or unreadable path is false/null, never a throw; the stat syscall decides kind.
3
3
  import fs from 'node:fs';
4
4
 
5
- /** `p` names a file; false on any stat error (missing path, dangling link, EPERM). */
6
- export const isFile = (p) => { try { return fs.statSync(p).isFile(); } catch { return false; } };
7
- /** `p` names a directory; false on any stat error. */
8
- export const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
5
+ /** `p` names a file within `maxBytes`; false on any stat error. `followLinks: false` probes the entry itself. */
6
+ export const isFile = (p, { followLinks = true, maxBytes = Infinity } = {}) => {
7
+ try {
8
+ const stat = followLinks ? fs.statSync(p) : fs.lstatSync(p);
9
+ return stat.isFile() && (followLinks || !stat.isSymbolicLink()) && stat.size <= maxBytes;
10
+ } catch { return false; }
11
+ };
12
+ /** `p` names a directory; false on any stat error. `followLinks: false` refuses linked entries. */
13
+ export const isDir = (p, { followLinks = true } = {}) => {
14
+ try {
15
+ const stat = followLinks ? fs.statSync(p) : fs.lstatSync(p);
16
+ return stat.isDirectory() && (followLinks || !stat.isSymbolicLink());
17
+ } catch { return false; }
18
+ };
9
19
  /** `p`'s canonical spelling via the filesystem (links resolved), or null when it cannot be read. */
10
20
  export const realpathOr = (p) => { try { return fs.realpathSync.native(p); } catch { return null; } };
@@ -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 };