@cosmicdrift/kumiko-framework 0.348.0 → 0.349.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.
@@ -1746,6 +1746,33 @@ function validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelI
1746
1746
  if (stat.valueField.length === 0) {
1747
1747
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty valueField.`);
1748
1748
  }
1749
+ validateDashboardValueFormat(featureName, screenId, stat.id, stat.valueFormat);
1750
+ }
1751
+ }
1752
+ function validateDashboardChartDisplay(featureName, screenId, panel) {
1753
+ validateDashboardValueFormat(featureName, screenId, panel.id, panel.valueFormat);
1754
+ const where = `[Feature ${featureName}] Screen "${screenId}" (dashboard) chart-panel "${panel.id}"`;
1755
+ if (panel.scrollable !== undefined && typeof panel.scrollable !== "boolean") {
1756
+ throw new Error(`${where} has a non-boolean scrollable.`);
1757
+ }
1758
+ if (panel.scrollable === true && panel.chart !== "stacked-area") {
1759
+ throw new Error(`${where} sets scrollable on chart "${panel.chart}" — only "stacked-area" supports it.`);
1760
+ }
1761
+ }
1762
+ const CURRENCY_CODE_RE = /^[A-Z]{3}$/;
1763
+ const MAX_VALUE_FORMAT_FRACTION_DIGITS = 4;
1764
+ function validateDashboardValueFormat(featureName, screenId, panelId, valueFormat) {
1765
+ // skip: valueFormat is optional, nothing to validate without it
1766
+ if (valueFormat === undefined)
1767
+ return;
1768
+ const where = `[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panelId}" valueFormat`;
1769
+ if (!CURRENCY_CODE_RE.test(valueFormat.currency)) {
1770
+ throw new Error(`${where}.currency "${String(valueFormat.currency)}" must be a 3-letter uppercase ISO currency code.`);
1771
+ }
1772
+ const digits = valueFormat.fractionDigits;
1773
+ if (digits !== undefined &&
1774
+ (!Number.isInteger(digits) || digits < 0 || digits > MAX_VALUE_FORMAT_FRACTION_DIGITS)) {
1775
+ throw new Error(`${where}.fractionDigits "${String(digits)}" must be an integer from 0 to ${MAX_VALUE_FORMAT_FRACTION_DIGITS}.`);
1749
1776
  }
1750
1777
  }
