@cosmicdrift/kumiko-framework 0.364.0 → 0.365.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 (150) hide show
  1. package/AGENTS.md +36 -5
  2. package/agents/examples/anonymous-access-multitenant.md +9 -6
  3. package/agents/examples/anonymous-access.md +14 -26
  4. package/agents/examples/auth-mfa.md +5 -9
  5. package/agents/examples/custom-fields-basic.md +7 -6
  6. package/agents/examples/custom-handlers.md +72 -25
  7. package/agents/examples/dashboard-stats.md +229 -0
  8. package/agents/examples/encrypted-tenant-config.md +17 -8
  9. package/agents/examples/error-contract.md +1 -5
  10. package/agents/examples/feature-toggles.md +28 -17
  11. package/agents/examples/ledger-banking.md +7 -6
  12. package/agents/examples/ledger-invoicing.md +7 -6
  13. package/agents/examples/legal-pages.md +2 -2
  14. package/agents/examples/managed-pages.md +2 -2
  15. package/agents/examples/notes-history-basic.md +7 -6
  16. package/agents/examples/ownership.md +30 -22
  17. package/agents/examples/pdf-audit-export.md +87 -51
  18. package/agents/examples/session-revocation.md +1 -10
  19. package/agents/examples/tags-basic.md +7 -7
  20. package/agents/examples/user-data-rights.md +25 -17
  21. package/agents/examples/workflow-engine.md +2 -2
  22. package/agents/examples.md +2 -1
  23. package/agents/features.md +3 -3
  24. package/dist/api/auth-routes.d.ts +0 -1
  25. package/dist/api/auth-routes.js +44 -43
  26. package/dist/api/index.d.ts +1 -1
  27. package/dist/api/index.js +1 -1
  28. package/dist/api/server.d.ts +2 -2
  29. package/dist/api/server.js +21 -4
  30. package/dist/bun-db/connection.js +3 -16
  31. package/dist/bun-db/index.d.ts +1 -1
  32. package/dist/bun-db/index.js +1 -1
  33. package/dist/bun-db/query.d.ts +7 -0
  34. package/dist/bun-db/query.js +50 -18
  35. package/dist/crypto/owner-transition-rekey.d.ts +1 -0
  36. package/dist/crypto/owner-transition-rekey.js +7 -3
  37. package/dist/crypto/pii-field-encryption.d.ts +1 -0
  38. package/dist/crypto/pii-field-encryption.js +9 -2
  39. package/dist/crypto/subject-resolver.d.ts +1 -0
  40. package/dist/crypto/subject-resolver.js +7 -0
  41. package/dist/db/api.d.ts +4 -3
  42. package/dist/db/apply-entity-event.js +26 -2
  43. package/dist/db/blind-index-cleanup.js +2 -1
  44. package/dist/db/bun-provider.js +9 -18
  45. package/dist/db/dialect.d.ts +1 -1
  46. package/dist/db/dialect.js +2 -0
  47. package/dist/db/eagerload.js +2 -1
  48. package/dist/db/event-store-executor-context.d.ts +1 -0
  49. package/dist/db/event-store-executor-context.js +1 -0
  50. package/dist/db/event-store-executor-write.d.ts +1 -1
  51. package/dist/db/event-store-executor-write.js +70 -47
  52. package/dist/db/index.d.ts +1 -0
  53. package/dist/db/index.js +1 -0
  54. package/dist/db/migrate-runner.d.ts +1 -0
  55. package/dist/db/migrate-runner.js +56 -31
  56. package/dist/db/queries/backfill-pii.js +63 -25
  57. package/dist/db/queries/event-consumer.js +2 -1
  58. package/dist/db/queries/event-store-transfer.js +15 -4
  59. package/dist/db/queries/event-store.js +11 -10
  60. package/dist/db/queries/pii-backfill-state.js +3 -2
  61. package/dist/db/queries/postgres-array-literal.d.ts +2 -0
  62. package/dist/db/queries/postgres-array-literal.js +6 -0
  63. package/dist/db/queries/projection-rebuild.js +3 -2
  64. package/dist/db/queries/shadow-swap.d.ts +1 -1
  65. package/dist/db/queries/shadow-swap.js +12 -8
  66. package/dist/db/query.d.ts +1 -1
  67. package/dist/db/query.js +1 -1
  68. package/dist/db/retired-framework-tables.js +22 -4
  69. package/dist/db/table-builder.js +6 -2
  70. package/dist/db/tenant-db.d.ts +2 -0
  71. package/dist/db/tenant-db.js +20 -1
  72. package/dist/derivatives/delete-stored-file.d.ts +4 -0
  73. package/dist/derivatives/delete-stored-file.js +9 -0
  74. package/dist/derivatives/index.d.ts +1 -1
  75. package/dist/derivatives/index.js +1 -1
  76. package/dist/engine/boot-validator/entity-handler.js +3 -0
  77. package/dist/engine/boot-validator/query-refs.js +14 -2
  78. package/dist/engine/dedupe-features.js +31 -4
  79. package/dist/engine/entity-handlers.js +14 -7
  80. package/dist/engine/index.d.ts +1 -1
  81. package/dist/engine/index.js +1 -1
  82. package/dist/engine/screen-helpers.d.ts +1 -0
  83. package/dist/engine/screen-helpers.js +2 -2
  84. package/dist/engine/steps/_read-source.js +5 -2
  85. package/dist/engine/tier-resolver-extension.d.ts +1 -0
  86. package/dist/engine/tier-resolver-extension.js +6 -0
  87. package/dist/entrypoint/index.d.ts +2 -2
  88. package/dist/errors/docs-url.js +6 -6
  89. package/dist/errors/index.d.ts +1 -1
  90. package/dist/errors/index.js +1 -1
  91. package/dist/errors/subject-erased.d.ts +1 -0
  92. package/dist/errors/subject-erased.js +6 -0
  93. package/dist/event-store/admin-api.js +3 -2
  94. package/dist/event-store/errors.d.ts +9 -0
  95. package/dist/event-store/errors.js +20 -0
  96. package/dist/event-store/event-store.d.ts +6 -0
  97. package/dist/event-store/event-store.js +28 -6
  98. package/dist/event-store/index.d.ts +1 -1
  99. package/dist/event-store/index.js +1 -1
  100. package/dist/files/create-file-ref.d.ts +2 -1
  101. package/dist/files/create-file-ref.js +16 -2
  102. package/dist/files/dispose-removed-file-refs.js +21 -6
  103. package/dist/files/feature.d.ts +1 -0
  104. package/dist/files/feature.js +2 -1
  105. package/dist/files/file-handle.js +4 -0
  106. package/dist/files/file-routes.js +9 -5
  107. package/dist/files/index.d.ts +1 -1
  108. package/dist/files/index.js +1 -1
  109. package/dist/files/purge-expired-file-ref-bytes.js +2 -3
  110. package/dist/files/resolve-content-type.js +40 -4
  111. package/dist/files/storage-tracking.js +5 -4
  112. package/dist/files/types.js +5 -8
  113. package/dist/files/zip-entry-names.d.ts +1 -0
  114. package/dist/files/zip-entry-names.js +46 -0
  115. package/dist/jobs/job-runner.d.ts +1 -0
  116. package/dist/jobs/job-runner.js +116 -33
  117. package/dist/observability/escape-hatch-report.d.ts +1 -0
  118. package/dist/observability/escape-hatch-report.js +31 -2
  119. package/dist/observability/index.d.ts +1 -1
  120. package/dist/observability/index.js +1 -1
  121. package/dist/observability/metrics-handle.d.ts +1 -0
  122. package/dist/observability/metrics-handle.js +7 -0
  123. package/dist/observability/standard-metrics.js +4 -4
  124. package/dist/pipeline/dispatch-query.d.ts +2 -1
  125. package/dist/pipeline/dispatch-query.js +6 -6
  126. package/dist/pipeline/dispatch-shared.d.ts +1 -1
  127. package/dist/pipeline/dispatch-shared.js +41 -30
  128. package/dist/pipeline/dispatch-stream.js +10 -4
  129. package/dist/pipeline/dispatcher.js +3 -1
  130. package/dist/pipeline/event-dispatcher.d.ts +2 -2
  131. package/dist/pipeline/event-dispatcher.js +11 -9
  132. package/dist/pipeline/event-retention.d.ts +1 -0
  133. package/dist/pipeline/event-retention.js +7 -1
  134. package/dist/pipeline/notify-scope.js +3 -1
  135. package/dist/pipeline/pre-transaction-reservations.js +2 -1
  136. package/dist/schema-cli.js +12 -1
  137. package/dist/stack/db.d.ts +2 -2
  138. package/dist/stack/test-stack.js +2 -1
  139. package/dist/testing/index.d.ts +1 -0
  140. package/dist/testing/index.js +1 -0
  141. package/dist/testing/multipart-helper.d.ts +6 -0
  142. package/dist/testing/multipart-helper.js +1 -1
  143. package/dist/testing/zip-fixture.d.ts +1 -0
  144. package/dist/testing/zip-fixture.js +33 -0
  145. package/dist/ui-types/card-column-roles.js +4 -2
  146. package/dist/ui-types/index.d.ts +1 -1
  147. package/dist/ui-types/index.js +1 -1
  148. package/dist/upgrade-cli.js +46 -13
  149. package/package.json +4 -4
  150. package/src/changes.json +305 -0
