@cosmicdrift/kumiko-framework 0.349.0 → 0.351.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.
@@ -19,9 +19,9 @@
19
19
  // drizzle's getTableName + getTableColumns (drizzle kept only as a type
20
20
  // reference, never as a runtime API call)
21
21
  import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
22
- // Static polyfill import (not getTemporal()): coercion results stay on one
23
- // Temporal implementation repo-wide, even when Bun exposes a global (#1480).
24
- import { Temporal } from "temporal-polyfill";
22
+ // Static import (not getTemporal()): coercion works without an installed
23
+ // global and returns the same Temporal classes as the rest of the repo (#1480).
24
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
25
25
  import { requestContext } from "../api/request-context.js";
26
26
  import { computeBlindIndex, configuredBlindIndexKey } from "../crypto/blind-index.js";
27
27
  import { isPiiCiphertext } from "../crypto/pii-ciphertext-format.js";
@@ -1,5 +1,5 @@
1
1
  import { type ColumnHandle, type SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
2
- import { Temporal } from "temporal-polyfill";
2
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
3
  import type { PgType } from "./entity-table-meta.js";
4
4
  export type { ColumnHandle, SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
5
5
  export type TableColumns<_T = any> = SchemaTable;
@@ -13,10 +13,11 @@
13
13
  // The framework no longer imports drizzle-orm at runtime — schema-files
14
14
  // use only this module.
15
15
  //
16
- // Static import, not the ambient global: Bun doesn't expose Temporal on
17
- // globalThis, so instantToDriver crashed on timestamptz writes (#1480).
16
+ // Static import of the shared Temporal module, not the ambient global: the
17
+ // global may be absent (Hermes/Safari) or torn down in tests, and instantToDriver
18
+ // crashed on timestamptz writes without it (#1480).
18
19
  import { KUMIKO_COLUMNS_SYMBOL, KUMIKO_META_SYMBOL, KUMIKO_NAME_SYMBOL, } from "@cosmicdrift/kumiko-types/schema-table-types";
19
- import { Temporal } from "temporal-polyfill";
20
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
20
21
  function isNumericPgType(t) {
21
22
  return t.startsWith("numeric(");
22
23
  }
@@ -11,7 +11,7 @@
11
11
  // Static import, not the ambient global: Bun doesn't expose Temporal on
12
12
  // globalThis, so this crashed with "Temporal is not defined" outside boot
13
13
  // paths that install it (#1480).
14
- import { Temporal } from "temporal-polyfill";
14
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
15
15
  // Sprint F: <name>Utc-Spalte ist jetzt instant() (siehe dialect.ts) —
16
16
  // Drizzle gibt direkt Temporal.Instant zurück. Vor Sprint F kam ein PG-
17
17
  // Wire-Format-String "2026-04-15 09:00:00+00" rein der via String-Massage
@@ -167,20 +167,20 @@ export declare function buildBaseColumns(softDelete: boolean, idType?: "serial"
167
167
  id: ColumnBuilder<string> | ColumnBuilder<number>;
168
168
  tenantId: ColumnBuilder<string>;
169
169
  version: ColumnBuilder<number>;
170
- insertedAt: ColumnBuilder<import("temporal-spec").Temporal.Instant>;
171
- modifiedAt: ColumnBuilder<import("temporal-spec").Temporal.Instant>;
170
+ insertedAt: ColumnBuilder<Temporal.Instant>;
171
+ modifiedAt: ColumnBuilder<Temporal.Instant>;
172
172
  insertedById: ColumnBuilder<string>;
173
173
  modifiedById: ColumnBuilder<string>;
174
174
  } | {
175
175
  id: ColumnBuilder<string> | ColumnBuilder<number>;
176
176
  tenantId: ColumnBuilder<string>;
177
177
  version: ColumnBuilder<number>;
178
- insertedAt: ColumnBuilder<import("temporal-spec").Temporal.Instant>;
179
- modifiedAt: ColumnBuilder<import("temporal-spec").Temporal.Instant>;
178
+ insertedAt: ColumnBuilder<Temporal.Instant>;
179
+ modifiedAt: ColumnBuilder<Temporal.Instant>;
180
180
  insertedById: ColumnBuilder<string>;
181
181
  modifiedById: ColumnBuilder<string>;
182
182
  isDeleted: ColumnBuilder<boolean>;
183
- deletedAt: ColumnBuilder<import("temporal-spec").Temporal.Instant>;
183
+ deletedAt: ColumnBuilder<Temporal.Instant>;
184
184
  deletedById: ColumnBuilder<string>;
185
185
  };
186
186
  export declare function rowMetaFieldNames(softDelete: boolean): ReadonlySet<string>;
@@ -175,6 +175,9 @@ function checkVisibleWhenField(queryHandlers, visibleWhen, owner) {
175
175
  }
176
176
  function checkDashboardOutputFields(queryHandlers, featureName, screenId, screen) {
177
177
  for (const panel of screen.panels) {
178
+ if (panel.kind !== "custom") {
179
+ checkVisibleWhenField(queryHandlers, panel.visibleWhen, `[Feature ${featureName}] Screen "${screenId}" (dashboard) ${panel.kind === "screen" ? "screen-panel" : "panel"} "${panel.id}"`);
180
+ }
178
181
  if (panel.kind === "stat") {
179
182
  checkDashboardStatPanelFields(queryHandlers, featureName, screenId, panel);
180
183
  }
@@ -183,9 +186,6 @@ function checkDashboardOutputFields(queryHandlers, featureName, screenId, screen
183
186
  checkDashboardStatPanelFields(queryHandlers, featureName, screenId, stat);
184
187
  }
185
188
  }
186
- else if (panel.kind === "screen") {
187
- checkVisibleWhenField(queryHandlers, panel.visibleWhen, `[Feature ${featureName}] Screen "${screenId}" (dashboard) screen-panel "${panel.id}"`);
188
- }
189
189
  else if (panel.kind === "list") {
190
190
  const rowShape = resolveListRowShape(queryHandlers, panel.query, `[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}"`);
191
191
  for (const column of panel.columns) {
@@ -26,12 +26,11 @@ function checkDashboardQueryRefs(queryHandlers, featureName, screenId, screen) {
26
26
  for (const panel of screen.panels) {
27
27
  if (panel.kind === "custom")
28
28
  continue;
29
- if (panel.kind === "screen") {
30
- if (panel.visibleWhen !== undefined) {
31
- checkQueryRef(queryHandlers, panel.visibleWhen.query, () => `[Feature ${featureName}] Screen "${screenId}" (dashboard) screen-panel "${panel.id}" visibleWhen`);
32
- }
33
- continue;
29
+ if (panel.visibleWhen !== undefined) {
30
+ checkQueryRef(queryHandlers, panel.visibleWhen.query, () => `[Feature ${featureName}] Screen "${screenId}" (dashboard) ${panel.kind === "screen" ? "screen-panel" : "panel"} "${panel.id}" visibleWhen`);
34
31
  }
32
+ if (panel.kind === "screen")
33
+ continue;
35
34
  if (panel.kind === "stat-group") {
36
35
  for (const stat of panel.stats) {
37
36
  checkQueryRef(queryHandlers, stat.query, () => `[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}"`);
@@ -1729,17 +1729,27 @@ export function validateDashboardScreenPanel(featureName, screenId, panel, featu
1729
1729
  `navigate away from the dashboard on submit or cancel. Embed a screen without redirect and ` +
1730
1730
  `with cancelTarget: false (or omitted).`);
1731
1731
  }
1732
- const visibleWhen = panel.visibleWhen;
1732
+ validateDashboardPanelVisibleWhen(context, panel.visibleWhen);
1733
+ }
1734
+ function validateDashboardPanelVisibleWhen(context, visibleWhen) {
1733
1735
  if (visibleWhen !== undefined && (visibleWhen.query === "" || visibleWhen.field === "")) {
1734
1736
  throw new Error(`${context} visibleWhen needs a non-empty query and field.`);
1735
1737
  }
1736
1738
  }
1737
1739
  function validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelId) {
1740
+ const context = `[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}"`;
1738
1741
  if (panel.stats.length === 0) {
1739
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" has an empty stats list.`);
1742
+ throw new Error(`${context} has an empty stats list.`);
1743
+ }
1744
+ if (panel.subtitle !== undefined && panel.label === undefined) {
1745
+ throw new Error(`${context} sets subtitle without label — an unlabeled group renders as a KPI strip without a header.`);
1740
1746
  }
1747
+ validateDashboardPanelVisibleWhen(context, panel.visibleWhen);
1741
1748
  for (const stat of panel.stats) {
1742
1749
  addPanelId(stat.id, "stat-group child");
1750
+ if (stat.visibleWhen !== undefined) {
1751
+ throw new Error(`${context} child "${stat.id}" sets visibleWhen — gate the whole group instead; a hidden cell would leave a gap in the strip.`);
1752
+ }
1743
1753
  if (!stat.query || typeof stat.query !== "string") {
1744
1754
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty or non-string query.`);
1745
1755
  }
@@ -1758,6 +1768,67 @@ function validateDashboardChartDisplay(featureName, screenId, panel) {
1758
1768
  if (panel.scrollable === true && panel.chart !== "stacked-area") {
1759
1769
  throw new Error(`${where} sets scrollable on chart "${panel.chart}" — only "stacked-area" supports it.`);
1760
1770
  }
1771
+ validateStackedAreaOnlyProps(where, panel);
1772
+ if (panel.legendTotals !== undefined && typeof panel.legendTotals !== "boolean") {
1773
+ throw new Error(`${where} has a non-boolean legendTotals.`);
1774
+ }
1775
+ for (const [key, color] of Object.entries(panel.seriesColors ?? {})) {
1776
+ if (typeof color !== "string" || color.trim() === "") {
1777
+ throw new Error(`${where} seriesColors["${key}"] must be a non-empty CSS color.`);
1778
+ }
1779
+ }
1780
+ for (const [kind, look] of Object.entries(panel.markerKinds ?? {})) {
1781
+ validateDashboardChartMarkerKind(`${where} markerKinds["${kind}"]`, look);
1782
+ }
1783
+ if (panel.ranges !== undefined)
1784
+ validateDashboardChartRanges(`${where} ranges`, panel.ranges);
1785
+ }
1786
+ function validateStackedAreaOnlyProps(where, panel) {
1787
+ // skip: stacked-area is the one chart kind that supports these props
1788
+ if (panel.chart === "stacked-area")
1789
+ return;
1790
+ const stackedAreaOnly = ["markerKinds", "legendTotals", "ranges"].filter((prop) => panel[prop] !== undefined);
1791
+ if (stackedAreaOnly.length > 0) {
1792
+ throw new Error(`${where} sets ${stackedAreaOnly.join(", ")} on chart "${panel.chart}" — only "stacked-area" supports it.`);
1793
+ }
1794
+ }
1795
+ const DASHBOARD_CHART_TONES = new Set([
1796
+ "positive",
1797
+ "negative",
1798
+ "active",
1799
+ "neutral",
1800
+ ]);
1801
+ function validateDashboardChartMarkerKind(where, look) {
1802
+ if (look.tone === undefined && look.color === undefined) {
1803
+ throw new Error(`${where} needs a tone or a color.`);
1804
+ }
1805
+ if (look.tone !== undefined && !DASHBOARD_CHART_TONES.has(look.tone)) {
1806
+ throw new Error(`${where}.tone "${String(look.tone)}" is not one of ${[...DASHBOARD_CHART_TONES].join(", ")}.`);
1807
+ }
1808
+ if (look.color !== undefined && (typeof look.color !== "string" || look.color.trim() === "")) {
1809
+ throw new Error(`${where}.color must be a non-empty CSS color.`);
1810
+ }
1811
+ }
1812
+ function validateDashboardChartRanges(where, ranges) {
1813
+ if (ranges.options.length === 0) {
1814
+ throw new Error(`${where}.options is empty — declare at least one option.`);
1815
+ }
1816
+ const values = new Set();
1817
+ for (const option of ranges.options) {
1818
+ if (option.value.length === 0 || option.label.length === 0) {
1819
+ throw new Error(`${where} option needs a non-empty value and label.`);
1820
+ }
1821
+ if (values.has(option.value)) {
1822
+ throw new Error(`${where}.options has duplicate value "${option.value}".`);
1823
+ }
1824
+ if (option.months !== undefined && (!Number.isInteger(option.months) || option.months <= 0)) {
1825
+ throw new Error(`${where} option "${option.value}" months "${String(option.months)}" must be a positive integer.`);
1826
+ }
1827
+ values.add(option.value);
1828
+ }
1829
+ if (!values.has(ranges.default)) {
1830
+ throw new Error(`${where}.default "${ranges.default}" is not among the options (${[...values].join(", ")}).`);
1831
+ }
1761
1832
  }
1762
1833
  const CURRENCY_CODE_RE = /^[A-Z]{3}$/;
1763
1834
  const MAX_VALUE_FORMAT_FRACTION_DIGITS = 4;
@@ -1797,6 +1868,7 @@ function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
1797
1868
  }
1798
1869
  validateDashboardValueFormat(featureName, screenId, panel.id, panel.valueFormat);
1799
1870
  }
1871
+ validateDashboardPanelVisibleWhen(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}"`, panel.visibleWhen);
1800
1872
  if (panel.kind === "chart")
1801
1873
  validateDashboardChartDisplay(featureName, screenId, panel);
1802
1874
  if (panel.kind === "list") {
@@ -1,7 +1,7 @@
1
1
  // ISO-8601 duration arithmetic — shared by wait and waitForEvent steps.
2
2
  // Accepts "P1D", "PT1H", "P1Y2M3DT4H5M6S" etc.
3
3
  // Uses approximate calendar math (365d/year, 30d/month). See #23.
4
- import { Temporal } from "temporal-polyfill";
4
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
5
5
  export function addDuration(baseIso, duration) {
6
6
  const pattern = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
7
7
  const match = duration.match(pattern);
@@ -17,7 +17,7 @@ export type { ContentCollectionDefinition, NavDefinition, NavIconKey, } from "@c
17
17
  export type { FromRule, FromRuleKind, OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule, SqlFragment, WhereRule, WhereRuleContext, } from "@cosmicdrift/kumiko-types/ownership";
18
18
  export type { EntityProjectionExtension, MspErrorMode, MspErrorPolicy, MultiStreamApplyFn, MultiStreamProjectionDefinition, ProjectionDefinition, ProjectionRowIdOf, ProjectionTable, SingleStreamApplyFn, } from "@cosmicdrift/kumiko-types/projection";
19
19
  export type { BelongsToRelation, EntityRelations, HasManyRelation, ManyToManyRelation, RelationDefinition, } from "@cosmicdrift/kumiko-types/relations";
20
- export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RecordHeaderSubtitlePart, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
20
+ export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelGate, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RecordHeaderSubtitlePart, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
21
21
  export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-types/screen";
22
22
  export type { TargetRef } from "@cosmicdrift/kumiko-types/target-ref";
23
23
  export type { Subscribe, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, } from "@cosmicdrift/kumiko-types/tree-node";
@@ -1,5 +1,6 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
2
  import { type EventMetadata, type StoredEvent } from "@cosmicdrift/kumiko-types/event-store-types";
3
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
4
  import type { DbRunner } from "../db/index.js";
4
5
  import type { TenantId } from "../engine/types/index.js";
5
6
  export type { EventMetadata, StoredEvent } from "@cosmicdrift/kumiko-types/event-store-types";
@@ -1,12 +1,6 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
2
  import { isPersonalDataGated, UNATTRIBUTED_ORIGIN, } from "@cosmicdrift/kumiko-types/event-store-types";
3
- // Value-only import, aliased to avoid shadowing the ambient global
4
- // `Temporal` TYPE this file's other Temporal.Instant annotations resolve
5
- // against (StoredEvent.createdAt et al. — importing the bare name here
6
- // conflicts with that ambient type, see #1438). Bun doesn't expose Temporal
7
- // as a runtime value on globalThis, so the un-aliased call below crashed
8
- // with "Temporal is not defined" outside boot paths that install it (#1480).
9
- import { Temporal as TemporalPolyfill } from "temporal-polyfill";
3
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
10
4
  import { requestContext } from "../api/request-context.js";
11
5
  import { encryptEventPayloadPii } from "../crypto/event-pii.js";
12
6
  import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
@@ -150,18 +144,11 @@ async function insertSubsequentEvent(db, event, newVersion, eventVersion, seedCr
150
144
  if (!row)
151
145
  throw new VersionConflictError(event.aggregateId, event.expectedVersion);
152
146
  const createdAt = row.created_at instanceof Date
153
- ? TemporalPolyfill.Instant.fromEpochMilliseconds(row.created_at.getTime())
154
- : TemporalPolyfill.Instant.from(row.created_at);
147
+ ? Temporal.Instant.fromEpochMilliseconds(row.created_at.getTime())
148
+ : Temporal.Instant.from(row.created_at);
155
149
  return {
156
150
  id: typeof row.id === "bigint" ? row.id : BigInt(row.id),
157
- // @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal.Instant
158
- // at runtime, two separate .d.ts sources (temporal-polyfill's bundled
159
- // types vs the ambient temporal-spec global) disagree on a couple of
160
- // method-overload signatures (until/equals argument types), so TS treats
161
- // them as nominally distinct. Safe: this value is only ever consumed as
162
- // a Temporal.Instant (comparisons, .toString(), arithmetic), never
163
- // narrowed on the specific overload shape that differs.
164
- createdAt: createdAt,
151
+ createdAt,
165
152
  };
166
153
  }
167
154
  function buildStoredEvent(event, newVersion, eventVersion, row) {
@@ -90,8 +90,10 @@ function pushDashboardPanelKeys(out, panel) {
90
90
  if (panel.kind === "custom")
91
91
  return;
92
92
  pushKey(out, panel.label);
93
- if (panel.kind === "chart")
93
+ if (panel.kind === "chart" || panel.kind === "stat-group")
94
94
  pushKey(out, panel.subtitle);
95
+ if (panel.kind === "chart")
96
+ pushDashboardChartRangeKeys(out, panel);
95
97
  if (panel.kind === "chart" ||
96
98
  panel.kind === "list" ||
97
99
  panel.kind === "feed" ||
@@ -111,6 +113,10 @@ function pushDashboardPanelKeys(out, panel) {
111
113
  }
112
114
  }
113
115
  }
116
+ function pushDashboardChartRangeKeys(out, panel) {
117
+ for (const option of panel.ranges?.options ?? [])
118
+ pushKey(out, option.label);
119
+ }
114
120
  function pushDashboardFilterKeys(out, filter) {
115
121
  pushKey(out, filter.label);
116
122
  if (filter.allLabel !== undefined)
@@ -1,4 +1,5 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
3
  import type { DbConnection, DbRunner, DbTx } from "../db/connection.js";
3
4
  import { type PendingIdRange } from "../db/queries/event-store.js";
4
5
  import type { AppContext } from "../engine/types/index.js";
@@ -1,8 +1,5 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
- // Value-only import, aliased to avoid shadowing the ambient global
3
- // `Temporal` TYPE that ConsumerStateRow.updatedAt/StoredEventRow.createdAt
4
- // resolve against (same #1438 dual-package-hazard pattern as event-store.ts).
5
- import { Temporal as TemporalPolyfill } from "temporal-polyfill";
2
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
6
3
  import { requestContext } from "../api/request-context.js";
7
4
  import { insertConsumerIfAbsent, markConsumerProcessing, rearmDeadConsumer, selectConsumerForUpdate, selectConsumerForUpdateSkipLocked, selectProvablyIdleConsumerPairs, updateConsumerDeliveryOutcome, } from "../db/queries/event-consumer.js";
8
5
  import { selectEventsHeadId, selectPendingAndNewEventRows, } from "../db/queries/event-store.js";
@@ -54,14 +51,10 @@ export async function acquireConsumerState(tx, name, instanceId, rearmCooldownMs
54
51
  // a prior re-arm) gates the retry; maxRearmCount stops a poison event
55
52
  // from looping forever (re-arm → same event fails → dead → re-arm →
56
53
  // ...) — after the cap it stays dead until a human intervenes.
57
- const cooldownDeadline = TemporalPolyfill.Now.instant().subtract({
54
+ const cooldownDeadline = Temporal.Now.instant().subtract({
58
55
  milliseconds: rearmCooldownMs,
59
56
  });
60
- // @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal.Instant
61
- // at runtime — state.updatedAt is DB-row-typed against the ambient
62
- // global, two distinct nominal types across the two .d.ts sources (see
63
- // event-store.ts).
64
- const cooldownElapsed = TemporalPolyfill.Instant.compare(state.updatedAt, cooldownDeadline) <= 0;
57
+ const cooldownElapsed = Temporal.Instant.compare(state.updatedAt, cooldownDeadline) <= 0;
65
58
  if (cooldownElapsed && state.rearmCount < maxRearmCount) {
66
59
  const rearmed = await rearmDeadConsumer(tx, name, instanceId);
67
60
  const rearmedState = rearmed &&
@@ -1,7 +1,4 @@
1
- // Value-only import, aliased to avoid shadowing the ambient global
2
- // `Temporal` TYPE that RateLimitDecision.resetAt resolves against (see
3
- // event-store.ts for the same #1438 dual-package-hazard pattern).
4
- import { Temporal as TemporalPolyfill } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
5
2
  import { RateLimitError } from "../errors/index.js";
6
3
  import { RedisKeys } from "../pipeline/redis-keys.js";
7
4
  import { toPublicBucketName } from "./bucket.js";
@@ -134,7 +131,7 @@ export function createRateLimitResolver(opts) {
134
131
  const nowMs = now();
135
132
  const [allowedFlag, remaining, retryAfterMs] = await client.kumikoRateLimit(`${prefix}${bucket}`, String(config.limit), String(refillRatePerMs), String(cost), String(nowMs), String(ttlSeconds));
136
133
  const retryAfterSeconds = Math.ceil(retryAfterMs / 1000);
137
- const resetAt = TemporalPolyfill.Instant.fromEpochMilliseconds(nowMs + retryAfterMs);
134
+ const resetAt = Temporal.Instant.fromEpochMilliseconds(nowMs + retryAfterMs);
138
135
  return {
139
136
  allowed: allowedFlag === 1,
140
137
  limit: config.limit,
@@ -162,7 +159,7 @@ export function createRateLimitResolver(opts) {
162
159
  const nowMs = now();
163
160
  const [remaining, retryAfterMs] = await client.kumikoRateLimitPeek(`${prefix}${bucket}`, String(config.limit), String(refillRatePerMs), String(nowMs));
164
161
  const retryAfterSeconds = Math.ceil(retryAfterMs / 1000);
165
- const resetAt = TemporalPolyfill.Instant.fromEpochMilliseconds(nowMs + retryAfterMs);
162
+ const resetAt = Temporal.Instant.fromEpochMilliseconds(nowMs + retryAfterMs);
166
163
  return {
167
164
  // peek doesn't deduct, so a "would-be" allowed flag is meaningful:
168
165
  // true iff at least one token is available right now.
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  export declare function instantToLegacyDate(instant: Temporal.Instant): Date;
3
3
  /** Gegenrichtung: Date aus einer Fremd-Lib (imapflow INTERNALDATE,
4
4
  * mailparser Date-Header) → Temporal.Instant. */
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  // Temporal→Date-Bridge für Lib-APIs die JS-Date verlangen (imapflow
3
3
  // search({since}), nodemailer-Header, ...). Gegenstück zur
4
4
  // dateToInstant-Richtung der DB-/Event-Store-Boundaries: Feature-Code
@@ -1,4 +1,5 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
3
  /**
3
4
  * Ensure `globalThis.Temporal` is available. Idempotent.
4
5
  */
@@ -1,45 +1,26 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
- // Temporal polyfill bootstrap.
2
+ // Temporal bootstrap.
3
3
  //
4
- // Temporal is native in Chromium 144+ / Firefox 139+, but missing in Safari,
5
- // iOS, and Hermes. Bun/Node coverage is incomplete. Boot installs
6
- // `temporal-polyfill` once so server/web/mobile share one API.
4
+ // Temporal is native in Bun >= 1.4 and current Chromium/Firefox, but missing in
5
+ // Safari, iOS and Hermes. `@cosmicdrift/kumiko-types/temporal` resolves one
6
+ // implementation (native first, polyfill otherwise) so framework, consumer and
7
+ // ambient-global code all see the same classes.
7
8
  //
8
9
  // Idempotent: if `globalThis.Temporal` already exists the call is a no-op.
9
- // The cache is the live global check — not a sticky module flag — so tests
10
- // that delete the ambient global (fw#1550) still re-install on the next call.
11
- // Re-install uses the value export + Object.assign (not `temporal-polyfill/global`
12
- // side-effect import): ESM caches the side-effect module and would not re-run
13
- // after a teardown.
14
- let polyfillPromise = null;
10
+ // The check is the live global — not a sticky module flag — so tests that
11
+ // delete the ambient global (fw#1550) still get it back on the next call.
12
+ // Re-install assigns the already-resolved module value: re-importing
13
+ // `temporal-polyfill` here would put a polyfill instance on the global while
14
+ // the rest of the code uses the native classes (instanceof mismatch).
15
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
15
16
  /**
16
17
  * Ensure `globalThis.Temporal` is available. Idempotent.
17
18
  */
18
19
  export async function ensureTemporalPolyfill() {
19
- if ("Temporal" in globalThis) {
20
- // skip: Temporal already on globalThis (native or prior polyfill)
20
+ // skip: Temporal global already present (native or installed by an earlier call)
21
+ if ("Temporal" in globalThis)
21
22
  return;
22
- }
23
- if (polyfillPromise) {
24
- await polyfillPromise;
25
- if ("Temporal" in globalThis) {
26
- // skip: Concurrent boot — peer call finished install
27
- return;
28
- }
29
- // Peer resolved but global still missing (torn down mid-flight).
30
- polyfillPromise = null;
31
- }
32
- polyfillPromise = (async () => {
33
- if ("Temporal" in globalThis) {
34
- // skip: raced native/peer install
35
- return;
36
- }
37
- // Value export — assign ourselves so teardown + re-call still works
38
- // (side-effect `temporal-polyfill/global` is ESM-cached and silent on re-import).
39
- const { Temporal } = await import("temporal-polyfill");
40
- Object.assign(globalThis, { Temporal });
41
- })();
42
- await polyfillPromise;
23
+ Object.assign(globalThis, { Temporal });
43
24
  }
44
25
  /**
45
26
  * Type-safe access to globalThis.Temporal. Throws if the polyfill has not
@@ -2,7 +2,7 @@ import type { TzContext, TzContextOptions } from "@cosmicdrift/kumiko-types/tz-c
2
2
  export type { LocatedTimestampJson, TzContext, TzContextOptions, } from "@cosmicdrift/kumiko-types/tz-context";
3
3
  /**
4
4
  * Factory: creates a TzContext for the current request.
5
- * Uses temporal-polyfill's module export (not globalThis.Temporal) so
5
+ * Uses the kumiko-types Temporal module export (not globalThis.Temporal) so
6
6
  * buildHandlerContext stays ambient-free (fw#1525/#1550).
7
7
  */
8
8
  export declare function createTzContext(options?: TzContextOptions): TzContext;
@@ -17,33 +17,30 @@
17
17
  //
18
18
  // The pure type contracts (TzContext, TzContextOptions, LocatedTimestampJson)
19
19
  // live in @cosmicdrift/kumiko-types/tz-context — only the factories are here.
20
- import { Temporal as TemporalPolyfill } from "temporal-polyfill";
20
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
21
21
  import { ensureTemporalPolyfill } from "./polyfill.js";
22
22
  /**
23
23
  * Factory: creates a TzContext for the current request.
24
- * Uses temporal-polyfill's module export (not globalThis.Temporal) so
24
+ * Uses the kumiko-types Temporal module export (not globalThis.Temporal) so
25
25
  * buildHandlerContext stays ambient-free (fw#1525/#1550).
26
26
  */
27
27
  export function createTzContext(options = {}) {
28
28
  const tenant = options.tenant ?? "UTC";
29
29
  const user = options.user ?? tenant;
30
30
  const geoTz = options.geoTz;
31
- // @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal values at
32
- // runtime; TzContext is typed against ambient Temporal from temporal-spec.
33
- const T = TemporalPolyfill;
34
31
  return {
35
32
  tenant,
36
33
  user,
37
- now: () => T.Now.instant(), // @wrapper-known semantic-alias
38
- nowIn: (tz) => T.Now.zonedDateTimeISO(tz), // @wrapper-known semantic-alias
39
- today: (tz) => T.Now.plainDateISO(tz), // @wrapper-known semantic-alias
34
+ now: () => Temporal.Now.instant(), // @wrapper-known semantic-alias
35
+ nowIn: (tz) => Temporal.Now.zonedDateTimeISO(tz), // @wrapper-known semantic-alias
36
+ today: (tz) => Temporal.Now.plainDateISO(tz), // @wrapper-known semantic-alias
40
37
  todayRange: (tz) => {
41
- const today = T.Now.plainDateISO(tz);
38
+ const today = Temporal.Now.plainDateISO(tz);
42
39
  const startZdt = today.toZonedDateTime({ timeZone: tz });
43
40
  const endZdt = today.add({ days: 1 }).toZonedDateTime({ timeZone: tz });
44
41
  return { start: startZdt.toInstant(), end: endZdt.toInstant() };
45
42
  },
46
- parse: (wallClock, tz) => T.PlainDateTime.from(wallClock).toZonedDateTime(tz),
43
+ parse: (wallClock, tz) => Temporal.PlainDateTime.from(wallClock).toZonedDateTime(tz),
47
44
  toInstant: (zdt) => zdt.toInstant(),
48
45
  toLocatedJson: (zdt) => ({
49
46
  // Wall-clock WITHOUT offset (no "Z", no "+01:00") plus the IANA name.
@@ -52,7 +49,7 @@ export function createTzContext(options = {}) {
52
49
  at: zdt.toPlainDateTime().toString(),
53
50
  tz: zdt.timeZoneId,
54
51
  }),
55
- fromLocatedJson: (obj) => T.PlainDateTime.from(obj.at).toZonedDateTime(obj.tz),
52
+ fromLocatedJson: (obj) => Temporal.PlainDateTime.from(obj.at).toZonedDateTime(obj.tz),
56
53
  fromCoordinates: async (coords) => {
57
54
  if (geoTz === undefined) {
58
55
  throw new Error("ctx.tz.fromCoordinates requires a GeoTzProvider — inject one via the app context (e.g. buildServer({ context: { geoTzProvider } }) or runProdApp({ extraContext: { geoTzProvider } })) or install a provider package.");
@@ -9,7 +9,7 @@ export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefin
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";
11
11
  export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav.js";
12
- export type { ActionFormFooterAction, ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartPanel, DashboardChartTone, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardTimeRangeDefinition, DashboardValueFormat, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionDisplay, RowActionDrawer, RowActionMode, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
12
+ export type { ActionFormFooterAction, ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardChartTone, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelGate, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardTimeRangeDefinition, DashboardValueFormat, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionDisplay, RowActionDrawer, RowActionMode, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
13
13
  export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.js";
14
14
  export type { TargetRef } from "../engine/types/target-ref.js";
15
15
  export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.js";
@@ -6,45 +6,42 @@ import { compareVersions, filterEntriesAfter, parseFeatureChangelog, sortEntries
6
6
  import { ensureTemporalPolyfill } from "./time/index.js";
7
7
  const SEMVER_RE = /^\d+\.\d+\.\d+$/;
8
8
  const CODEMOD_SUBDIR = "scripts/codemod";
9
- function readPackageVersion(cwd, pkgName, repoLocalPath) {
10
- // Walk up from cwd to find node_modules/@cosmicdrift/<pkgName>/package.json
11
- // (handles bun workspace hoisting where packages live in parent node_modules)
12
- let dir = cwd;
13
- for (let i = 0; i < 10; i++) {
14
- const nmPath = join(dir, `node_modules/@cosmicdrift/${pkgName}/package.json`);
15
- if (existsSync(nmPath)) {
16
- try {
17
- const pkg = JSON.parse(readFileSync(nmPath, "utf-8"));
18
- return pkg.version ?? null;
19
- }
20
- catch {
21
- return null;
22
- }
23
- }
24
- const parent = join(dir, "..");
25
- if (parent === dir)
26
- break;
27
- dir = parent;
28
- }
29
- // Fallback: repo-local package root
30
- const repoPath = join(cwd, repoLocalPath);
31
- if (existsSync(repoPath)) {
32
- try {
33
- const pkg = JSON.parse(readFileSync(repoPath, "utf-8"));
34
- return pkg.version ?? null;
35
- }
36
- catch {
9
+ function readPackageJsonVersion(packageJsonPath) {
10
+ if (!existsSync(packageJsonPath))
11
+ return null;
12
+ try {
13
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
14
+ if (typeof pkg !== "object" || pkg === null || !("version" in pkg))
37
15
  return null;
38
- }
16
+ return typeof pkg.version === "string" ? pkg.version : null;
39
17
  }
40
- return null;
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ // bun.lock pins transitive packages too, so a repo that only depends on
23
+ // kumiko-cli/-guards still records the bundled-features version it resolved.
24
+ function readLockfileVersion(root, pkgName) {
25
+ const lockPath = join(root, "bun.lock");
26
+ if (!existsSync(lockPath))
27
+ return null;
28
+ const pinned = new RegExp(`"@cosmicdrift/${pkgName}": \\["@cosmicdrift/${pkgName}@(\\d+\\.\\d+\\.\\d+)"`).exec(readFileSync(lockPath, "utf-8"));
29
+ return pinned?.[1] ?? null;
30
+ }
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 readPackageVersion(root, pkgName, repoLocalPath) {
35
+ return (readPackageJsonVersion(join(root, `node_modules/@cosmicdrift/${pkgName}/package.json`)) ??
36
+ readPackageJsonVersion(join(root, repoLocalPath)) ??
37
+ readLockfileVersion(root, pkgName));
41
38
  }
42
39
  // Changelog entries come from @cosmicdrift/kumiko-bundled-features (see
43
40
  // findFeaturesDirs); comparing against the framework version instead
44
41
  // compares unrelated packages once the two stop being versioned in lockstep.
45
- function readCurrentVersion(cwd) {
46
- return (readPackageVersion(cwd, "kumiko-bundled-features", "packages/bundled-features/package.json") ??
47
- readPackageVersion(cwd, "kumiko-framework", "packages/framework/package.json"));
42
+ function readCurrentVersion(root) {
43
+ return (readPackageVersion(root, "kumiko-bundled-features", "packages/bundled-features/package.json") ??
44
+ readPackageVersion(root, "kumiko-framework", "packages/framework/package.json"));
48
45
  }
49
46
  function readChangelogFile(filePath) {
50
47
  if (!existsSync(filePath))
@@ -677,11 +674,18 @@ export async function runUpgradeCli(argv, cwd, out, options) {
677
674
  for (const changelogFile of packageChangelogFiles) {
678
675
  allEntries.push(...readChangelogFile(changelogFile));
679
676
  }
680
- const pending = sortEntries(filterEntriesAfter(allEntries, currentVersion));
677
+ // Installed version of the target the marker belongs to (cwd unless --dir).
678
+ const targetInstalledVersion = dirFlag ? readCurrentVersion(targetDir) : installedVersion;
679
+ // Changelogs can be found in a parent workspace's newer install. A change
680
+ // the repo has not installed yet is not pending for it.
681
+ const installedEntries = targetInstalledVersion === null
682
+ ? allEntries
683
+ : allEntries.filter((entry) => compareVersions(entry.version, targetInstalledVersion) <= 0);
684
+ const pending = sortEntries(filterEntriesAfter(installedEntries, currentVersion));
681
685
  if (getFlag(args, "apply")) {
682
686
  // Marker must reflect what is actually installed under the target (or
683
687
  // cwd), not the filter baseline above — otherwise CI stays green forever.
684
- const markerVersion = dirFlag ? readCurrentVersion(targetDir) : installedVersion;
688
+ const markerVersion = targetInstalledVersion;
685
689
  const dryRun = getFlag(args, "dry-run");
686
690
  if (markerVersion === null && !dryRun) {
687
691
  out.err("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.349.0",
3
+ "version": "0.351.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>",
@@ -206,8 +206,8 @@
206
206
  "./package.json": "./package.json"
207
207
  },
208
208
  "dependencies": {
209
- "@cosmicdrift/kumiko-http": "0.349.0",
210
- "@cosmicdrift/kumiko-types": "0.349.0",
209
+ "@cosmicdrift/kumiko-http": "0.351.0",
210
+ "@cosmicdrift/kumiko-types": "0.351.0",
211
211
  "bullmq": "^5.76.7",
212
212
  "bun-types": "^1.3.13",
213
213
  "hono": "^4.13.1",
@@ -223,7 +223,7 @@
223
223
  "zod": "^4.4.3"
224
224
  },
225
225
  "devDependencies": {
226
- "@cosmicdrift/kumiko-dispatcher-live": "0.349.0",
226
+ "@cosmicdrift/kumiko-dispatcher-live": "0.351.0",
227
227
  "bun-types": "^1.3.13",
228
228
  "pino-pretty": "^13.1.3"
229
229
  },