@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
@@ -0,0 +1,262 @@
1
+ // graphql-sdl.mjs - the GraphQL tokenizer and the schema (SDL) reader of graphql-contract.mjs: tokens, a cursor over them, the
2
+ // type, value and directive readers a document shares, and `parseSchema` over the SDL `starci app emit` writes. No dependency:
3
+ // the runtime ships no GraphQL library.
4
+
5
+ const PUNCTUATORS = new Set(['{', '}', '(', ')', '[', ']', ':', '!', '=', '$', '@', '|', '&']);
6
+
7
+ /** The tokens of a GraphQL source: names, punctuators, `...`, strings and numbers; comments and commas are insignificant. */
8
+ function commentEnd(source, index) { while (index < source.length && source[index] !== '\n') { index += 1; } return index; }
9
+ function blockStringAt(source, index) { const end = source.indexOf('"""', index + 3); if (end === -1) { throw new SyntaxError('unterminated block string'); } return { token: { kind: 'string', value: source.slice(index + 3, end) }, next: end + 3 }; }
10
+ function quotedStringAt(source, index) { let end = index + 1; while (end < source.length && source[end] !== '"') { end += source[end] === '\\' ? 2 : 1; } return { token: { kind: 'string', value: source.slice(index + 1, end) }, next: end + 1 }; }
11
+ function nameAt(source, index) { const match = /^[_A-Za-z]\w*/.exec(source.slice(index)); return { token: { kind: 'name', value: match[0] }, next: index + match[0].length }; }
12
+ function numberAt(source, index) { return /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(source.slice(index)); }
13
+ export function tokenize(source) {
14
+ const tokens = [];
15
+ let index = 0;
16
+ while (index < source.length) {
17
+ const char = source[index];
18
+ if (char === '#') {
19
+ index = commentEnd(source, index);
20
+ } else if (/[\s,]/.test(char)) {
21
+ index += 1;
22
+ } else if (source.startsWith('...', index)) {
23
+ tokens.push({ kind: 'spread', value: '...' });
24
+ index += 3;
25
+ } else if (source.startsWith('"""', index)) {
26
+ const parsed = blockStringAt(source, index);
27
+ tokens.push(parsed.token);
28
+ index = parsed.next;
29
+ } else if (char === '"') {
30
+ const parsed = quotedStringAt(source, index);
31
+ tokens.push(parsed.token);
32
+ index = parsed.next;
33
+ } else if (PUNCTUATORS.has(char)) {
34
+ tokens.push({ kind: 'punct', value: char });
35
+ index += 1;
36
+ } else if (/[_A-Za-z]/.test(char)) {
37
+ const parsed = nameAt(source, index);
38
+ tokens.push(parsed.token);
39
+ index = parsed.next;
40
+ } else if (/[-0-9]/.test(char)) {
41
+ const match = numberAt(source, index);
42
+ if (!match) throw new SyntaxError(`unexpected character "${char}"`);
43
+ tokens.push({ kind: 'number', value: match[0] });
44
+ index += match[0].length;
45
+ } else {
46
+ throw new SyntaxError(`unexpected character "${char}"`);
47
+ }
48
+ }
49
+ return tokens;
50
+ }
51
+
52
+ /** A cursor over tokens with the expectations both parsers share. */
53
+ export function cursorOf(tokens) {
54
+ let index = 0;
55
+ const cursor = {
56
+ peek: (offset = 0) => tokens[index + offset] ?? null,
57
+ next: () => {
58
+ const token = tokens[index];
59
+ if (token === undefined) throw new SyntaxError('unexpected end of document');
60
+ index += 1;
61
+ return token;
62
+ },
63
+ is: (value) => tokens[index]?.value === value && tokens[index]?.kind !== 'string',
64
+ done: () => index >= tokens.length,
65
+ take: (value) => {
66
+ if (!cursor.is(value)) return false;
67
+ index += 1;
68
+ return true;
69
+ },
70
+ expect: (value) => {
71
+ const token = cursor.next();
72
+ if (token.value !== value || token.kind === 'string') throw new SyntaxError(`expected "${value}", found "${token.value}"`);
73
+ return token;
74
+ },
75
+ name: () => {
76
+ const token = cursor.next();
77
+ if (token.kind !== 'name') throw new SyntaxError(`expected a name, found "${token.value}"`);
78
+ return token.value;
79
+ },
80
+ };
81
+ return cursor;
82
+ }
83
+
84
+ /** A type reference: `{ name, list, nonNull, item }`. */
85
+ export function parseTypeRef(cursor) {
86
+ let type;
87
+ if (cursor.take('[')) {
88
+ const item = parseTypeRef(cursor);
89
+ cursor.expect(']');
90
+ type = { list: true, item, name: item.name, nonNull: false };
91
+ } else {
92
+ type = { list: false, name: cursor.name(), nonNull: false };
93
+ }
94
+ if (cursor.take('!')) type = { ...type, nonNull: true };
95
+ return type;
96
+ }
97
+
98
+ /** Skips one value of any shape (a default value, a directive argument). */
99
+ export function skipValue(cursor) {
100
+ const token = cursor.next();
101
+ if (token.kind !== 'punct') return;
102
+ if (token.value === '$') return void cursor.name();
103
+ let close = null;
104
+ if (token.value === '{') close = '}';
105
+ else if (token.value === '[') close = ']';
106
+ if (close === null) return;
107
+ while (!cursor.take(close)) {
108
+ if (token.value === '{') {
109
+ cursor.name();
110
+ cursor.expect(':');
111
+ }
112
+ skipValue(cursor);
113
+ }
114
+ }
115
+
116
+ export function skipDirectives(cursor) {
117
+ while (cursor.take('@')) {
118
+ cursor.name();
119
+ if (cursor.take('(')) {
120
+ while (!cursor.take(')')) {
121
+ cursor.name();
122
+ cursor.expect(':');
123
+ skipValue(cursor);
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ function skipDescription(cursor) {
130
+ if (cursor.peek()?.kind === 'string') cursor.next();
131
+ }
132
+
133
+ /** Field or input-value definitions between braces. */
134
+ function parseFieldDefinitions(cursor, withArguments) {
135
+ const fields = new Map();
136
+ cursor.expect('{');
137
+ while (!cursor.take('}')) {
138
+ skipDescription(cursor);
139
+ const name = cursor.name();
140
+ const args = new Map();
141
+ if (withArguments && cursor.take('(')) {
142
+ while (!cursor.take(')')) {
143
+ skipDescription(cursor);
144
+ const argName = cursor.name();
145
+ cursor.expect(':');
146
+ const type = parseTypeRef(cursor);
147
+ const hasDefault = cursor.take('=');
148
+ if (hasDefault) skipValue(cursor);
149
+ skipDirectives(cursor);
150
+ args.set(argName, { type, hasDefault });
151
+ }
152
+ }
153
+ cursor.expect(':');
154
+ const type = parseTypeRef(cursor);
155
+ const hasDefault = cursor.take('=');
156
+ if (hasDefault) skipValue(cursor);
157
+ skipDirectives(cursor);
158
+ fields.set(name, { type, args, hasDefault });
159
+ }
160
+ return fields;
161
+ }
162
+
163
+ // `schema { query: Q ... }` (or `extend schema`): the root operation type names.
164
+ function readSchemaDefinition(cursor, keyword, roots) {
165
+ if (keyword === 'extend') cursor.name();
166
+ skipDirectives(cursor);
167
+ cursor.expect('{');
168
+ while (!cursor.take('}')) {
169
+ const operation = cursor.name();
170
+ cursor.expect(':');
171
+ roots[operation] = cursor.name();
172
+ }
173
+ }
174
+
175
+ // `directive @name(args) repeatable on A | B`: read and dropped.
176
+ function skipDirectiveDefinition(cursor) {
177
+ cursor.expect('@');
178
+ cursor.name();
179
+ if (cursor.take('(')) parseArgumentList(cursor);
180
+ if (cursor.is('repeatable')) cursor.name();
181
+ cursor.name();
182
+ cursor.take('|');
183
+ cursor.name();
184
+ while (cursor.take('|')) cursor.name();
185
+ }
186
+
187
+ // The type readers by keyword: each returns [name, definition] for the types map.
188
+ function readScalar(cursor) {
189
+ const name = cursor.name();
190
+ skipDirectives(cursor);
191
+ return [name, { kind: 'scalar' }];
192
+ }
193
+
194
+ function readEnum(cursor) {
195
+ const name = cursor.name();
196
+ skipDirectives(cursor);
197
+ const values = new Set();
198
+ cursor.expect('{');
199
+ while (!cursor.take('}')) {
200
+ skipDescription(cursor);
201
+ values.add(cursor.name());
202
+ skipDirectives(cursor);
203
+ }
204
+ return [name, { kind: 'enum', values }];
205
+ }
206
+
207
+ function readUnion(cursor) {
208
+ const name = cursor.name();
209
+ skipDirectives(cursor);
210
+ cursor.expect('=');
211
+ cursor.take('|');
212
+ const members = [cursor.name()];
213
+ while (cursor.take('|')) members.push(cursor.name());
214
+ return [name, { kind: 'union', members }];
215
+ }
216
+
217
+ function readObjectLike(cursor, keyword) {
218
+ const name = cursor.name();
219
+ if (cursor.is('implements')) {
220
+ cursor.name();
221
+ cursor.take('&');
222
+ cursor.name();
223
+ while (cursor.take('&')) cursor.name();
224
+ }
225
+ skipDirectives(cursor);
226
+ let kind = 'object';
227
+ if (keyword === 'input') kind = 'input';
228
+ else if (keyword === 'interface') kind = 'interface';
229
+ return [name, { kind, fields: parseFieldDefinitions(cursor, kind !== 'input') }];
230
+ }
231
+
232
+ const TYPE_READERS = new Map([['scalar', readScalar], ['enum', readEnum], ['union', readUnion], ['type', readObjectLike], ['interface', readObjectLike], ['input', readObjectLike]]);
233
+
234
+ /** The types of an SDL source: Map(name -> { kind, fields? , values? , members? }), plus the root operation type names. */
235
+ export function parseSchema(source) {
236
+ const cursor = cursorOf(tokenize(source));
237
+ const types = new Map();
238
+ const roots = { query: 'Query', mutation: 'Mutation', subscription: 'Subscription' };
239
+ while (!cursor.done()) {
240
+ skipDescription(cursor);
241
+ const keyword = cursor.name();
242
+ const readType = TYPE_READERS.get(keyword);
243
+ if (keyword === 'schema' || (keyword === 'extend' && cursor.is('schema'))) readSchemaDefinition(cursor, keyword, roots);
244
+ else if (keyword === 'directive') skipDirectiveDefinition(cursor);
245
+ else if (readType) {
246
+ const [name, definition] = readType(cursor, keyword);
247
+ types.set(name, definition);
248
+ } else throw new SyntaxError(`unexpected "${keyword}" in a schema`);
249
+ }
250
+ return { types, roots };
251
+ }
252
+
253
+ function parseArgumentList(cursor) {
254
+ while (!cursor.take(')')) {
255
+ skipDescription(cursor);
256
+ cursor.name();
257
+ cursor.expect(':');
258
+ parseTypeRef(cursor);
259
+ if (cursor.take('=')) skipValue(cursor);
260
+ skipDirectives(cursor);
261
+ }
262
+ }
@@ -11,6 +11,7 @@
11
11
  // A placeholder is `{name}`; the English and the Vietnamese of one entry carry exactly the same placeholders
12
12
  // (scripts/checks/check-i18n-catalog.mjs). An English source with no entry is returned as is: a missing translation degrades
13
13
  // to English, never to a blank.
14
+ import { byCodeUnit } from './list.mjs';
14
15
  import { escapeRegExp } from './regex.mjs';
15
16
  import fs from 'node:fs';
16
17
  import path from 'node:path';
@@ -19,7 +20,7 @@ import { parseYaml } from '../../engine/yaml.mjs';
19
20
  import { DEFAULT_OWNER_LANGUAGE, loadConfig } from '../../engine/config.mjs';
20
21
 
21
22
  export const CATALOG_DIR = 'modules/i18n/messages';
22
- export const PLACEHOLDER = /\{([A-Za-z_][\w]*)\}/g;
23
+ export const PLACEHOLDER = /\{([A-Za-z_]\w*)\}/g;
23
24
 
24
25
  const cache = new Map();
25
26
 
@@ -54,7 +55,7 @@ export const resetCatalogCache = () => cache.clear();
54
55
  /** The names each `pattern` match captures (group 1) in `text`, in match order (`sort` sorts them). */
55
56
  export const captureNames = (text, pattern, { sort = false } = {}) => {
56
57
  const names = [...String(text).matchAll(pattern)].map((m) => m[1]);
57
- return sort ? names.sort() : names;
58
+ return sort ? names.sort(byCodeUnit) : names;
58
59
  };
59
60
 
60
61
  /** The names of the placeholders of a message. */
@@ -0,0 +1,71 @@
1
+ // in-order.mjs — asynchronous work done strictly one after another, named by what it does. A sequential `await` in a
2
+ // loop is often the point (ordered ledger writes, one Orca or git call at a time, a rate limit, stop at the first
3
+ // failure or match); these helpers state that intent at the call site so no loop body carries an `await`. Each one
4
+ // starts item n+1 only after item n has settled, lets the first rejection (or a synchronous throw of `fn`) stop the run
5
+ // with later items never started, pulls a lazy iterable one item at a time, and closes it on an early stop, the way
6
+ // `for (const x of xs) await fn(x)` does. All of them run on the one `walk` below: promise callbacks, not an async
7
+ // recursion, so the stack and the memory stay flat however long the input is.
8
+ // Provably independent work that needs no order, short-circuit or error contract may use `Promise.all` instead.
9
+
10
+ /** Close `iterator` the way a `for...of` leaves it: `return()` when it has one. */
11
+ const close = (iterator) => iterator.return?.();
12
+
13
+ /**
14
+ * Drive `visit(item, index)` over `items` one at a time. `decide(outcome, item)` runs after each settled visit: a
15
+ * `{ value }` box ends the walk with that value, anything else goes on. Resolves with the box value, or `undefined` when
16
+ * the items ran out; rejects with the first rejection or throw, after closing the iterator.
17
+ */
18
+ const walk = (items, visit, decide) => new Promise((resolve, reject) => {
19
+ const iterator = items[Symbol.iterator]();
20
+ let index = 0;
21
+ const fail = (error) => {
22
+ try { close(iterator); } catch { /* the failure being reported wins, as it does in a for...of */ }
23
+ reject(error);
24
+ };
25
+ const stop = (value) => {
26
+ try { close(iterator); } catch (error) { reject(error); return; }
27
+ resolve(value);
28
+ };
29
+ const advance = () => {
30
+ let step, pending;
31
+ try {
32
+ step = iterator.next();
33
+ if (step.done) { resolve(undefined); return; }
34
+ pending = visit(step.value, index++);
35
+ } catch (error) {
36
+ if (step === undefined || step.done) reject(error); else fail(error);
37
+ return;
38
+ }
39
+ Promise.resolve(pending).then((outcome) => {
40
+ let box;
41
+ try { box = decide(outcome, step.value); } catch (error) { fail(error); return; }
42
+ if (box) stop(box.value); else advance();
43
+ }, fail);
44
+ };
45
+ advance();
46
+ });
47
+
48
+ const never = () => undefined;
49
+
50
+ /** `for (const x of items) await fn(x, index)`: resolves undefined once every item has run, one at a time. */
51
+ export const eachInOrder = (items, fn) => walk(items, fn, never);
52
+
53
+ /** The sequential map: `fn(x, index)` for each item in turn, resolving the results in item order. */
54
+ export const mapInOrder = (items, fn) => {
55
+ const results = [];
56
+ return walk(items, fn, (outcome) => { results.push(outcome); }).then(() => results);
57
+ };
58
+
59
+ /** The first item whose `predicate(x, index)` resolves truthy (undefined when none); later items never run. */
60
+ export const findInOrder = (items, predicate) => walk(items, predicate, (outcome, item) => (outcome ? { value: item } : undefined));
61
+
62
+ /**
63
+ * `for (;;) await step(attempt)`: `step(attempt)` runs again, after the previous run settled, until it resolves
64
+ * something other than undefined; that value (null included) is the result. A poll or retry returns undefined to go
65
+ * on and its answer to stop. `attempt` counts from 0.
66
+ */
67
+ export const repeatInOrder = (step) => walk(forever(), step, (outcome) => (outcome === undefined ? undefined : { value: outcome }));
68
+
69
+ function* forever() {
70
+ for (let attempt = 0; ; attempt++) yield attempt;
71
+ }
@@ -50,13 +50,13 @@ export const DECLARED_VIETNAMESE_FIELDS = Object.freeze({
50
50
  'modules/ops/_labels.yaml': Object.freeze({ flowFields: Object.freeze(['vi']) }),
51
51
  'modules/goal/archetypes.yaml': Object.freeze({
52
52
  fields: Object.freeze([
53
- 'buildIntent', 'canonIntent', 'e2eIntent', 'uatIntent', 'proofNegation', 'integrationIntent', 'integrationNegation', 'brandIntent',
53
+ 'buildIntent', 'canonIntent', 'e2eIntent', 'uatIntent', 'proofNegation', 'proofStateCue', 'integrationIntent', 'brandIntent',
54
54
  'phrases', 'requires', 'excludes', 'backend', 'frontend', 'package',
55
55
  ]),
56
56
  }),
57
57
  'modules/goal/source-phrases.yaml': Object.freeze({
58
58
  fields: Object.freeze([
59
- 'prefer', 'avoid', 'negation', 'refused', 'accept', 'golden',
59
+ 'prefer', 'avoid', 'only', 'negation', 'refused', 'accept', 'golden',
60
60
  'missingPaths', 'grantTooNarrow', 'toolTimeout', 'testGap', 'checkerUnavailable',
61
61
  'supervisorVerbs', 'ownerWord', 'orWord', 'beyondAuthority', 'ownerOnly', 'workerDied', 'contractConflict', 'decision',
62
62
  'product', 'grammar', 'knowledge', 'sourceLabel', 'internalVocabulary', 'actor', 'success',
@@ -124,7 +124,7 @@ export function documentLanguageHits(rel, text) {
124
124
  if (declared.fields?.includes(keys[hit.line - 1])) return false;
125
125
  if (!declared.flowFields) return true;
126
126
  // an inline flow map: drop the declared fields' values and judge what is left of the line
127
- const rest = declared.flowFields.reduce((line, field) => line.replace(new RegExp(`\\b${field}\\s*:\\s*(?:"(?:[^"\\\\]|\\\\.)*"|'(?:[^'\\\\]|\\\\.)*'|[^,}]*)`, 'g'), ''), lines[hit.line - 1]);
127
+ const rest = declared.flowFields.reduce((line, field) => line.replace(new RegExp(String.raw`\b${field}\s*:\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^,}]*)`, 'g'), ''), lines[hit.line - 1]);
128
128
  return hasSecondLanguage(rest);
129
129
  });
130
130
  }
@@ -4,7 +4,11 @@
4
4
  export const list = (v) => (Array.isArray(v) ? v : []);
5
5
 
6
6
  /** `v` as an array: itself when already one, [] for null/undefined, else [v]. */
7
- export const asList = (v) => (Array.isArray(v) ? v : v == null ? [] : [v]);
7
+ export const asList = (v) => {
8
+ if (Array.isArray(v)) return v;
9
+ if (v == null) return [];
10
+ return [v];
11
+ };
8
12
 
9
13
  /** The object entries of `v` (a non-array reads as []). */
10
14
  export const objectList = (v) => (Array.isArray(v) ? v : []).filter((item) => item && typeof item === 'object');
@@ -18,3 +22,6 @@ export const splitList = (v, { sep = ',', dedupe = false } = {}) => {
18
22
  const items = String(v ?? '').split(sep).map((s) => s.trim()).filter(Boolean);
19
23
  return dedupe ? [...new Set(items)] : items;
20
24
  };
25
+
26
+ // The comparator of a bare `sort()` (UTF-16 code-unit order) is owned by the engine, which cannot import scripts/lib.
27
+ export { byCodeUnit } from '../../engine/by-code-unit.mjs';
@@ -0,0 +1,15 @@
1
+ // mutation-fence.mjs — ephemeral admission carried through awaited work, never a state store.
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ const scope = new AsyncLocalStorage();
4
+
5
+ /** Run one admitted call; the checker is synchronous and reads its existing durable owners again. */
6
+ export const withMutationFence = (check, authority, fn) => scope.run({ check, authority, checking: false }, fn);
7
+ /** Recheck after the actual transaction lock or immediately before a native effect. */
8
+ export function assertMutationFence(boundary) {
9
+ const current = scope.getStore();
10
+ if (!current || current.checking) return;
11
+ current.checking = true;
12
+ try { current.check(boundary); } finally { current.checking = false; }
13
+ }
14
+ /** Identity stamped by existing write owners, never accepted from a command argument. */
15
+ export const mutationAuthority = () => scope.getStore()?.authority ?? null;
@@ -6,6 +6,17 @@ const WIN = process.platform === 'win32';
6
6
 
7
7
  /** `p` with every backslash turned into a forward slash; null and undefined read as ''. */
8
8
  export const slash = (p) => String(p ?? '').replaceAll('\\', '/');
9
+ /** `text` without its trailing '/' run (a loop: a trailing-run pattern backtracks super-linearly). */
10
+ export const trimTrailingSlashes = (text) => {
11
+ for (let end = text.length; ; end -= 1) if (end === 0 || text[end - 1] !== '/') return text.slice(0, end);
12
+ };
13
+ // A trailing '*' run with the one '/' before it (the glob suffix of 'dir/**'); text without a trailing '*' is unchanged.
14
+ const stripTrailingStars = (text) => {
15
+ let end = text.length;
16
+ while (end > 0 && text[end - 1] === '*') end -= 1;
17
+ if (end === text.length) return text;
18
+ return text.slice(0, end > 0 && text[end - 1] === '/' ? end - 1 : end);
19
+ };
9
20
  /** A relative path in git's spelling: forward slashes, no leading './'. */
10
21
  export const posixPath = (p) => slash(p).replace(/^\.\//, '');
11
22
  /** Whether posix `file` is `prefix` itself or under it (`file === prefix || file` starts with `prefix/`). */
@@ -19,7 +30,7 @@ export const samePath = (a, b) => foldCase(a) === foldCase(b);
19
30
  * `fold: true` folds case on every platform (a comparison against a lowercase-stored root needs it).
20
31
  */
21
32
  export const pathKey = (p, { fold = WIN } = {}) => {
22
- const key = slash(path.resolve(p)).replace(/\/+$/, '');
33
+ const key = trimTrailingSlashes(slash(path.resolve(p)));
23
34
  return fold ? key.toLowerCase() : key;
24
35
  };
25
36
  /** `a` is `b` or under it, or the reverse: two path spellings whose trees touch. */
@@ -36,37 +47,59 @@ export const normPath = (p, { trim = false, collapse = false, dot = false, glob
36
47
  s = slash(s);
37
48
  if (collapse) s = s.replace(/\/{2,}/g, '/');
38
49
  if (dot) s = s.replace(/^\.\//, '');
39
- if (glob === 'star') s = s.replace(/\/?\*+$/, '');
50
+ if (glob === 'star') s = stripTrailingStars(s);
40
51
  else if (glob === 'double') s = s.replace(/\/\*\*$/, '');
41
- s = s.replace(/\/+$/, '');
52
+ s = trimTrailingSlashes(s);
42
53
  return fold ? s.toLowerCase() : s;
43
54
  };
44
55
  /** A work-relative path in one spelling: forward slashes, no leading ./, no trailing slash. */
45
- export const normWork = (p) => String(p ?? '').trim().replaceAll('\\', '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
56
+ export const normWork = (p) => trimTrailingSlashes(String(p ?? '').trim().replaceAll('\\', '/').replace(/^\.\/+/, ''));
46
57
 
47
58
  /** `p` resolved and dereferenced (realpath where the path exists) — the canonical spelling of a checkout dir. */
48
59
  export const realPath = (p) => {
49
60
  const resolved = path.resolve(p);
50
61
  try { return fs.realpathSync.native(resolved); } catch { return resolved; }
51
62
  };
63
+ /** `p` with its nearest existing ancestor dereferenced and the not-yet-created rest appended: a root's canonical spelling before it exists. */
64
+ export const canonicalPath = (p) => {
65
+ const rest = [];
66
+ for (let at = path.resolve(p); ; at = path.dirname(at)) {
67
+ try { return path.join(fs.realpathSync.native(at), ...rest); } catch { /* not created yet */ }
68
+ if (path.dirname(at) === at) return path.resolve(p);
69
+ rest.unshift(path.basename(at));
70
+ }
71
+ };
52
72
  /** `p` resolved and case-folded where the filesystem ignores case — the Map key of an absolute path. */
53
73
  export const resolvedKey = (p) => foldCase(path.resolve(p));
54
74
  /** Whether `a` and `b` resolve to the same path (always case-folded, so differently-cased spellings count). */
55
75
  export const sameResolvedPath = (a, b) => pathKey(a, { fold: true }) === pathKey(b, { fold: true });
56
76
  /**
57
- * Whether `file` sits strictly under `root`. `key` normalizes both sides first (e.g. the registry's
58
- * realpath-folded treeKey) when the callers' spellings need it.
77
+ * Whether `file` sits under `root`; `includeSelf` also admits the root itself. `key` normalizes
78
+ * both sides first (e.g. the registry's realpath-folded treeKey) when their spellings need it.
59
79
  */
60
- export const insidePath = (root, file, { key = (p) => p } = {}) => {
80
+ export const insidePath = (root, file, { key = (p) => p, includeSelf = false } = {}) => {
61
81
  const rel = path.relative(key(root), key(file));
62
- return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);
82
+ return (includeSelf || Boolean(rel)) && !rel.startsWith('..') && !path.isAbsolute(rel);
83
+ };
84
+ /**
85
+ * `candidate` (absolute, or relative to `base`) resolved and dereferenced, required to sit under `base` (or be it):
86
+ * the boundary check for a path an operator, an agent or another process hands in. `base` is canonicalised once and the
87
+ * candidate is judged below it, so `..` segments and links leading out are refused; returns the canonical path, or throws
88
+ * a PATH_OUTSIDE_BASE refusal (`label` names the argument in the message).
89
+ */
90
+ export const containedPath = (base, candidate, { label = 'path' } = {}) => {
91
+ const root = canonicalPath(base);
92
+ const resolved = canonicalPath(path.resolve(root, String(candidate ?? '')));
93
+ const prefix = root.endsWith(path.sep) ? root : root + path.sep;
94
+ if (!insidePath(root, resolved, { includeSelf: true }) || (resolved !== root && !resolved.startsWith(prefix))) throw Object.assign(new Error(`${label} ${String(candidate)} is outside ${root} [PATH_OUTSIDE_BASE]`), { code: 'PATH_OUTSIDE_BASE' });
95
+ return resolved;
63
96
  };
64
97
  /**
65
98
  * A path-ish value (a string or a {path} entry) in relative compare form: trimmed, forward slashes,
66
99
  * no trailing '/**' glob or slashes, no leading './'; `fold` lowercases for a case-insensitive key.
67
100
  */
68
101
  export const normRel = (p, { fold = false } = {}) => {
69
- const s = String(typeof p === 'string' ? p : p?.path ?? '').trim().replace(/\\/g, '/').replace(/\/\*\*$/, '').replace(/\/+$/, '').replace(/^\.\//, '');
102
+ const s = trimTrailingSlashes(String(typeof p === 'string' ? p : p?.path ?? '').trim().replaceAll('\\', '/').replace(/\/\*\*$/, '')).replace(/^\.\//, '');
70
103
  return fold ? s.toLowerCase() : s;
71
104
  };
72
105
  /** Whether `file` equals or sits under one of `prefixes` (posix spellings; `dot` counts a '.' prefix as covering all). */
@@ -0,0 +1,7 @@
1
+ // pid-alive.mjs - whether a process id names a live process: the one probe the host locks, the file claim and the machine store share.
2
+
3
+ /** True while `pid` names a live process (EPERM counts as alive). */
4
+ export const pidAlive = (pid) => {
5
+ if (!Number.isInteger(pid) || pid <= 0) { return false; }
6
+ try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
7
+ };
@@ -1,4 +1,4 @@
1
1
  // regex.mjs - building a RegExp from text.
2
2
 
3
3
  /** `text` with every regular-expression metacharacter escaped, so it matches itself literally. */
4
- export const escapeRegExp = (text) => String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4
+ export const escapeRegExp = (text) => String(text).replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
@@ -2,4 +2,4 @@
2
2
  // carry CRLF): the one comparison the database rules use for a committed file against its base or against a regenerated one.
3
3
 
4
4
  /** True when both texts exist and are equal once CRLF is folded to LF and trailing whitespace at the end is dropped. */
5
- export const sameText = (a, b) => a !== null && b !== null && a.replace(/\r\n/g, '\n').trimEnd() === b.replace(/\r\n/g, '\n').trimEnd();
5
+ export const sameText = (a, b) => a !== null && b !== null && a.replaceAll('\r\n', '\n').trimEnd() === b.replaceAll('\r\n', '\n').trimEnd();
@@ -15,7 +15,7 @@ export const FORBIDDEN_FILES = [
15
15
  export const SECRET_PATTERNS = [
16
16
  { name: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
17
17
  { name: 'aws-access-key', re: /\bAKIA[0-9A-Z]{16}\b/ },
18
- { name: 'github-token', re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b|\bgithub_pat_[A-Za-z0-9_]{50,}\b/ },
18
+ { name: 'github-token', re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b|\bgithub_pat_\w{50,}\b/ },
19
19
  { name: 'anthropic-key', re: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
20
20
  { name: 'openai-key', re: /\bsk-(?:proj-)?[A-Za-z0-9]{32,}\b/ },
21
21
  { name: 'slack-token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/ },
@@ -28,7 +28,11 @@ export const SECRET_PATTERNS = [
28
28
  // candidate; only this heuristic takes the exemption, never a provider-shaped token above.
29
29
  // A spec file's keyword-assigned values are test inputs (a spec's sign-in
30
30
  // form password and a mocked accessToken once refused a push): the heuristic skips spec files too.
31
- { name: 'assigned-secret', re: /\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|client[_-]?secret)\b\s*[:=]\s*['"](?!env\([A-Z_][A-Z0-9_]*\)['"])([^'"\s$<{]{12,})['"]/i, placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
31
+ { name: 'assigned-secret', re: new RegExp([
32
+ String.raw`\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|client[_-]?secret)`,
33
+ String.raw`\b\s*[:=]\s*['"](?!env\([A-Z_][A-Z0-9_]*\)['"])([^'"\s$<{]{12,})`,
34
+ `['"]`,
35
+ ].join(''), 'i'), placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
32
36
  skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$/ },
33
37
  ];
34
38
 
@@ -3,8 +3,8 @@
3
3
  /**
4
4
  * Test seam: STARCI_SLEEP_SCALE (a number >= 0, default 1) multiplies every wait. A spec that drives a real
5
5
  * dispatch/boot against a fake host sets it small so the card's settle/attestation windows (seconds of pure
6
- * waiting, counted logically by their callers, not by the wall clock) cost milliseconds and the run no longer
7
- * depends on machine load. Unset in production.
6
+ * waiting, counted logically by their callers, not by the wall clock) cost milliseconds and the run does not
7
+ * depend on machine load. Unset in production.
8
8
  */
9
9
  import { readEnv } from './env.mjs';
10
10
  const scaleOf = () => {