@cosmicdrift/kumiko-framework 0.220.0 → 0.221.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 (73) hide show
  1. package/package.json +3 -3
  2. package/src/__tests__/store-table.integration.test.ts +2 -1
  3. package/src/__tests__/upgrade-cli.test.ts +81 -12
  4. package/src/api/__tests__/request-locale.integration.test.ts +2 -2
  5. package/src/api/api-constants.ts +10 -0
  6. package/src/api/index.ts +1 -0
  7. package/src/api/server.ts +17 -1
  8. package/src/bun-db/__tests__/coerce-row-plain-date.test.ts +2 -0
  9. package/src/bun-db/__tests__/coerce-row-temporal.test.ts +2 -1
  10. package/src/bun-db/index.ts +1 -0
  11. package/src/bun-db/query.ts +30 -14
  12. package/src/crypto/index.ts +1 -0
  13. package/src/crypto/is-self-pii-field.ts +8 -0
  14. package/src/crypto/subject-resolver.ts +4 -3
  15. package/src/db/__tests__/event-store-executor-list.integration.test.ts +31 -2
  16. package/src/db/__tests__/migrate-generator.test.ts +12 -0
  17. package/src/db/__tests__/multi-row-insert.integration.test.ts +2 -0
  18. package/src/db/__tests__/schema-migration.integration.test.ts +1 -0
  19. package/src/db/__tests__/source-shadow-create.integration.test.ts +2 -0
  20. package/src/db/blind-index-cleanup.ts +36 -19
  21. package/src/db/event-store-executor-context.ts +2 -2
  22. package/src/db/event-store-executor-read.ts +7 -6
  23. package/src/db/event-store-executor-write.ts +103 -49
  24. package/src/db/index.ts +2 -0
  25. package/src/db/migrate-generator.ts +14 -0
  26. package/src/db/queries/__tests__/unsafe-read-retrying.test.ts +8 -1
  27. package/src/db/queries/backfill-pii.ts +13 -10
  28. package/src/db/queries/raw-sql.ts +14 -2
  29. package/src/db/queries/seed-context.ts +8 -4
  30. package/src/derivatives/__tests__/variant-route.integration.test.ts +3 -0
  31. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +32 -26
  32. package/src/engine/__tests__/boot-validator.test.ts +29 -3
  33. package/src/engine/__tests__/role-assignment.test.ts +41 -17
  34. package/src/engine/__tests__/schema-builder.test.ts +3 -3
  35. package/src/engine/boot-validator/__tests__/i18n-keys.test.ts +147 -14
  36. package/src/engine/boot-validator/entity-handler.ts +5 -0
  37. package/src/engine/boot-validator/pii-retention.ts +16 -4
  38. package/src/engine/boot-validator/screens.ts +9 -2
  39. package/src/engine/embedded-derived.ts +11 -10
  40. package/src/engine/feature-ast/__tests__/patch.test.ts +10 -0
  41. package/src/engine/feature-ast/patch.ts +9 -0
  42. package/src/engine/role-assignment.ts +36 -18
  43. package/src/errors/__tests__/classes.test.ts +5 -0
  44. package/src/errors/__tests__/write-failures.test.ts +3 -3
  45. package/src/errors/kumiko-error.ts +11 -11
  46. package/src/event-store/__tests__/backfill-pii.integration.test.ts +58 -0
  47. package/src/event-store/__tests__/perf.integration.test.ts +5 -1
  48. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +1 -0
  49. package/src/event-store/event-store.ts +7 -0
  50. package/src/event-store/index.ts +1 -0
  51. package/src/files/__tests__/files.integration.test.ts +181 -2
  52. package/src/files/__tests__/storage-tracking.integration.test.ts +3 -0
  53. package/src/files/file-routes.ts +53 -6
  54. package/src/i18n/__tests__/mail-registry.test.ts +13 -1
  55. package/src/i18n/__tests__/request-locale.test.ts +19 -0
  56. package/src/i18n/index.ts +7 -1
  57. package/src/i18n/mail-registry.ts +10 -0
  58. package/src/i18n/request-locale.ts +11 -2
  59. package/src/i18n/required-surface-keys.ts +3 -1
  60. package/src/lifecycle/signal-handlers.ts +2 -0
  61. package/src/pipeline/__tests__/distributed-lock.integration.test.ts +12 -0
  62. package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.test.ts +26 -42
  63. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +8 -6
  64. package/src/pipeline/distributed-lock.ts +3 -0
  65. package/src/schema-cli.ts +9 -4
  66. package/src/scripts/codemod/crypto-shredding-testing-move.ts +64 -32
  67. package/src/scripts/codemod/pii-personal-migration.ts +30 -14
  68. package/src/search/purge-subject.ts +4 -3
  69. package/src/search/reindex-entity.ts +2 -2
  70. package/src/stack/__tests__/request-helper.integration.test.ts +24 -10
  71. package/src/stack/test-stack.ts +3 -0
  72. package/src/ui-types/index.ts +1 -0
  73. package/src/upgrade-cli.ts +100 -12