1751
1778
  export function validateDashboardCustomPanel(featureName, screenId, panel) {
@@ -1764,9 +1791,14 @@ function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
1764
1791
  panel.span !== "full") {
1765
1792
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has span "${String(panel.span)}" — expected "half" or "full".`);
1766
1793
  }
1767
- if (panel.kind === "stat" && panel.valueField.length === 0) {
1768
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-panel "${panel.id}" has empty valueField.`);
1794
+ if (panel.kind === "stat") {
1795
+ if (panel.valueField.length === 0) {
1796
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-panel "${panel.id}" has empty valueField.`);
1797
+ }
1798
+ validateDashboardValueFormat(featureName, screenId, panel.id, panel.valueFormat);
1769
1799
  }
1800
+ if (panel.kind === "chart")
1801
+ validateDashboardChartDisplay(featureName, screenId, panel);
1770
1802
  if (panel.kind === "list") {
1771
1803
  if (panel.columns.length === 0) {
1772
1804
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) list-panel "${panel.id}" has an empty columns list.`);
@@ -74,7 +74,7 @@ export { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
74
74
  export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
75
75
  export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
76
76
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
77
- export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
77
+ export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
78
78
  export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
79
79
  export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
80
80
  export { runValidation } from "./validation.js";
@@ -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, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, 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, 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";
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";
@@ -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, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardTimeRangeDefinition, 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, 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";
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";
@@ -7,6 +7,18 @@ export declare function findPackageChangelogFiles(cwd: string): string[];
7
7
  export declare function findFeaturesDirs(cwd: string): string[];
8
8
  export declare function findCodemodScriptsRoot(repoRoot: string): string | null;
9
9
  export declare function resolveCodemodScript(repoRoot: string, codemodField: string | undefined): string | null;
10
+ export type UpgradeMarkerManual = {
11
+ readonly id: string;
12
+ readonly version: string;
13
+ readonly title: string;
14
+ };
15
+ export type ManualResolution = "migrated" | "not-applicable";
16
+ export type UpgradeMarkerResolvedManual = UpgradeMarkerManual & {
17
+ readonly resolution: ManualResolution;
18
+ readonly reason: string;
19
+ readonly resolvedAt: string;
20
+ };
21
+ export declare function manualEntryId(version: string, title: string): string;
10
22
  export declare function runUpgradeCli(argv: readonly string[], cwd: string, out: UpgradeCliOut, options?: {
11
23
  readonly repoRoot?: string;
12
24
  }): Promise<number>;
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from "node:fs";
2
3
  import { isAbsolute, join, relative, resolve } from "node:path";
3
4
  import { getFlag, getStringFlag, parseArgs } from "./arg-parser.js";
@@ -259,71 +260,153 @@ async function runCodemodScript(scriptPath, targetDir, repoRoot, dryRun) {
259
260
  function hasCodemod(e) {
260
261
  return typeof e.codemod === "string" && e.codemod.length > 0;
261
262
  }
263
+ const MANUAL_ID_HASH_LENGTH = 8;
264
+ const MAX_RESOLVE_REASON_LENGTH = 500;
265
+ // The hash tells apart entries that share a version (studio has several at 0.341.0).
266
+ export function manualEntryId(version, title) {
267
+ const hash = createHash("sha256").update(title).digest("hex").slice(0, MANUAL_ID_HASH_LENGTH);
268
+ return `${version}:${hash}`;
269
+ }
270
+ function toMarkerManual(entry) {
271
+ return {
272
+ id: manualEntryId(entry.version, entry.title),
273
+ version: entry.version,
274
+ title: entry.title,
275
+ };
276
+ }
277
+ function dedupeManualById(entries) {
278
+ const seen = new Set();
279
+ return entries.filter((entry) => {
280
+ if (seen.has(entry.id))
281
+ return false;
282
+ seen.add(entry.id);
283
+ return true;
284
+ });
285
+ }
286
+ // Open manual items survive every marker write until --resolve moves them.
287
+ function carryOverManual(previous, newManual) {
288
+ const resolvedIds = new Set(previous.resolvedManual.map((entry) => entry.id));
289
+ const pendingManual = dedupeManualById([...previous.pendingManual, ...newManual]).filter((entry) => !resolvedIds.has(entry.id));
290
+ return {
291
+ ...(pendingManual.length > 0 && { pendingManual }),
292
+ ...(previous.resolvedManual.length > 0 && { resolvedManual: previous.resolvedManual }),
293
+ };
294
+ }
262
295
  function writeUpgradeMarker(targetDir, marker) {
263
296
  const dir = join(targetDir, ".kumiko");
264
297
  mkdirSync(dir, { recursive: true });
265
298
  writeFileSync(join(dir, "upgrade-state.json"), `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
266
299
  }
300
+ const NO_CARRY = { pendingManual: [], resolvedManual: [] };
301
+ function isRecord(value) {
302
+ return typeof value === "object" && value !== null && !Array.isArray(value);
303
+ }
304
+ function parseManualEntry(value) {
305
+ if (!isRecord(value))
306
+ return null;
307
+ const { id, version, title } = value;
308
+ if (typeof version !== "string" || typeof title !== "string")
309
+ return null;
310
+ return {
311
+ id: typeof id === "string" && id.length > 0 ? id : manualEntryId(version, title),
312
+ version,
313
+ title,
314
+ };
315
+ }
316
+ function parseResolvedEntry(value) {
317
+ const base = parseManualEntry(value);
318
+ if (base === null || !isRecord(value))
319
+ return null;
320
+ const { resolution, reason, resolvedAt } = value;
321
+ if (resolution !== "migrated" && resolution !== "not-applicable")
322
+ return null;
323
+ if (typeof reason !== "string" || typeof resolvedAt !== "string")
324
+ return null;
325
+ return { ...base, resolution, reason, resolvedAt };
326
+ }
327
+ function parseList(value, parseEntry) {
328
+ if (value === undefined)
329
+ return [];
330
+ if (!Array.isArray(value))
331
+ return null;
332
+ const parsed = value.map(parseEntry);
333
+ return parsed.every((entry) => entry !== null) ? parsed : null;
334
+ }
335
+ function isMarkerCodemod(value) {
336
+ return (isRecord(value) &&
337
+ typeof value["version"] === "string" &&
338
+ typeof value["codemod"] === "string" &&
339
+ typeof value["title"] === "string");
340
+ }
341
+ function parseStoredMarker(raw) {
342
+ if (!isRecord(raw))
343
+ return null;
344
+ const { version, appliedAt, codemods } = raw;
345
+ if (typeof version !== "string" || !SEMVER_RE.test(version))
346
+ return null;
347
+ const pendingManual = parseList(raw["pendingManual"], parseManualEntry);
348
+ const resolvedManual = parseList(raw["resolvedManual"], parseResolvedEntry);
349
+ if (pendingManual === null || resolvedManual === null)
350
+ return null;
351
+ return {
352
+ version,
353
+ appliedAt: typeof appliedAt === "string" ? appliedAt : Temporal.Now.instant().toString(),
354
+ codemods: Array.isArray(codemods) ? codemods.filter(isMarkerCodemod) : [],
355
+ pendingManual: dedupeManualById(pendingManual),
356
+ resolvedManual: dedupeManualById(resolvedManual),
357
+ };
358
+ }
267
359
  // A missing marker is a bootstrap app that never ran `--apply` (fw#2299); an
268
360
  // existing but unreadable one must not silently fall back to the installed
269
361
  // version, or a bare `--apply` would skip every codemod in between.
270
- function readMarkerVersion(targetDir) {
362
+ function readMarker(targetDir) {
271
363
  const markerPath = join(targetDir, ".kumiko", "upgrade-state.json");
272
364
  if (!existsSync(markerPath))
273
365
  return { kind: "missing" };
274
366
  try {
275
- const parsed = JSON.parse(readFileSync(markerPath, "utf-8"));
276
- return typeof parsed.version === "string" && SEMVER_RE.test(parsed.version)
277
- ? { kind: "ok", version: parsed.version }
278
- : { kind: "invalid" };
367
+ const marker = parseStoredMarker(JSON.parse(readFileSync(markerPath, "utf-8")));
368
+ return marker === null ? { kind: "invalid" } : { kind: "ok", version: marker.version, marker };
279
369
  }
280
370
  catch {
281
371
  return { kind: "invalid" };
282
372
  }
283
373
  }
284
- /** Highest pending version strictly below the earliest open manual breaking
285
- * entry (no codemod). Falls back to highest non-breaking pending when every
286
- * pending version is at/after that manual. If only manuals remain, returns
287
- * `fallbackInstalled` so the marker stamp moves without claiming manuals done. */
288
- function markerVersionForPending(pending, manualEntries, fallbackInstalled) {
289
- const earliestManual = [...manualEntries].sort((a, b) => compareVersions(a.version, b.version))[0];
290
- const eligible = pending.filter((e) => earliestManual === undefined || compareVersions(e.version, earliestManual.version) < 0);
291
- const headEligible = eligible[0];
292
- if (headEligible !== undefined) {
293
- return eligible.reduce((max, e) => (compareVersions(e.version, max) > 0 ? e.version : max), headEligible.version);
294
- }
295
- const nonBreaking = pending.filter((e) => e.type !== "breaking");
296
- const headNonBreaking = nonBreaking[0];
297
- if (headNonBreaking !== undefined) {
298
- return nonBreaking.reduce((max, e) => (compareVersions(e.version, max) > 0 ? e.version : max), headNonBreaking.version);
299
- }
300
- return fallbackInstalled;
374
+ function carryOf(read) {
375
+ return read.kind === "ok" ? read.marker : NO_CARRY;
376
+ }
377
+ // The marker file is editable repo content; a tampered title must not reach
378
+ // the terminal as an escape sequence.
379
+ function printable(text) {
380
+ return text.replace(/\p{Cc}/gu, "");
301
381
  }
302
- function logManualMarkerOutcome(out, manualEntries, markerVersion) {
303
- if (manualEntries.length > 0) {
304
- const markerHeldBack = manualEntries.some((e) => compareVersions(e.version, markerVersion) > 0);
305
- out.log(markerHeldBack
306
- ? ` ⚠ Marker stays at ${markerVersion}, so the upgrade guard keeps listing the manual change(s) above. Migrate them by hand, then run --apply again to acknowledge them.`
307
- : ` ⚠ Marker moved past the manual change(s) above to ${markerVersion}; the upgrade guard no longer lists them, so make sure they are migrated.`);
382
+ function logOpenManual(out, open) {
383
+ // skip: no open manual migrations, nothing to report
384
+ if (open.length === 0)
385
+ return;
386
+ out.log(` ⚠ ${open.length} manual migration(s) still open:`);
387
+ for (const entry of open) {
388
+ out.log(` ${printable(entry.id)} · ${printable(entry.version)} · ${printable(entry.title)}`);
308
389
  }
390
+ out.log(' Migrate them by hand, then mark them done: kumiko-upgrade --resolve <id|version> --reason "<what you did>" [--not-applicable]');
309
391
  }
310
392
  // Records the codemods that already wrote files before a later one failed, so
311
393
  // a re-run resumes after them instead of replaying non-idempotent codemods.
312
- function writePartialMarker(out, targetDir, pending, manualEntries, failedVersion, ran) {
394
+ function writePartialMarker(out, targetDir, pending, manualEntries, failedVersion, ran, previous) {
313
395
  // skip: no codemod wrote files before the failure, nothing to resume after
314
396
  if (ran.length === 0)
315
397
  return;
316
- const doneBeforeFailure = pending.filter((e) => compareVersions(e.version, failedVersion) < 0);
317
- const version = markerVersionForPending(doneBeforeFailure, manualEntries, "");
398
+ const versionsBeforeFailure = pending
399
+ .map((e) => e.version)
400
+ .filter((version) => compareVersions(version, failedVersion) < 0);
401
+ const version = versionsBeforeFailure.reduce((max, candidate) => (max === null || compareVersions(candidate, max) > 0 ? candidate : max), null);
318
402
  // skip: no pending entry completed before the failure, so no marker version to record
319
- if (version === "")
403
+ if (version === null)
320
404
  return;
321
- const pendingManual = manualEntries.map((e) => ({ version: e.version, title: e.title }));
322
405
  writeUpgradeMarker(targetDir, {
323
406
  version,
324
407
  appliedAt: Temporal.Now.instant().toString(),
325
408
  codemods: ran,
326
- ...(pendingManual.length > 0 && { pendingManual }),
409
+ ...carryOverManual(previous, manualEntries.map(toMarkerManual)),
327
410
  });
328
411
  out.err(` ⚠ ${ran.length} codemod(s) already applied; wrote marker at ${version} so a re-run resumes at the failed one.`);
329
412
  }
@@ -338,16 +421,19 @@ async function applyCodemods(out, pending, repoRoot, targetDir, dryRun,
338
421
  // Installed version for the zero-pending bootstrap marker — never a
339
422
  // `--from` filter override (that would permanently skip real pending
340
423
  // codemods once the CI guard compares against the fake marker).
341
- markerVersion) {
424
+ markerVersion, previous) {
342
425
  if (pending.length === 0) {
343
426
  out.log(" ✓ Nothing new since your version.");
344
427
  if (!dryRun) {
428
+ const carried = carryOverManual(previous, []);
345
429
  writeUpgradeMarker(targetDir, {
346
430
  version: markerVersion,
347
431
  appliedAt: Temporal.Now.instant().toString(),
348
432
  codemods: [],
433
+ ...carried,
349
434
  });
350
435
  out.log(` ✓ Applied 0 codemod(s). Wrote ${join(targetDir, ".kumiko/upgrade-state.json")}`);
436
+ logOpenManual(out, carried.pendingManual ?? []);
351
437
  }
352
438
  return 0;
353
439
  }
@@ -364,16 +450,15 @@ markerVersion) {
364
450
  ? " No automatable codemods among the pending breaking changes."
365
451
  : " ✓ No breaking changes pending.");
366
452
  if (!dryRun) {
367
- const markerVer = markerVersionForPending(pending, manualEntries, markerVersion);
368
- const pendingManual = manualEntries.map((e) => ({ version: e.version, title: e.title }));
453
+ const carried = carryOverManual(previous, manualEntries.map(toMarkerManual));
369
454
  writeUpgradeMarker(targetDir, {
370
- version: markerVer,
455
+ version: markerVersion,
371
456
  appliedAt: Temporal.Now.instant().toString(),
372
457
  codemods: [],
373
- ...(pendingManual.length > 0 && { pendingManual }),
458
+ ...carried,
374
459
  });
375
460
  out.log(` ✓ Applied 0 codemod(s). Wrote ${join(targetDir, ".kumiko/upgrade-state.json")}`);
376
- logManualMarkerOutcome(out, manualEntries, markerVer);
461
+ logOpenManual(out, carried.pendingManual ?? []);
377
462
  }
378
463
  return 0;
379
464
  }
@@ -388,7 +473,7 @@ markerVersion) {
388
473
  if (!scriptPath) {
389
474
  out.err(` ✗ ${e.version} · ${e.title} — invalid codemod path "${e.codemod}"`);
390
475
  if (!dryRun)
391
- writePartialMarker(out, targetDir, pending, manualEntries, e.version, ran);
476
+ writePartialMarker(out, targetDir, pending, manualEntries, e.version, ran, previous);
392
477
  return 1;
393
478
  }
394
479
  out.log(` → ${e.version} · running ${e.codemod}${dryRun ? " (dry-run)" : ""}`);
@@ -398,7 +483,7 @@ markerVersion) {
398
483
  if (!result.ok) {
399
484
  out.err(` ✗ ${e.version} · ${e.codemod} failed`);
400
485
  if (!dryRun)
401
- writePartialMarker(out, targetDir, pending, manualEntries, e.version, ran);
486
+ writePartialMarker(out, targetDir, pending, manualEntries, e.version, ran, previous);
402
487
  return 1;
403
488
  }
404
489
  ran.push({ version: e.version, codemod: e.codemod, title: e.title });
@@ -407,28 +492,114 @@ markerVersion) {
407
492
  out.log(` ✓ Dry-run: ${ran.length} codemod(s) would run. Nothing written.`);
408
493
  return 0;
409
494
  }
410
- const latestVersion = markerVersionForPending(pending, manualEntries, markerVersion);
411
- const pendingManual = manualEntries.map((e) => ({ version: e.version, title: e.title }));
495
+ const carried = carryOverManual(previous, manualEntries.map(toMarkerManual));
412
496
  writeUpgradeMarker(targetDir, {
413
- version: latestVersion,
497
+ version: markerVersion,
414
498
  appliedAt: Temporal.Now.instant().toString(),
415
499
  codemods: ran,
416
- ...(pendingManual.length > 0 && { pendingManual }),
500
+ ...carried,
417
501
  });
418
502
  out.log(` ✓ Applied ${ran.length} codemod(s). Wrote ${join(targetDir, ".kumiko/upgrade-state.json")}`);
419
- if (pendingManual.length > 0) {
420
- out.log(` ⚠ ${pendingManual.length} breaking change(s) still need manual migration.`);
503
+ logOpenManual(out, carried.pendingManual ?? []);
504
+ return 0;
505
+ }
506
+ function parseResolveInput(args) {
507
+ if (getFlag(args, "apply"))
508
+ return { error: "--resolve cannot be combined with --apply." };
509
+ const refs = (getStringFlag(args, "resolve") ?? "")
510
+ .split(",")
511
+ .map((ref) => ref.trim())
512
+ .filter((ref) => ref.length > 0);
513
+ if (refs.length === 0)
514
+ return { error: "--resolve needs at least one id or version." };
515
+ const reason = getStringFlag(args, "reason")?.trim() ?? "";
516
+ if (reason.length === 0)
517
+ return { error: '--resolve requires --reason "<text>".' };
518
+ if (reason.length > MAX_RESOLVE_REASON_LENGTH) {
519
+ return { error: `--reason is too long (max ${MAX_RESOLVE_REASON_LENGTH} characters).` };
520
+ }
521
+ return { refs, reason };
522
+ }
523
+ function matchManualRefs(open, refs) {
524
+ const resolvedIds = new Set();
525
+ const errors = [];
526
+ for (const ref of refs) {
527
+ const byId = open.find((entry) => entry.id === ref);
528
+ const byVersion = open.filter((entry) => entry.version === ref);
529
+ if (byId !== undefined) {
530
+ resolvedIds.add(byId.id);
531
+ }
532
+ else if (byVersion.length === 1 && byVersion[0] !== undefined) {
533
+ resolvedIds.add(byVersion[0].id);
534
+ }
535
+ else if (byVersion.length > 1) {
536
+ errors.push(`"${ref}" matches ${byVersion.length} open entries, pass one of: ${byVersion.map((entry) => entry.id).join(", ")}`);
537
+ }
538
+ else {
539
+ errors.push(`"${ref}" matches no open manual migration`);
540
+ }
541
+ }
542
+ return { resolvedIds, errors };
543
+ }
544
+ function resolveManualMigrations(out, args, targetDir) {
545
+ const fail = (...lines) => {
546
+ out.err("");
547
+ for (const line of lines)
548
+ out.err(` ${line}`);
549
+ out.err("");
550
+ return 1;
551
+ };
552
+ const input = parseResolveInput(args);
553
+ if ("error" in input)
554
+ return fail(input.error);
555
+ const read = readMarker(targetDir);
556
+ if (read.kind === "missing") {
557
+ return fail(`No .kumiko/upgrade-state.json under ${targetDir}, nothing to resolve.`);
558
+ }
559
+ if (read.kind === "invalid")
560
+ return fail("invalid .kumiko/upgrade-state.json, fix it first.");
561
+ const open = read.marker.pendingManual;
562
+ const { resolvedIds, errors } = matchManualRefs(open, input.refs);
563
+ if (errors.length > 0)
564
+ return fail(...errors, "Nothing was written.");
565
+ const { reason } = input;
566
+ const notApplicableFlag = args.flags.get("not-applicable");
567
+ const resolution = notApplicableFlag !== undefined && notApplicableFlag !== false ? "not-applicable" : "migrated";
568
+ const resolvedAt = Temporal.Now.instant().toString();
569
+ const newlyResolved = open
570
+ .filter((entry) => resolvedIds.has(entry.id))
571
+ .map((entry) => ({ ...entry, resolution, reason, resolvedAt }));
572
+ const remaining = open.filter((entry) => !resolvedIds.has(entry.id));
573
+ const resolvedManual = dedupeManualById([...read.marker.resolvedManual, ...newlyResolved]);
574
+ writeUpgradeMarker(targetDir, {
575
+ version: read.marker.version,
576
+ appliedAt: read.marker.appliedAt,
577
+ codemods: read.marker.codemods,
578
+ ...(remaining.length > 0 && { pendingManual: remaining }),
579
+ resolvedManual,
580
+ });
581
+ out.log("");
582
+ out.log(` ✓ Marked ${newlyResolved.length} manual migration(s) as ${resolution}:`);
583
+ for (const entry of newlyResolved) {
584
+ out.log(` ${printable(entry.id)} · ${printable(entry.title)}`);
421
585
  }
422
- logManualMarkerOutcome(out, manualEntries, latestVersion);
586
+ if (remaining.length > 0)
587
+ logOpenManual(out, remaining);
588
+ out.log("");
423
589
  return 0;
424
590
  }
425
591
  const UPGRADE_USAGE = `kumiko-upgrade [--from <version>] [--dir <path>] [--json] [--verbose] [--apply [--dry-run]]
592
+ kumiko-upgrade --resolve <id|version>[,<id|version>...] --reason "<text>" [--not-applicable] [--dir <path>]
426
593
 
427
594
  --from <version> filter baseline version (default: last applied marker, or the installed version)
428
595
  --dir <path> target directory to check/apply against (default: cwd)
429
596
  --json machine-readable output
430
597
  --verbose include full migration details for each pending entry
431
598
  --apply run codemods for pending breaking changes and write the upgrade marker
599
+ (one run moves the marker to the installed version; manual migrations stay listed as open)
600
+ --resolve <refs> mark open manual migrations as done; ref = full id or a version with exactly one open entry
601
+ --reason <text> required with --resolve, max 500 characters
602
+ --not-applicable with --resolve, record the entries as not applicable instead of migrated
432
603
  --dry-run with --apply, report what would run without writing anything
433
604
  --help, -h print this usage and exit`;
434
605
  // kumiko-lint-ignore complexity-budget CLI orchestration moved from bin/commands/upgrade.ts — same branching surface, shared by kumiko upgrade + published kumiko-upgrade bin
@@ -454,6 +625,9 @@ export async function runUpgradeCli(argv, cwd, out, options) {
454
625
  out.err("");
455
626
  return 1;
456
627
  }
628
+ if (args.flags.has("resolve")) {
629
+ return resolveManualMigrations(out, args, targetDir);
630
+ }
457
631
  // Always resolve the actually installed version, even when --from is set,
458
632
  // so --json can report both the filter baseline and what's really there.
459
633
  const installedVersion = readCurrentVersion(cwd);
@@ -465,7 +639,8 @@ export async function runUpgradeCli(argv, cwd, out, options) {
465
639
  // when the marker says otherwise — a bare `--apply` would then always
466
640
  // report "Nothing new" and bootstrap the marker onto the installed
467
641
  // version, hiding breaking changes the marker never actually saw.
468
- const marker = fromFlag === undefined ? readMarkerVersion(targetDir) : { kind: "missing" };
642
+ const markerRead = readMarker(targetDir);
643
+ const marker = fromFlag === undefined ? markerRead : { kind: "missing" };
469
644
  if (marker.kind === "invalid") {
470
645
  out.err("");
471
646
  out.err(" invalid .kumiko/upgrade-state.json, fix it or pass --from <version>");
@@ -516,23 +691,26 @@ export async function runUpgradeCli(argv, cwd, out, options) {
516
691
  return 1;
517
692
  }
518
693
  out.log("");
519
- const code = await applyCodemods(out, pending, repoRoot, targetDir, dryRun, markerVersion ?? currentVersion);
694
+ const code = await applyCodemods(out, pending, repoRoot, targetDir, dryRun, markerVersion ?? currentVersion, carryOf(markerRead));
520
695
  out.log("");
521
696
  return code;
522
697
  }
523
698
  if (jsonMode) {
524
- out.log(JSON.stringify({ currentVersion, installedVersion, pending }, null, 2));
699
+ const { pendingManual } = carryOf(markerRead);
700
+ out.log(JSON.stringify({ currentVersion, installedVersion, pending, pendingManual }, null, 2));
525
701
  return 0;
526
702
  }
527
703
  const breaking = pending.filter((e) => e.type === "breaking");
528
704
  const improvements = pending.filter((e) => e.type === "improvement");
529
705
  const fixes = pending.filter((e) => e.type === "fix");
706
+ const openManual = carryOf(markerRead).pendingManual;
530
707
  out.log("");
531
708
  out.log(` Upgrade: ${currentVersion} → latest`);
532
709
  out.log("");
533
710
  if (pending.length === 0) {
534
711
  out.log(" ✓ Nothing new since your version.");
535
712
  out.log("");
713
+ logOpenManual(out, openManual);
536
714
  return 0;
537
715
  }
538
716
  if (breaking.length > 0) {
@@ -576,5 +754,6 @@ export async function runUpgradeCli(argv, cwd, out, options) {
576
754
  out.log(" Run with --verbose for full migration details.");
577
755
  out.log("");
578
756
  }
757
+ logOpenManual(out, openManual);
579
758
  return 0;
580
759
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.348.0",
3
+ "version": "0.349.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.348.0",
210
- "@cosmicdrift/kumiko-types": "0.348.0",
209
+ "@cosmicdrift/kumiko-http": "0.349.0",
210
+ "@cosmicdrift/kumiko-types": "0.349.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.348.0",
226
+ "@cosmicdrift/kumiko-dispatcher-live": "0.349.0",
227
227
  "bun-types": "^1.3.13",
228
228
  "pino-pretty": "^13.1.3"
229
229
  },
package/src/changes.json CHANGED
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.349.0",
4
+ "type": "improvement",
5
+ "title": "kumiko-upgrade advances in one --apply run, tracks open manual migrations with ids and resolves them via --resolve",
6
+ "detail": "kumiko-upgrade: one --apply run advances, manual items are resolved with --resolve\n`--apply` now moves the marker to the installed version in a single run. Breaking changes without a codemod stay in the marker as `pendingManual` with a stable id (`<version>:<8-char title hash>`) and are listed after every run; the upgrade guard no longer fails on them. `--resolve <id|version>[,...] --reason \"<text>\" [--not-applicable]` moves entries to `resolvedManual` (version must match exactly one open entry; unknown refs write nothing). Resolved ids do not come back. The plain report lists open entries, and `--json` carries them as `pendingManual`.",
7
+ "migration": "If you relied on a second --apply to acknowledge manual migrations, run kumiko-upgrade --resolve <id> --reason \"<text>\" instead."
8
+ },
2
9
  {
3
10
  "version": "0.348.0",
4
11
  "type": "improvement",