@cosmicdrift/kumiko-framework 0.269.2 → 0.273.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 (176) hide show
  1. package/README.md +5 -1
  2. package/package.json +3 -3
  3. package/src/__tests__/anonymous-access.integration.test.ts +1 -1
  4. package/src/__tests__/error-contract.integration.test.ts +1 -1
  5. package/src/__tests__/fixtures/migrate-cross-tenant/manual-only.expected.ts +44 -0
  6. package/src/__tests__/fixtures/migrate-cross-tenant/manual-only.input.ts +44 -0
  7. package/src/__tests__/fixtures/migrate-cross-tenant/rewrite.expected.ts +41 -0
  8. package/src/__tests__/fixtures/migrate-cross-tenant/rewrite.input.ts +41 -0
  9. package/src/__tests__/fixtures/migrate-open-to-all/manual-only.expected.ts +16 -0
  10. package/src/__tests__/fixtures/migrate-open-to-all/manual-only.input.ts +16 -0
  11. package/src/__tests__/fixtures/migrate-open-to-all/test-nested-shorthand.expected.ts +12 -0
  12. package/src/__tests__/fixtures/migrate-open-to-all/test-nested-shorthand.input.ts +12 -0
  13. package/src/__tests__/fixtures/migrate-open-to-all/test-simple.expected.ts +7 -0
  14. package/src/__tests__/fixtures/migrate-open-to-all/test-simple.input.ts +7 -0
  15. package/src/__tests__/full-stack.integration.test.ts +2 -2
  16. package/src/__tests__/migrate-cross-tenant-codemod.test.ts +106 -0
  17. package/src/__tests__/migrate-open-to-all-codemod.test.ts +84 -0
  18. package/src/__tests__/related-list-parent-filter.integration.test.ts +103 -0
  19. package/src/__tests__/transition-guard.integration.test.ts +7 -7
  20. package/src/api/__tests__/api.test.ts +2 -2
  21. package/src/api/__tests__/body-limit.test.ts +1 -1
  22. package/src/api/__tests__/nested-write-ownership.integration.test.ts +268 -0
  23. package/src/api/__tests__/pii-leak-guard.integration.test.ts +2 -2
  24. package/src/api/__tests__/request-locale.integration.test.ts +1 -1
  25. package/src/api/__tests__/server-error-logging.test.ts +1 -1
  26. package/src/api/server.ts +7 -20
  27. package/src/bun-db/__tests__/query-guards.test.ts +60 -3
  28. package/src/bun-db/query.ts +24 -21
  29. package/src/changes.json +99 -0
  30. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +77 -0
  31. package/src/db/__tests__/event-store-executor.integration.test.ts +1 -1
  32. package/src/db/__tests__/list-filter-field-access.integration.test.ts +1 -1
  33. package/src/db/__tests__/replay-migration-sql.test.ts +17 -0
  34. package/src/db/__tests__/tenant-db-convention-cross-tenant.test.ts +72 -0
  35. package/src/db/__tests__/tenant-db-declared-unsafe-raw.test.ts +104 -0
  36. package/src/db/__tests__/tenant-db-global.test.ts +92 -0
  37. package/src/db/__tests__/tenant-db-no-raw.test.ts +75 -0
  38. package/src/db/__tests__/tenant-db.integration.test.ts +3 -1
  39. package/src/db/__tests__/unchecked-system-db.test.ts +76 -0
  40. package/src/db/apply-entity-event.ts +1 -1
  41. package/src/db/assert-exists-in.ts +1 -1
  42. package/src/db/event-store-executor-context.ts +5 -2
  43. package/src/db/event-store-executor-read.ts +6 -4
  44. package/src/db/event-store-executor-write.ts +24 -18
  45. package/src/db/queries/shadow-swap.ts +36 -0
  46. package/src/db/replay-migration-sql.ts +5 -5
  47. package/src/db/tenant-db-runner.ts +23 -0
  48. package/src/db/tenant-db.ts +97 -24
  49. package/src/derivatives/__tests__/derivatives-context.integration.test.ts +2 -2
  50. package/src/derivatives/__tests__/derivatives-context.test.ts +0 -1
  51. package/src/engine/__tests__/boot-validator-dashboard.test.ts +5 -5
  52. package/src/engine/__tests__/boot-validator-nav-area.test.ts +116 -0
  53. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +1 -1
  54. package/src/engine/__tests__/boot-validator-projection-list.test.ts +108 -17
  55. package/src/engine/__tests__/boot-validator-query-output-schema.test.ts +161 -18
  56. package/src/engine/__tests__/boot-validator-query-refs.test.ts +3 -3
  57. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +2 -2
  58. package/src/engine/__tests__/boot-validator.test.ts +69 -18
  59. package/src/engine/__tests__/build-app-schema.test.ts +10 -6
  60. package/src/engine/__tests__/build-config-feature-schema-secrets.test.ts +48 -0
  61. package/src/engine/__tests__/build-config-feature-schema.test.ts +34 -5
  62. package/src/engine/__tests__/engine.test.ts +39 -25
  63. package/src/engine/__tests__/entity-create-caller-id.integration.test.ts +87 -0
  64. package/src/engine/__tests__/entity-handlers-escape-hatch.test.ts +91 -0
  65. package/src/engine/__tests__/entity-handlers.test.ts +47 -8
  66. package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +2 -2
  67. package/src/engine/__tests__/entity-restore-tenant-isolation.integration.test.ts +30 -5
  68. package/src/engine/__tests__/entity-write-crosstenant.integration.test.ts +206 -47
  69. package/src/engine/__tests__/extends-registrar.test.ts +3 -3
  70. package/src/engine/__tests__/feature-crud-shorthand.test.ts +9 -3
  71. package/src/engine/__tests__/feature-crud-verb-access.integration.test.ts +1 -1
  72. package/src/engine/__tests__/hook-phases.test.ts +5 -5
  73. package/src/engine/__tests__/lifecycle-hooks.test.ts +9 -3
  74. package/src/engine/__tests__/multiselect-filter.integration.test.ts +1 -1
  75. package/src/engine/__tests__/post-query-hook.test.ts +6 -2
  76. package/src/engine/__tests__/projection-detail-actions.test.ts +2 -2
  77. package/src/engine/__tests__/projection-detail-singleton.test.ts +3 -3
  78. package/src/engine/__tests__/projection-detail-tabs.test.ts +3 -3
  79. package/src/engine/__tests__/projection-list-filter.integration.test.ts +1 -1
  80. package/src/engine/__tests__/read-steps-tenant-filter.integration.test.ts +215 -0
  81. package/src/engine/__tests__/registry.test.ts +19 -13
  82. package/src/engine/__tests__/required-surface-keys.test.ts +16 -0
  83. package/src/engine/__tests__/screen.test.ts +176 -20
  84. package/src/engine/__tests__/soft-delete-cleanup.test.ts +0 -1
  85. package/src/engine/__tests__/steps-read.test.ts +204 -8
  86. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +7 -4
  87. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +6 -3
  88. package/src/engine/boot-validator/__tests__/access-declarations.test.ts +5 -4
  89. package/src/engine/boot-validator/__tests__/global-tenancy.test.ts +16 -1
  90. package/src/engine/boot-validator/__tests__/parent-ref.test.ts +18 -6
  91. package/src/engine/boot-validator/__tests__/rate-limit-disabled-reason.test.ts +45 -0
  92. package/src/engine/boot-validator/access-declarations.ts +3 -5
  93. package/src/engine/boot-validator/entity-handler.ts +21 -0
  94. package/src/engine/boot-validator/global-tenancy.ts +13 -1
  95. package/src/engine/boot-validator/nav.ts +4 -3
  96. package/src/engine/boot-validator/parent-ref.ts +1 -1
  97. package/src/engine/boot-validator/projection-list-screens.ts +67 -2
  98. package/src/engine/boot-validator/query-output-columns.ts +33 -0
  99. package/src/engine/boot-validator/screens.ts +154 -31
  100. package/src/engine/build-config-feature-schema.ts +12 -2
  101. package/src/engine/entity-handlers.ts +72 -20
  102. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +3 -1
  103. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +2 -2
  104. package/src/engine/feature-ast/__tests__/parse.test.ts +106 -4
  105. package/src/engine/feature-ast/__tests__/patcher.test.ts +3 -3
  106. package/src/engine/feature-ast/__tests__/read-optional-access-rule.test.ts +4 -4
  107. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +5 -1
  108. package/src/engine/feature-ast/extractors/handlers.ts +16 -2
  109. package/src/engine/feature-ast/extractors/hooks.ts +25 -4
  110. package/src/engine/feature-ast/patcher.ts +15 -3
  111. package/src/engine/feature-ast/patterns.ts +4 -0
  112. package/src/engine/feature-ast/render.ts +3 -0
  113. package/src/engine/feature-entity-handlers.ts +4 -4
  114. package/src/engine/index.ts +5 -0
  115. package/src/engine/pattern-library/shared-fields.ts +7 -3
  116. package/src/engine/registry-validate.ts +12 -4
  117. package/src/engine/schema-builder.ts +6 -0
  118. package/src/engine/screen-helpers.ts +52 -0
  119. package/src/engine/steps/_read-source.ts +12 -0
  120. package/src/engine/steps/read-find-many.ts +8 -2
  121. package/src/engine/steps/read-find-one.ts +11 -8
  122. package/src/engine/steps/unsafe-projection-delete.ts +2 -1
  123. package/src/engine/steps/unsafe-projection-upsert.ts +2 -1
  124. package/src/engine/types/index.ts +8 -1
  125. package/src/entrypoint/__tests__/entrypoint-attach-dispatcher.integration.test.ts +1 -1
  126. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +3 -3
  127. package/src/entrypoint/__tests__/split-deploy.integration.test.ts +1 -1
  128. package/src/errors/i18n/de.yaml +14 -0
  129. package/src/errors/i18n/en.yaml +13 -0
  130. package/src/errors/reasons.ts +4 -0
  131. package/src/es-ops/__tests__/runner.integration.test.ts +2 -2
  132. package/src/i18n/required-surface-keys.ts +6 -2
  133. package/src/jobs/__tests__/job-event-trigger.integration.test.ts +2 -2
  134. package/src/jobs/__tests__/job-multi-trigger.integration.test.ts +2 -2
  135. package/src/jobs/__tests__/job-systemdb.integration.test.ts +5 -4
  136. package/src/jobs/__tests__/jobs.integration.test.ts +42 -1
  137. package/src/jobs/job-runner.ts +35 -5
  138. package/src/observability/__tests__/observability.integration.test.ts +7 -7
  139. package/src/pipeline/__tests__/archive-stream.integration.test.ts +2 -2
  140. package/src/pipeline/__tests__/dispatcher.test.ts +15 -15
  141. package/src/pipeline/__tests__/escape-hatch-audit.integration.test.ts +254 -0
  142. package/src/pipeline/__tests__/escape-hatch-report.test.ts +149 -0
  143. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +10 -10
  144. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +1 -1
  145. package/src/pipeline/__tests__/msp-rebuild.integration.test.ts +28 -0
  146. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  147. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +94 -0
  148. package/src/pipeline/__tests__/projections-runner-systemscope.integration.test.ts +101 -0
  149. package/src/pipeline/__tests__/query-projection.integration.test.ts +172 -17
  150. package/src/pipeline/__tests__/rate-limit-default.integration.test.ts +118 -0
  151. package/src/pipeline/__tests__/rate-limit-default.test.ts +117 -0
  152. package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +5 -2
  153. package/src/pipeline/__tests__/system-identity-switch.test.ts +240 -1
  154. package/src/pipeline/dispatch-query.ts +27 -8
  155. package/src/pipeline/dispatch-shared.ts +141 -57
  156. package/src/pipeline/dispatch-stream.ts +1 -3
  157. package/src/pipeline/dispatch-write.ts +62 -14
  158. package/src/pipeline/dispatcher.ts +20 -0
  159. package/src/pipeline/escape-hatch-report.ts +127 -0
  160. package/src/pipeline/index.ts +8 -0
  161. package/src/pipeline/member-read-only-transaction.ts +75 -0
  162. package/src/pipeline/msp-rebuild.ts +2 -0
  163. package/src/pipeline/projection-rebuild.ts +2 -0
  164. package/src/pipeline/projections-runner.ts +15 -13
  165. package/src/pipeline/system-identity-switch.ts +121 -9
  166. package/src/scripts/codemod/migrate-cross-tenant.ts +335 -0
  167. package/src/scripts/codemod/migrate-db-raw.ts +19 -6
  168. package/src/scripts/codemod/migrate-open-to-all.ts +249 -0
  169. package/src/secrets/__tests__/contains-secret.test.ts +5 -5
  170. package/src/stack/__tests__/request-helper.integration.test.ts +3 -3
  171. package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +1 -1
  172. package/src/testing/__tests__/e2e-generator.test.ts +19 -5
  173. package/src/ui-types/index.ts +3 -0
  174. package/src/utils/__tests__/ids.test.ts +29 -1
  175. package/src/utils/ids.ts +10 -1
  176. package/src/utils/index.ts +1 -1
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env bun
2
+ // migrate-open-to-all — rewrites deprecated `openToAll: true` to the required
3
+ // `{ reason }` form in test files, and reports every remaining non-test site
4
+ // for a human to pick a reason (fw#2858).
5
+ //
6
+ // Usage:
7
+ // bun scripts/codemod/migrate-open-to-all.ts [--dry-run] [--test-reason "<text>"] <path...>
8
+
9
+ import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
10
+ import { join, resolve } from "node:path";
11
+ import {
12
+ Node,
13
+ type ObjectLiteralExpression,
14
+ Project,
15
+ type PropertyAssignment,
16
+ SyntaxKind,
17
+ } from "ts-morph";
18
+
19
+ const DEFAULT_TEST_REASON = "test handler callable by any signed-in test user";
20
+
21
+ export interface Rewrite {
22
+ readonly file: string;
23
+ readonly line: number;
24
+ readonly before: string;
25
+ readonly after: string;
26
+ }
27
+
28
+ export interface ManualSite {
29
+ readonly file: string;
30
+ readonly line: number;
31
+ readonly handlerName?: string;
32
+ readonly description?: string;
33
+ }
34
+
35
+ export interface MigrateOpenToAllResult {
36
+ readonly output: string;
37
+ readonly rewrites: Rewrite[];
38
+ readonly manual: ManualSite[];
39
+ }
40
+
41
+ export interface MigrateOpenToAllOptions {
42
+ readonly testReason: string;
43
+ }
44
+
45
+ function isTestFile(fileName: string): boolean {
46
+ return /(^|\/)__tests__\//.test(fileName) || /\.test\.(ts|tsx)$/.test(fileName);
47
+ }
48
+
49
+ function isOpenToAllTrueAssignment(node: Node): node is PropertyAssignment {
50
+ if (!Node.isPropertyAssignment(node)) return false;
51
+ if (node.getName() !== "openToAll") return false;
52
+ return node.getInitializer()?.getKind() === SyntaxKind.TrueKeyword;
53
+ }
54
+
55
+ function readStringLiteralProperty(obj: ObjectLiteralExpression, name: string): string | undefined {
56
+ const prop = obj.getProperty(name);
57
+ if (!prop || !Node.isPropertyAssignment(prop)) return undefined;
58
+ const init = prop.getInitializer();
59
+ return init && Node.isStringLiteral(init) ? init.getLiteralValue() : undefined;
60
+ }
61
+
62
+ function enclosingObjectLiterals(node: Node): ObjectLiteralExpression[] {
63
+ const result: ObjectLiteralExpression[] = [];
64
+ let current: Node | undefined = node.getParent();
65
+ while (current) {
66
+ if (Node.isObjectLiteralExpression(current)) result.push(current);
67
+ current = current.getParent();
68
+ }
69
+ return result;
70
+ }
71
+
72
+ // Best-effort human-triage hint (not the rewrite's source of truth): derive a handler name/description from the enclosing object literal, or the enclosing call's first string-literal argument for the positional form.
73
+ function deriveHandlerContext(assignment: PropertyAssignment): {
74
+ readonly handlerName?: string;
75
+ readonly description?: string;
76
+ } {
77
+ let handlerName: string | undefined;
78
+ let description: string | undefined;
79
+ for (const obj of enclosingObjectLiterals(assignment)) {
80
+ if (handlerName === undefined) handlerName = readStringLiteralProperty(obj, "name");
81
+ if (description === undefined) description = readStringLiteralProperty(obj, "description");
82
+ if (handlerName !== undefined && description !== undefined) break;
83
+ }
84
+ if (handlerName === undefined) {
85
+ const call = assignment.getFirstAncestorByKind(SyntaxKind.CallExpression);
86
+ const firstArg = call?.getArguments()[0];
87
+ if (firstArg && Node.isStringLiteral(firstArg)) handlerName = firstArg.getLiteralValue();
88
+ }
89
+ return {
90
+ ...(handlerName !== undefined && { handlerName }),
91
+ ...(description !== undefined && { description }),
92
+ };
93
+ }
94
+
95
+ export function migrateOpenToAllSource(
96
+ source: string,
97
+ fileName: string,
98
+ opts: MigrateOpenToAllOptions,
99
+ ): MigrateOpenToAllResult {
100
+ const project = new Project({ useInMemoryFileSystem: true, skipFileDependencyResolution: true });
101
+ const sourceFile = project.createSourceFile(fileName, source);
102
+ const testFile = isTestFile(fileName);
103
+
104
+ const targets = sourceFile
105
+ .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
106
+ .filter(isOpenToAllTrueAssignment);
107
+
108
+ const rewrites: Rewrite[] = [];
109
+ const manual: ManualSite[] = [];
110
+
111
+ for (const assignment of targets) {
112
+ if (assignment.wasForgotten()) continue;
113
+ const line = assignment.getStartLineNumber();
114
+
115
+ if (testFile) {
116
+ const before = assignment.getText();
117
+ const after = `openToAll: { reason: ${JSON.stringify(opts.testReason)} }`;
118
+ assignment.replaceWithText(after);
119
+ rewrites.push({ file: fileName, line, before, after });
120
+ continue;
121
+ }
122
+
123
+ manual.push({ file: fileName, line, ...deriveHandlerContext(assignment) });
124
+ }
125
+
126
+ return { output: sourceFile.getFullText(), rewrites, manual };
127
+ }
128
+
129
+ class CliError extends Error {}
130
+
131
+ interface CliOptions {
132
+ readonly dryRun: boolean;
133
+ readonly testReason: string;
134
+ readonly paths: string[];
135
+ }
136
+
137
+ function parseArgs(argv: string[]): CliOptions {
138
+ let dryRun = false;
139
+ let testReason = DEFAULT_TEST_REASON;
140
+ const paths: string[] = [];
141
+
142
+ for (let i = 0; i < argv.length; i++) {
143
+ const arg = argv[i];
144
+ if (!arg) break;
145
+ if (arg === "--dry-run") {
146
+ dryRun = true;
147
+ continue;
148
+ }
149
+ if (arg === "--test-reason") {
150
+ const value = argv[++i];
151
+ if (!value) throw new CliError("--test-reason requires a value");
152
+ testReason = value;
153
+ continue;
154
+ }
155
+ if (arg.startsWith("--")) throw new CliError(`unknown flag: ${arg}`);
156
+ paths.push(arg);
157
+ }
158
+
159
+ if (paths.length === 0) throw new CliError("at least one path is required");
160
+ return { dryRun, testReason, paths };
161
+ }
162
+
163
+ function isMigratableFile(name: string): boolean {
164
+ return /\.(ts|tsx)$/.test(name);
165
+ }
166
+
167
+ function walkDir(dir: string, out: string[]): void {
168
+ for (const name of readdirSync(dir)) {
169
+ if (name === "node_modules") continue;
170
+ const full = join(dir, name);
171
+ const stat = statSync(full);
172
+ if (stat.isDirectory()) {
173
+ walkDir(full, out);
174
+ continue;
175
+ }
176
+ if (isMigratableFile(name)) out.push(full);
177
+ }
178
+ }
179
+
180
+ function collectFiles(paths: string[]): string[] {
181
+ const files: string[] = [];
182
+ for (const p of paths) {
183
+ const abs = resolve(p);
184
+ const stat = statSync(abs);
185
+ if (stat.isDirectory()) walkDir(abs, files);
186
+ else if (isMigratableFile(abs)) files.push(abs);
187
+ }
188
+ return files;
189
+ }
190
+
191
+ async function main(): Promise<void> {
192
+ let options: CliOptions;
193
+ try {
194
+ options = parseArgs(process.argv.slice(2));
195
+ } catch (err) {
196
+ if (err instanceof CliError) {
197
+ console.error(`[migrate-open-to-all] ${err.message}`);
198
+ process.exit(2);
199
+ }
200
+ throw err;
201
+ }
202
+
203
+ const files = collectFiles(options.paths);
204
+ const allManual: ManualSite[] = [];
205
+ let filesChanged = 0;
206
+ let totalRewrites = 0;
207
+
208
+ for (const file of files) {
209
+ const before = readFileSync(file, "utf8");
210
+ if (!before.includes("openToAll")) continue;
211
+ const result = migrateOpenToAllSource(before, file, { testReason: options.testReason });
212
+ allManual.push(...result.manual);
213
+
214
+ if (result.rewrites.length === 0) continue;
215
+ filesChanged += 1;
216
+ totalRewrites += result.rewrites.length;
217
+
218
+ if (options.dryRun) {
219
+ console.log(`\n=== ${file} (${result.rewrites.length} rewrite(s)) ===`);
220
+ for (const r of result.rewrites) {
221
+ console.log(`${file}:${r.line}`);
222
+ console.log(`- ${r.before}`);
223
+ console.log(`+ ${r.after}`);
224
+ }
225
+ } else {
226
+ writeFileSync(file, result.output);
227
+ }
228
+ }
229
+
230
+ console.log(
231
+ `\n[migrate-open-to-all] ${options.dryRun ? "would change" : "changed"} ${filesChanged} file(s); ` +
232
+ `rewrites: ${totalRewrites}`,
233
+ );
234
+
235
+ if (allManual.length > 0) {
236
+ console.log(`\n[migrate-open-to-all] manual review needed (${allManual.length}):`);
237
+ for (const m of allManual) {
238
+ console.log(
239
+ `${m.file}:${m.line} ${m.handlerName ?? "<unknown>"} ${m.description ?? "<no description>"}`,
240
+ );
241
+ }
242
+ }
243
+
244
+ process.exit(0);
245
+ }
246
+
247
+ if (import.meta.main) {
248
+ await main();
249
+ }
@@ -54,7 +54,7 @@ declare const aSecret: Secret<string>;
54
54
  defineQueryHandler({
55
55
  name: "t:query:leak",
56
56
  schema,
57
- access: { openToAll: true },
57
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
58
58
  handler: async () => ({ apiKey: aSecret }),
59
59
  });