@@ -114,7 +114,7 @@ export function createEventDispatcher(options) {
114
114
  // to EVENTS_PUBSUB_CHANNEL; cleared by .stop(). The timer remains active
115
115
  // even with LISTEN attached — it's a cheap safety net against missed
116
116
  // NOTIFYs (subscription drop, crash mid-commit).
117
- let pgUnlisten = null;
117
+ let pgSubscription = null;
118
118
  // Bounds how many consumer turns hold a DB transaction at once. Each turn's
119
119
  // db.begin() checks out one pool connection, plus any writes the handler
120
120
  // itself makes via context.db — the app db pool defaults to postgres-js's
@@ -498,15 +498,16 @@ export function createEventDispatcher(options) {
498
498
  // succeeds (onlisten callback). It drops to 0 on the initial connect
499
499
  // failure, on stop() and when the idle pre-check detects a DB outage.
500
500
  // An outage shorter than one poll tick is not detected; the gauge then
501
- // stays at 1 until postgres.js's delayed re-LISTEN lands.
501
+ // stays at 1 until the driver's delayed re-LISTEN lands.
502
502
  emitEventDispatcherListenConnected(meter, false);
503
503
  if (options.pgClient) {
504
504
  try {
505
505
  const sub = await options.pgClient.listen(EVENTS_PUBSUB_CHANNEL, runBackgroundPass, () => {
506
- // Fires on initial connect and again when postgres.js's own
506
+ // Fires on initial connect and again when the driver's own
507
507
  // re-LISTEN lands after a drop; during an outage that re-LISTEN
508
- // waits in its connection backoff (measured 10-30s after the DB
509
- // returns), polling covers delivery meanwhile. Never unlisten()
508
+ // waits in its connection backoff (postgres-js: measured 10-30s
509
+ // after the DB returns, Bun.SQL: up to 32s), polling covers
510
+ // delivery meanwhile. Never unlisten()
510
511
  // + listen() by hand: that leaves duplicate listeners.
511
512
  // ponytail: no own LISTEN reconnect, the backoff gap is polled;
512
513
  // add a dedicated LISTEN connection if prod wires a pgClient
@@ -514,7 +515,7 @@ export function createEventDispatcher(options) {
514
515
  listenSubscribed = true;
515
516
  emitEventDispatcherListenConnected(meter, true);
516
517
  });
517
- pgUnlisten = sub.unlisten;
518
+ pgSubscription = sub;
518
519
  }
519
520
  catch (e) {
520
521
  emitEventDispatcherListenConnected(meter, false);
@@ -532,12 +533,13 @@ export function createEventDispatcher(options) {
532
533
  clearInterval(timer);
533
534
  timer = null;
534
535
  }
535
- if (pgUnlisten) {
536
- await pgUnlisten().catch(() => {
536
+ if (pgSubscription) {
537
+ // Called on the subscription: Bun.SQL's unlisten is a prototype method.
538
+ await pgSubscription.unlisten().catch(() => {
537
539
  // skip: unlisten failure only matters during shutdown — the
538
540
  // subscription is being torn down anyway.
539
541
  });
540
- pgUnlisten = null;
542
+ pgSubscription = null;
541
543
  listenSubscribed = false;
542
544
  emitEventDispatcherListenConnected(meter, false);
543
545
  }
@@ -6,6 +6,7 @@ export type PruneEventsOptions = {
6
6
  readonly aggregateTypes: readonly string[];
7
7
  readonly aggregateIds?: readonly string[];
8
8
  readonly dryRun?: boolean;
9
+ readonly batchSize?: number;
9
10
  };
10
11
  export type PruneEventsResult = {
11
12
  readonly deletedCount: number;
@@ -34,6 +34,10 @@ export async function pruneEvents(db, options) {
34
34
  if (options.aggregateIds !== undefined && options.aggregateIds.length === 0) {
35
35
  throw new Error("pruneEvents: aggregateIds must be non-empty when set.");
36
36
  }
37
+ if (options.batchSize !== undefined &&
38
+ (!Number.isInteger(options.batchSize) || options.batchSize < 1)) {
39
+ throw new Error("pruneEvents: batchSize must be a positive integer.");
40
+ }
37
41
  const aggregateTypes = options.aggregateTypes;
38
42
  const aggregateIds = options.aggregateIds;
39
43
  const dryRun = options.dryRun === true;
@@ -56,7 +60,9 @@ export async function pruneEvents(db, options) {
56
60
  aggregateType: [...aggregateTypes],
57
61
  ...(aggregateIds ? { aggregateId: [...aggregateIds] } : {}),
58
62
  createdAt: { lt: cutoff },
59
- });
63
+ }, options.batchSize === undefined
64
+ ? undefined
65
+ : { orderBy: { col: "id", direction: "asc" }, limit: options.batchSize });
60
66
  if (candidates.length === 0) {
61
67
  return { deletedCount: 0, cutoff, aggregateTypes, dryRun };
62
68
  }
@@ -23,8 +23,10 @@ export function createDeferringNotify(deps) {
23
23
  assertNotificationVariables(registry, notificationType, options.data);
24
24
  if (options.immediate === true)
25
25
  return notifyNow(notificationType, options);
26
+ // The hook runs after the handler returned; a caller reusing `data` meanwhile must not change what is sent.
27
+ const optionsAtCallTime = structuredClone(options);
26
28
  scheduleAfterCommit(async () => {
27
- await notifyAfterCommit(notificationType, options);
29
+ await notifyAfterCommit(notificationType, optionsAtCallTime);
28
30
  });
29
31
  return { kind: "deferred" };
30
32
  }
@@ -73,7 +73,8 @@ export async function reserveBeforeTransaction(ctx, commands, user, inheritedOri
73
73
  }
74
74
  let prepared;
75
75
  if (handler.beforeTransaction) {
76
- const phase = await handler.beforeTransaction(event, applyBeforeTransactionReadOnly(handlerContext));
76
+ const phaseContext = await buildHandlerContext(ctx, command.type, user, origin, undefined, undefined, undefined, undefined, undefined, true);
77
+ const phase = await handler.beforeTransaction(event, applyBeforeTransactionReadOnly(phaseContext));
77
78
  if (!phase.isSuccess) {
78
79
  await releaseAll(ctx, releases);
79
80
  return { isSuccess: false, error: phase.error, failedIndex: index };
@@ -439,8 +439,19 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
439
439
  await reportLeftoverDroppedTables(db, migrationsDir, out);
440
440
  return pending === 0 ? 0 : 1;
441
441
  }
442
+ catch (e) {
443
+ // Message only, never dbUrl: it can carry the password.
444
+ out.err(` ✗ schema status failed: ${e instanceof Error ? e.message : String(e)}`);
445
+ out.err(' Check that DATABASE_URL is reachable and the database exists (the scaffold\'s docker-compose creates "app").');
446
+ return 1;
447
+ }
442
448
  finally {
443
- await close();
449
+ try {
450
+ await close();
451
+ }
452
+ catch {
453
+ // The connection already failed and was reported above; a failing close must not throw again.
454
+ }
444
455
  }
445
456
  }
446
457
  default: {
@@ -1,9 +1,9 @@
1
- import type { DbConnection } from "@cosmicdrift/kumiko-types/db-connection";
1
+ import type { DbConnection, PgListenClient } from "@cosmicdrift/kumiko-types/db-connection";
2
2
  declare function requireEnv(name: string): string;
3
3
  export type TestDb = {
4
4
  db: DbConnection;
5
5
  outsideTransactionDb: DbConnection;
6
- client: unknown;
6
+ client: PgListenClient;
7
7
  dbName: string;
8
8
  cleanup: () => Promise<void>;
9
9
  };
@@ -275,7 +275,8 @@ export async function setupTestStack(options) {
275
275
  (appContext.redis === undefined || options.rateLimitNowMs !== undefined);
276
276
  const rateLimitFallback = needsOwnRateLimitResolver
277
277
  ? createRateLimitResolver({
278
- redis: testRedis.redis,
278
+ // An app-supplied redis keeps buckets and app redis on the same instance.
279
+ redis: appContext.redis ?? testRedis.redis,
279
280
  ...(options.rateLimitNowMs ? { nowMs: options.rateLimitNowMs } : {}),
280
281
  })
281
282
  : undefined;
@@ -22,3 +22,4 @@ export { sharedItemEntity, sharedItemTable, sharedUserEntity, sharedUserTable, s
22
22
  export { sleep } from "./utils.js";
23
23
  export { waitFor } from "./wait-for.js";
24
24
  export { withoutAmbientTemporal } from "./without-ambient-temporal.js";
25
+ export { zipWithEntries } from "./zip-fixture.js";
@@ -30,3 +30,4 @@ export { sharedItemEntity, sharedItemTable, sharedUserEntity, sharedUserTable, s
30
30
  export { sleep } from "./utils.js";
31
31
  export { waitFor } from "./wait-for.js";
32
32
  export { withoutAmbientTemporal } from "./without-ambient-temporal.js";
33
+ export { zipWithEntries } from "./zip-fixture.js";
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Serializes a FormData instance to multipart/form-data bytes.
3
+ *
4
+ * Returns the encoded body and the Content-Type header value (including the
5
+ * generated boundary). Pass both directly to app.request or fetch.
6
+ */
1
7
  export declare function buildMultipartBody(fd: FormData): Promise<{
2
8
  body: BodyInit;
3
9
  contentType: string;
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  // Workarounds for Bun v1.3.x bun:test limitations with multipart/form-data.
2
3
  //
3
4
  // Two bugs combine to break file upload tests in bun:test:
@@ -19,7 +20,6 @@
19
20
  * Returns the encoded body and the Content-Type header value (including the
20
21
  * generated boundary). Pass both directly to app.request or fetch.
21
22
  */
22
- import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
23
23
  export async function buildMultipartBody(fd) {
24
24
  const boundary = `KumikoBnd${Date.now()}${Math.random().toString(36).slice(2, 8)}`;
25
25
  const enc = new TextEncoder();
@@ -0,0 +1 @@
1
+ export declare function zipWithEntries(...entryNames: readonly string[]): Uint8Array;
@@ -0,0 +1,33 @@
1
+ const LOCAL_HEADER_FIXED_LENGTH = 30;
2
+ const CENTRAL_HEADER_FIXED_LENGTH = 46;
3
+ const EOCD_LENGTH = 22;
4
+ // Minimal stored ZIP (no compression, zero CRC): enough structure for readers that
5
+ // walk the central directory. Not for extraction.
6
+ export function zipWithEntries(...entryNames) {
7
+ const encoder = new TextEncoder();
8
+ const names = entryNames.map((name) => encoder.encode(name));
9
+ const localSize = names.reduce((sum, name) => sum + LOCAL_HEADER_FIXED_LENGTH + name.length, 0);
10
+ const centralSize = names.reduce((sum, name) => sum + CENTRAL_HEADER_FIXED_LENGTH + name.length, 0);
11
+ const bytes = new Uint8Array(localSize + centralSize + EOCD_LENGTH);
12
+ const view = new DataView(bytes.buffer);
13
+ let localOffset = 0;
14
+ let centralOffset = localSize;
15
+ for (const name of names) {
16
+ view.setUint32(localOffset, 0x04034b50, true);
17
+ view.setUint16(localOffset + 26, name.length, true);
18
+ bytes.set(name, localOffset + LOCAL_HEADER_FIXED_LENGTH);
19
+ view.setUint32(centralOffset, 0x02014b50, true);
20
+ view.setUint16(centralOffset + 28, name.length, true);
21
+ view.setUint32(centralOffset + 42, localOffset, true);
22
+ bytes.set(name, centralOffset + CENTRAL_HEADER_FIXED_LENGTH);
23
+ localOffset += LOCAL_HEADER_FIXED_LENGTH + name.length;
24
+ centralOffset += CENTRAL_HEADER_FIXED_LENGTH + name.length;
25
+ }
26
+ const eocdOffset = localSize + centralSize;
27
+ view.setUint32(eocdOffset, 0x06054b50, true);
28
+ view.setUint16(eocdOffset + 8, names.length, true);
29
+ view.setUint16(eocdOffset + 10, names.length, true);
30
+ view.setUint32(eocdOffset + 12, centralSize, true);
31
+ view.setUint32(eocdOffset + 16, localSize, true);
32
+ return bytes;
33
+ }
@@ -18,8 +18,10 @@ export function cardFileDisplayKind(type, renderer) {
18
18
  // boot validator so the validator counts exactly what the card would draw.
19
19
  // The first image column is the card's leading preview, neither title nor meta.
20
20
  export function cardColumnRoles(columns) {
21
- const image = columns.find((col) => col.fileDisplay?.kind === "image");
22
- const textColumns = columns.filter((col) => col !== image);
21
+ const isImage = (col) => col.fileDisplay?.kind === "image";
22
+ const image = columns.find((col) => isImage(col) && col.hideOnNarrow !== true);
23
+ // A narrow-hidden image must not turn into the title just because it is not the preview.
24
+ const textColumns = columns.filter((col) => col !== image && !(isImage(col) && col.hideOnNarrow === true));
23
25
  const title = textColumns.find((col) => col.highlighted === true) ?? textColumns[0];
24
26
  const status = textColumns.find((col) => col !== title && col.type === "select" && col.renderer === undefined);
25
27
  const meta = textColumns.filter((col) => col !== title && col !== status && col.hideOnNarrow !== true);
@@ -4,7 +4,7 @@ export { computeDerivedCellValue, roundDerivedCellValue } from "../engine/embedd
4
4
  export type { ParsedRefTarget } from "../engine/parse-ref-target.js";
5
5
  export { parseRefTarget } from "../engine/parse-ref-target.js";
6
6
  export type { FieldsOrGroupsSection, UnavailableSelectOption } from "../engine/screen-helpers.js";
7
- export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isRecordLocked, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
7
+ export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isRecordLocked, isUnchangedValue, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
8
8
  export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefinition, FieldDefinition, FileFieldDef, FilesFieldDef, ImageFieldDef, ImagesFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, SelectFieldDef, SelectOptionTone, TextFieldDef, } from "../engine/types/fields.js";
9
9
  export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.js";
10
10
  export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
@@ -22,7 +22,7 @@
22
22
  export { NO_WIDGET_FIELD_TYPES } from "@cosmicdrift/kumiko-types/fields";
23
23
  export { computeDerivedCellValue, roundDerivedCellValue } from "../engine/embedded-derived.js";
24
24
  export { parseRefTarget } from "../engine/parse-ref-target.js";
25
- export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isRecordLocked, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
25
+ export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isRecordLocked, isUnchangedValue, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
26
26
  export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
27
27
  export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.js";
28
28
  export { ACTION_FORM_ENTITY, handlerFieldLabelKey, handlerTitleKey, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
@@ -1,10 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from "node:fs";
3
- import { isAbsolute, join, relative, resolve } from "node:path";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { getFlag, getStringFlag, parseArgs } from "./arg-parser.js";
5
5
  import { compareVersions, filterEntriesAfter, parseFeatureChangelog, sortEntries, } from "./engine/index.js";
6
6
  import { ensureTemporalPolyfill } from "./time/index.js";
7
7
  const SEMVER_RE = /^\d+\.\d+\.\d+$/;
8
+ const SEMVER_CORE_RE = /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?$/;
8
9
  const CODEMOD_SUBDIR = "scripts/codemod";
9
10
  function readPackageJsonVersion(packageJsonPath) {
10
11
  if (!existsSync(packageJsonPath))
@@ -21,20 +22,49 @@ function readPackageJsonVersion(packageJsonPath) {
21
22
  }
22
23
  // bun.lock pins transitive packages too, so a repo that only depends on
23
24
  // kumiko-cli/-guards still records the bundled-features version it resolved.
24
- function readLockfileVersion(root, pkgName) {
25
- const lockPath = join(root, "bun.lock");
25
+ // Prerelease and snapshot pins (0.364.0-next.1, 0.0.0-pr-123) are reduced to
26
+ // their x.y.z core, the only shape the rest of the CLI compares.
27
+ function readLockfileVersion(dir, pkgName) {
28
+ const lockPath = join(dir, "bun.lock");
26
29
  if (!existsSync(lockPath))
27
30
  return null;
28
- const pinned = new RegExp(`"@cosmicdrift/${pkgName}": \\["@cosmicdrift/${pkgName}@(\\d+\\.\\d+\\.\\d+)"`).exec(readFileSync(lockPath, "utf-8"));
29
- return pinned?.[1] ?? null;
31
+ const pinned = new RegExp(`"@cosmicdrift/${pkgName}": \\["@cosmicdrift/${pkgName}@([^"]+)"`).exec(readFileSync(lockPath, "utf-8"));
32
+ return pinned?.[1] === undefined ? null : (SEMVER_CORE_RE.exec(pinned[1])?.[1] ?? null);
30
33
  }
31
- // Only the repo's own install, package dir and lockfile count. Walking up
32
- // from a worktree nested in a parent workspace read that workspace's version,
33
- // and the isolated linker keeps transitive packages out of node_modules.
34
+ function declaresWorkspaces(packageJsonPath) {
35
+ if (!existsSync(packageJsonPath))
36
+ return false;
37
+ try {
38
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
39
+ return typeof pkg === "object" && pkg !== null && "workspaces" in pkg;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ function isRepoBoundary(dir) {
46
+ return existsSync(join(dir, ".git")) || declaresWorkspaces(join(dir, "package.json"));
47
+ }
48
+ function readPackageVersionAt(dir, pkgName, repoLocalPath) {
49
+ return (readPackageJsonVersion(join(dir, `node_modules/@cosmicdrift/${pkgName}/package.json`)) ??
50
+ readPackageJsonVersion(join(dir, repoLocalPath)) ??
51
+ readLockfileVersion(dir, pkgName));
52
+ }
53
+ // Walks up from a workspace member to the repo's own root (.git or a
54
+ // package.json with workspaces) but never past it: walking on from a worktree
55
+ // nested in a parent workspace read that workspace's version, and the
56
+ // isolated linker keeps transitive packages out of node_modules.
34
57
  function readPackageVersion(root, pkgName, repoLocalPath) {
35
- return (readPackageJsonVersion(join(root, `node_modules/@cosmicdrift/${pkgName}/package.json`)) ??
36
- readPackageJsonVersion(join(root, repoLocalPath)) ??
37
- readLockfileVersion(root, pkgName));
58
+ let dir = root;
59
+ for (;;) {
60
+ const version = readPackageVersionAt(dir, pkgName, repoLocalPath);
61
+ if (version !== null || isRepoBoundary(dir))
62
+ return version;
63
+ const parent = dirname(dir);
64
+ if (parent === dir)
65
+ return null;
66
+ dir = parent;
67
+ }
38
68
  }
39
69
  // Changelog entries come from @cosmicdrift/kumiko-bundled-features (see
40
70
  // findFeaturesDirs); comparing against the framework version instead
@@ -508,6 +538,8 @@ markerVersion, previous) {
508
538
  function parseResolveInput(args) {
509
539
  if (getFlag(args, "apply"))
510
540
  return { error: "--resolve cannot be combined with --apply." };
541
+ if (getFlag(args, "dry-run"))
542
+ return { error: "--resolve cannot be combined with --dry-run." };
511
543
  const refs = (getStringFlag(args, "resolve") ?? "")
512
544
  .split(",")
513
545
  .map((ref) => ref.trim())
@@ -565,8 +597,9 @@ function resolveManualMigrations(out, args, targetDir) {
565
597
  if (errors.length > 0)
566
598
  return fail(...errors, "Nothing was written.");
567
599
  const { reason } = input;
568
- const notApplicableFlag = args.flags.get("not-applicable");
569
- const resolution = notApplicableFlag !== undefined && notApplicableFlag !== false ? "not-applicable" : "migrated";
600
+ const resolution = getFlag(args, "not-applicable")
601
+ ? "not-applicable"
602
+ : "migrated";
570
603
  const resolvedAt = Temporal.Now.instant().toString();
571
604
  const newlyResolved = open
572
605
  .filter((entry) => resolvedIds.has(entry.id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.364.0",
3
+ "version": "0.365.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -210,8 +210,8 @@
210
210
  "./package.json": "./package.json"
211
211
  },
212
212
  "dependencies": {
213
- "@cosmicdrift/kumiko-http": "0.364.0",
214
- "@cosmicdrift/kumiko-types": "0.364.0",
213
+ "@cosmicdrift/kumiko-http": "0.365.0",
214
+ "@cosmicdrift/kumiko-types": "0.365.0",
215
215
  "bullmq": "^5.76.7",
216
216
  "bun-types": "^1.3.13",
217
217
  "hono": "^4.13.1",
@@ -227,7 +227,7 @@
227
227
  "zod": "^4.4.3"
228
228
  },
229
229
  "devDependencies": {
230
- "@cosmicdrift/kumiko-dispatcher-live": "0.364.0",
230
+ "@cosmicdrift/kumiko-dispatcher-live": "0.365.0",
231
231
  "bun-types": "^1.3.13",
232
232
  "pino-pretty": "^13.1.3"
233
233
  },