@@ -4,6 +4,7 @@ import {
4
4
  readdirSync,
5
5
  readFileSync,
6
6
  realpathSync,
7
+ statSync,
7
8
  writeFileSync,
8
9
  } from "node:fs";
9
10
  import { isAbsolute, join, relative, resolve } from "node:path";
@@ -170,7 +171,23 @@ export function findFeaturesDirs(cwd: string): string[] {
170
171
  // node_modules after a plain npm/bun install (fw#2301).
171
172
  export function findCodemodScriptsRoot(repoRoot: string): string | null {
172
173
  const local = join(repoRoot, "packages/framework/src");
173
- if (existsSync(join(local, CODEMOD_SUBDIR))) return local;
174
+ if (existsSync(join(local, CODEMOD_SUBDIR))) {
175
+ // If package.json exists, require it to be kumiko-framework so a generic
176
+ // monorepo `packages/framework` cannot shadow the installed package.
177
+ // Missing package.json (test fixtures / incomplete trees) keeps prior behavior.
178
+ const pkgPath = join(repoRoot, "packages/framework/package.json");
179
+ if (existsSync(pkgPath)) {
180
+ try {
181
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { name?: string };
182
+ if (pkg.name === "@cosmicdrift/kumiko-framework") return local;
183
+ // wrong name → fall through
184
+ } catch {
185
+ // unreadable → fall through
186
+ }
187
+ } else {
188
+ return local;
189
+ }
190
+ }
174
191
 
175
192
  let dir = repoRoot;
176
193
  for (let i = 0; i < 10; i++) {
@@ -187,7 +204,10 @@ export function findCodemodScriptsRoot(repoRoot: string): string | null {
187
204
  // Resolves a changes.json `codemod` field to an absolute script path,
188
205
  // refusing anything that would escape scripts/codemod/ (path traversal,
189
206
  // absolute paths, symlinks pointing outward) or that isn't a real .ts file.
190
- export function resolveCodemodScript(
207
+ // First arg is the consumer repo root (same as findCodemodScriptsRoot) —
208
+ // not the scripts root — so out-of-tree callers keep compiling against the
209
+ // public upgrade-cli subpath without silently resolving to null (fw#2341).
210
+ function resolveCodemodScriptAt(
191
211
  codemodScriptsRoot: string,
192
212
  codemodField: string | undefined,
193
213
  ): string | null {
@@ -213,6 +233,15 @@ export function resolveCodemodScript(
213
233
  return resolved;
214
234
  }
215
235
 
236
+ export function resolveCodemodScript(
237
+ repoRoot: string,
238
+ codemodField: string | undefined,
239
+ ): string | null {
240
+ const scriptsRoot = findCodemodScriptsRoot(repoRoot);
241
+ if (!scriptsRoot) return null;
242
+ return resolveCodemodScriptAt(scriptsRoot, codemodField);
243
+ }
244
+
216
245
  type CodemodRunResult = { readonly ok: boolean; readonly output: string };
217
246
 
218
247
  // Array-form argv only — never a shell string. The script itself decides
@@ -254,6 +283,42 @@ function writeUpgradeMarker(targetDir: string, marker: UpgradeMarker): void {
254
283
  writeFileSync(join(dir, "upgrade-state.json"), `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
255
284
  }
256
285
 
286
+ /** Highest pending version strictly below the earliest open manual breaking
287
+ * entry (no codemod). Falls back to highest non-breaking pending when every
288
+ * pending version is at/after that manual — never advances onto the manual. */
289
+ /** Highest pending version strictly below the earliest open manual breaking
290
+ * entry (no codemod). Falls back to highest non-breaking pending when every
291
+ * pending version is at/after that manual. If only manuals remain, returns
292
+ * `fallbackInstalled` so the marker stamp moves without claiming manuals done. */
293
+ function markerVersionForPending(
294
+ pending: readonly ChangelogEntry[],
295
+ manualEntries: readonly ChangelogEntry[],
296
+ fallbackInstalled: string,
297
+ ): string {
298
+ const earliestManual = [...manualEntries].sort((a, b) =>
299
+ compareVersions(a.version, b.version),
300
+ )[0];
301
+ const eligible = pending.filter(
302
+ (e) => earliestManual === undefined || compareVersions(e.version, earliestManual.version) < 0,
303
+ );
304
+ const headEligible = eligible[0];
305
+ if (headEligible !== undefined) {
306
+ return eligible.reduce(
307
+ (max, e) => (compareVersions(e.version, max) > 0 ? e.version : max),
308
+ headEligible.version,
309
+ );
310
+ }
311
+ const nonBreaking = pending.filter((e) => e.type !== "breaking");
312
+ const headNonBreaking = nonBreaking[0];
313
+ if (headNonBreaking !== undefined) {
314
+ return nonBreaking.reduce(
315
+ (max, e) => (compareVersions(e.version, max) > 0 ? e.version : max),
316
+ headNonBreaking.version,
317
+ );
318
+ }
319
+ return fallbackInstalled;
320
+ }
321
+
257
322
  // Runs every pending breaking entry's codemod, oldest version first (so a
258
323
  // later codemod can assume an earlier one already ran). Stops on the first
259
324
  // failure — no partial marker. Writes the marker whenever dryRun is false —
@@ -265,13 +330,16 @@ async function applyCodemods(
265
330
  repoRoot: string,
266
331
  targetDir: string,
267
332
  dryRun: boolean,
268
- currentVersion: string,
333
+ // Installed version for the zero-pending bootstrap marker — never a
334
+ // `--from` filter override (that would permanently skip real pending
335
+ // codemods once the CI guard compares against the fake marker).
336
+ markerVersion: string,
269
337
  ): Promise<number> {
270
338
  if (pending.length === 0) {
271
339
  out.log(" ✓ Nothing new since your version.");
272
340
  if (!dryRun) {
273
341
  writeUpgradeMarker(targetDir, {
274
- version: currentVersion,
342
+ version: markerVersion,
275
343
  appliedAt: Temporal.Now.instant().toString(),
276
344
  codemods: [],
277
345
  });
@@ -296,15 +364,28 @@ async function applyCodemods(
296
364
  ? " No automatable codemods among the pending breaking changes."
297
365
  : " ✓ No breaking changes pending.",
298
366
  );
367
+ if (!dryRun) {
368
+ const markerVer = markerVersionForPending(pending, manualEntries, markerVersion);
369
+ writeUpgradeMarker(targetDir, {
370
+ version: markerVer,
371
+ appliedAt: Temporal.Now.instant().toString(),
372
+ codemods: [],
373
+ });
374
+ out.log(` ✓ Applied 0 codemod(s). Wrote ${join(targetDir, ".kumiko/upgrade-state.json")}`);
375
+ }
299
376
  return 0;
300
377
  }
301
378
 
302
379
  const codemodScriptsRoot = findCodemodScriptsRoot(repoRoot);
380
+ if (codemodScriptsRoot === null) {
381
+ out.err(
382
+ ` ✗ could not locate @cosmicdrift/kumiko-framework/src/scripts/codemod — is the framework installed? searched from ${repoRoot} upward`,
383
+ );
384
+ return 1;
385
+ }
303
386
  const ran: UpgradeMarkerCodemod[] = [];
304
387
  for (const e of codemodEntries) {
305
- const scriptPath = codemodScriptsRoot
306
- ? resolveCodemodScript(codemodScriptsRoot, e.codemod)
307
- : null;
388
+ const scriptPath = resolveCodemodScript(repoRoot, e.codemod);
308
389
  if (!scriptPath) {
309
390
  out.err(` ✗ ${e.version} · ${e.title} — invalid codemod path "${e.codemod}"`);
310
391
  return 1;
@@ -325,10 +406,7 @@ async function applyCodemods(
325
406
  return 0;
326
407
  }
327
408
 
328
- const latestVersion = pending.reduce(
329
- (max, e) => (compareVersions(e.version, max) > 0 ? e.version : max),
330
- pending[0]!.version,
331
- );
409
+ const latestVersion = markerVersionForPending(pending, manualEntries, markerVersion);
332
410
  writeUpgradeMarker(targetDir, {
333
411
  version: latestVersion,
334
412
  appliedAt: Temporal.Now.instant().toString(),
@@ -396,9 +474,19 @@ export async function runUpgradeCli(
396
474
  if (getFlag(args, "apply")) {
397
475
  const dirFlag = getStringFlag(args, "dir");
398
476
  const targetDir = dirFlag ? resolve(dirFlag) : cwd;
477
+ if (!existsSync(targetDir) || !statSync(targetDir).isDirectory()) {
478
+ out.err("");
479
+ out.err(` --dir path is not a directory: ${targetDir}`);
480
+ out.err("");
481
+ return 1;
482
+ }
483
+ // Marker must reflect what is actually installed under the target (or
484
+ // cwd), not a `--from` filter override — otherwise CI stays green forever.
485
+ const markerVersion =
486
+ (dirFlag ? readCurrentVersion(targetDir) : null) ?? installedVersion ?? currentVersion;
399
487
  const dryRun = getFlag(args, "dry-run");
400
488
  out.log("");
401
- const code = await applyCodemods(out, pending, repoRoot, targetDir, dryRun, currentVersion);
489
+ const code = await applyCodemods(out, pending, repoRoot, targetDir, dryRun, markerVersion);
402
490
  out.log("");
403
491
  return code;
404
492
  }