60
60
 
@@ -65,7 +65,7 @@ function genericResponseHandler<K extends string>(kind: K) {
65
65
  return defineQueryHandler({
66
66
  name: "t:query:generic",
67
67
  schema,
68
- access: { openToAll: true },
68
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
69
69
  handler: async () => ({ kind, ok: true }) as { kind: K; ok: boolean },
70
70
  });
71
71
  }
@@ -83,7 +83,7 @@ declare const leakingUnion: { ok: true } | { apiKey: Secret<string> };
83
83
  defineQueryHandler({
84
84
  name: "t:query:union-leak",
85
85
  schema,
86
- access: { openToAll: true },
86
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
87
87
  handler: async () => leakingUnion,
88
88
  });
89
89
 
@@ -91,7 +91,7 @@ declare const cleanUnion: { ok: true } | { id: string };
91
91
  defineQueryHandler({
92
92
  name: "t:query:union-clean",
93
93
  schema,
94
- access: { openToAll: true },
94
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
95
95
  handler: async () => cleanUnion,
96
96
  });
97
97
 
@@ -100,7 +100,7 @@ describe("R6 ContainsSecret", () => {
100
100
  const def = defineQueryHandler({
101
101
  name: "t:query:clean",
102
102
  schema,
103
- access: { openToAll: true },
103
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
104
104
  handler: async () => ({ ok: true, value: createSecret("x").reveal() }),
105
105
  });
106
106
  expect(def.name).toBe("t:query:clean");
@@ -20,7 +20,7 @@ const pingFeature = defineFeature("reqhelp", (r) => {
20
20
  isSuccess: true as const,
21
21
  data: { note: event.payload.note, userId: event.user.id },
22
22
  }),
23
- { access: { openToAll: true } },
23
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
24
24
  );
25
25
 
26
26
  r.writeHandler(
@@ -28,7 +28,7 @@ const pingFeature = defineFeature("reqhelp", (r) => {
28
28
  z.object({}),
29
29
  async () =>
30
30
  writeFailure(new UnprocessableError("reqhelp-boom", { i18nKey: "errors.unprocessable" })),
31
- { access: { openToAll: true } },
31
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
32
32
  );
33
33
 
34
34
  r.queryHandler(
@@ -40,7 +40,7 @@ const pingFeature = defineFeature("reqhelp", (r) => {
40
40
  }
41
41
  return { id: query.payload.id, ok: true };
42
42
  },
43
- { access: { openToAll: true } },
43
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
44
44
  );
45
45
  });
46
46
 
@@ -29,7 +29,7 @@ const manualDispatchFeature = defineFeature("manualdispatch", (r) => {
29
29
  await jobRunner.dispatch("manualdispatch:job:record", { note: event.payload.note });
30
30
  return { isSuccess: true as const, data: { id: 1, note: event.payload.note } };
31
31
  },
32
- { access: { openToAll: true } },
32
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
33
33
  );
34
34
  r.job("record", { trigger: { manual: true }, runIn: "worker" }, async (payload) => {
35
35
  jobRuns.push({ name: "manualdispatch:job:record", payload });
@@ -34,7 +34,11 @@ function createTasksFeature() {
34
34
  return defineFeature("tasks", (r) => {
35
35
  r.systemScope();
36
36
  r.entity("task", taskEntity);
37
- r.writeHandler(defineEntityCreateHandler("task", taskEntity, { access: { openToAll: true } }));
37
+ r.writeHandler(
38
+ defineEntityCreateHandler("task", taskEntity, {
39
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
40
+ }),
41
+ );
38
42
  r.screen({
39
43
  id: "task-list",
40
44
  type: "entityList",
@@ -105,7 +109,11 @@ describe("generateE2ESpec", () => {
105
109
  const feature = defineFeature("events", (r) => {
106
110
  r.systemScope();
107
111
  r.entity("event", entity);
108
- r.writeHandler(defineEntityCreateHandler("event", entity, { access: { openToAll: true } }));
112
+ r.writeHandler(
113
+ defineEntityCreateHandler("event", entity, {
114
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
115
+ }),
116
+ );
109
117
  r.screen({
110
118
  id: "event-edit",
111
119
  type: "entityEdit",
@@ -165,7 +173,9 @@ describe("generateE2ESpec", () => {
165
173
  r.systemScope();
166
174
  r.entity("task", taskEntity);
167
175
  r.writeHandler(
168
- defineEntityCreateHandler("task", taskEntity, { access: { openToAll: true } }),
176
+ defineEntityCreateHandler("task", taskEntity, {
177
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
178
+ }),
169
179
  );
170
180
  r.screen({
171
181
  id: "edit",
@@ -201,7 +211,9 @@ describe("generateE2ESpec", () => {
201
211
  r.systemScope();
202
212
  r.entity("contact", contactEntity);
203
213
  r.writeHandler(
204
- defineEntityCreateHandler("contact", contactEntity, { access: { openToAll: true } }),
214
+ defineEntityCreateHandler("contact", contactEntity, {
215
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
216
+ }),
205
217
  );
206
218
  r.screen({
207
219
  id: "list",
@@ -225,7 +237,9 @@ describe("generateE2ESpec", () => {
225
237
  r.systemScope();
226
238
  r.entity("task", taskEntity);
227
239
  r.writeHandler(
228
- defineEntityCreateHandler("task", taskEntity, { access: { openToAll: true } }),
240
+ defineEntityCreateHandler("task", taskEntity, {
241
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
242
+ }),
229
243
  );
230
244
  r.screen({ id: "list", type: "entityList", entity: "task", columns: ["title"] });
231
245
  r.screen({
@@ -27,12 +27,14 @@ export type { ParsedRefTarget } from "../engine/parse-ref-target";
27
27
  export { parseRefTarget } from "../engine/parse-ref-target";
28
28
  export {
29
29
  evalFieldCondition,
30
+ explicitListScreenId,
30
31
  isExtensionEditSection,
31
32
  isFieldsEditSection,
32
33
  isFormatSpec,
33
34
  isWriteFormEditSection,
34
35
  normalizeEditField,
35
36
  normalizeListColumn,
37
+ resolveNavParentScreen,
36
38
  } from "../engine/screen-helpers";
37
39
  // Entity + field types. EntityDefinition is the canonical shape that
38
40
  // view-model builders iterate; FieldDefinition is the per-field union
@@ -99,6 +101,7 @@ export type {
99
101
  PlatformComponent,
100
102
  ProjectionDetailScreenDefinition,
101
103
  ProjectionListScreenDefinition,
104
+ RelatedListToolbarAction,
102
105
  RowAction,
103
106
  RowActionDrawer,
104
107
  RowActionNavigate,
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { generateId } from "../ids";
2
+ import { generateDeterministicId, generateId } from "../ids";
3
3
 
4
4
  // generateId is the row/stream/correlation ID source. The callers rely on
5
5
  // it being a UUIDv7 (time-sortable → dense B-Tree indexes), not a v4 — a
@@ -22,3 +22,31 @@ describe("generateId", () => {
22
22
  expect(new Set(ids).size).toBe(ids.length);
23
23
  });
24
24
  });
25
+
26
+ describe("generateDeterministicId", () => {
27
+ test("returns a well-formed lowercase UUID", () => {
28
+ expect(generateDeterministicId("ledger:schedule", "contract-1")).toMatch(UUID_RX);
29
+ });
30
+
31
+ test("is UUID version 5 (content-addressed, not v7)", () => {
32
+ expect(generateDeterministicId("ledger:schedule", "contract-1")[14]).toBe("5");
33
+ });
34
+
35
+ test("is deterministic: same namespace + key always yields the same id", () => {
36
+ const a = generateDeterministicId("ledger:schedule", "contract-1");
37
+ const b = generateDeterministicId("ledger:schedule", "contract-1");
38
+ expect(a).toBe(b);
39
+ });
40
+
41
+ test("differs when the key changes", () => {
42
+ const a = generateDeterministicId("ledger:schedule", "contract-1");
43
+ const b = generateDeterministicId("ledger:schedule", "contract-2");
44
+ expect(a).not.toBe(b);
45
+ });
46
+
47
+ test("differs when the namespace changes for the same key", () => {
48
+ const a = generateDeterministicId("ledger:schedule", "contract-1");
49
+ const b = generateDeterministicId("ledger:transaction", "contract-1");
50
+ expect(a).not.toBe(b);
51
+ });
52
+ });
package/src/utils/ids.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { v7 } from "uuid";
1
+ import { v5, v7 } from "uuid";
2
2
 
3
3
  // Non-secret identifiers for DB rows, event streams, correlation/request
4
4
  // IDs, SSE connections, distributed locks. UUIDv7: first 48 bits are a
@@ -15,3 +15,12 @@ import { v7 } from "uuid";
15
15
  export function generateId(): string {
16
16
  return v7();
17
17
  }
18
+
19
+ // Derived from a DNS name (RFC 4122 §4.3) so the namespace is reproducible, not a hand-picked constant.
20
+ const DETERMINISTIC_ID_NAMESPACE = v5("kumiko.rocks", v5.DNS);
21
+
22
+ // Same (namespace, key) → same id, so a redelivered event hits the same row. Ids are
23
+ // unique per table, not per tenant: fold the tenant id into `key` or tenants collide.
24
+ export function generateDeterministicId(namespace: string, key: string): string {
25
+ return v5(`${namespace}:${key}`, DETERMINISTIC_ID_NAMESPACE);
26
+ }
@@ -2,7 +2,7 @@ export { assertUnreachable } from "./assert";
2
2
  export { toSnakeCase } from "./case";
3
3
  export { compareByCodepoint } from "./compare";
4
4
  export { readPositiveIntEnv } from "./env-parse";
5
- export { generateId } from "./ids";
5
+ export { generateDeterministicId, generateId } from "./ids";
6
6
  export { isPlainObject } from "./is-plain-object";
7
7
  export { parseStringArrayJson } from "./parse-string-array-json";
8
8
  export { parseJsonOrThrow, parseJsonSafe, stringifyJson } from "./safe-json";