@cosmicdrift/kumiko-framework 0.373.0 → 0.377.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 (76) hide show
  1. package/AGENTS.md +2 -2
  2. package/agents/examples/apex-landing.md +1 -1
  3. package/agents/examples/apex-surface-auth.md +3 -3
  4. package/agents/examples/compose-stacks.md +15 -8
  5. package/agents/examples/embedded-entity-form.md +1 -1
  6. package/agents/examples/notes-history-basic.md +1 -1
  7. package/agents/examples/tags-basic.md +1 -1
  8. package/agents/examples/user-profile.md +1 -1
  9. package/agents/examples/wizard-form.md +4 -4
  10. package/agents/widgets.md +4 -3
  11. package/dist/api/index.d.ts +1 -1
  12. package/dist/api/index.js +1 -1
  13. package/dist/api/routes.js +2 -2
  14. package/dist/api/server.js +2 -1
  15. package/dist/config/index.d.ts +4 -0
  16. package/dist/db/index.d.ts +2 -2
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/migrate-runner.js +12 -4
  19. package/dist/db/money.js +1 -1
  20. package/dist/db/query.js +14 -14
  21. package/dist/db/replay-migration-sql.d.ts +1 -0
  22. package/dist/db/replay-migration-sql.js +115 -11
  23. package/dist/db/tenant-db.d.ts +7 -0
  24. package/dist/db/tenant-db.js +17 -5
  25. package/dist/engine/boot-validator/projection-list-screens.js +28 -37
  26. package/dist/engine/boot-validator/query-refs.js +26 -8
  27. package/dist/engine/boot-validator/screens.d.ts +1 -1
  28. package/dist/engine/boot-validator/screens.js +334 -42
  29. package/dist/engine/boot-validator/url-prefill-fields.js +3 -0
  30. package/dist/engine/build-app-schema.js +7 -3
  31. package/dist/engine/entity-list-searchability.d.ts +5 -0
  32. package/dist/engine/entity-list-searchability.js +35 -0
  33. package/dist/engine/feature-ui-extensions.js +5 -2
  34. package/dist/engine/index.d.ts +3 -1
  35. package/dist/engine/index.js +2 -0
  36. package/dist/engine/paged-list.d.ts +29 -0
  37. package/dist/engine/paged-list.js +56 -0
  38. package/dist/engine/parse-ref-target.js +1 -1
  39. package/dist/engine/project-app-schema-for-roles.js +37 -6
  40. package/dist/engine/provider-plugin-context.d.ts +3 -0
  41. package/dist/engine/provider-plugin-context.js +28 -0
  42. package/dist/engine/schema-builder.js +7 -0
  43. package/dist/engine/types/index.d.ts +2 -2
  44. package/dist/env/index.js +1 -1
  45. package/dist/errors/zod-bridge.js +1 -1
  46. package/dist/files/file-ref-entity.d.ts +6 -6
  47. package/dist/files/file-ref-table.d.ts +6 -6
  48. package/dist/files/index.d.ts +1 -1
  49. package/dist/files/index.js +1 -1
  50. package/dist/files/provider-resolver.d.ts +17 -17
  51. package/dist/files/provider-resolver.js +1 -1
  52. package/dist/http/egress.d.ts +14 -0
  53. package/dist/http/egress.js +144 -0
  54. package/dist/http/index.d.ts +3 -2
  55. package/dist/http/index.js +2 -1
  56. package/dist/http/policy.d.ts +30 -0
  57. package/dist/http/policy.js +299 -0
  58. package/dist/i18n/required-surface-keys.js +52 -2
  59. package/dist/jobs/job-runner.js +4 -2
  60. package/dist/pipeline/config-accessor-factory.d.ts +3 -0
  61. package/dist/pipeline/config-accessor-factory.js +3 -0
  62. package/dist/pipeline/dispatch-shared.js +7 -5
  63. package/dist/pipeline/index.d.ts +1 -0
  64. package/dist/pipeline/index.js +1 -0
  65. package/dist/ui-types/index.d.ts +1 -1
  66. package/dist/upgrade-changelog.d.ts +25 -0
  67. package/dist/upgrade-changelog.js +48 -0
  68. package/dist/upgrade-cli.js +78 -13
  69. package/package.json +3 -5
  70. package/presets/biome-app.json +8 -0
  71. package/src/changes.json +132 -0
  72. package/src/scripts/codemod/README.md +25 -0
  73. package/src/scripts/codemod/codemod-support.ts +64 -6
  74. package/src/scripts/codemod/package-merge.ts +260 -0
  75. package/src/scripts/codemod/s3-imports.ts +4 -42
  76. package/src/scripts/codemod/testing-injection.ts +135 -0
@@ -15,6 +15,7 @@ import { isKumikoError } from "../errors/kumiko-error.js";
15
15
  import { createTenantFileContext } from "../files/file-handle.js";
16
16
  import { createFallbackLogger } from "../logging/index.js";
17
17
  import { createEscapeHatchProcessDedup, createEscapeHatchReporter, createEscapeHatchReportWindow, createMetricsForFactory, emitJobLastSuccess, emitJobQueueDepth, flushEscapeHatchAudits, getFallbackTracer, registerStandardMetrics, withEscapeHatchAuditScope, } from "../observability/index.js";
18
+ import { configAccessorFactoryOf } from "../pipeline/config-accessor-factory.js";
18
19
  import { createDistributedLock } from "../pipeline/distributed-lock.js";
19
20
  import { RedisKeys } from "../pipeline/redis-keys.js";
20
21
  import { createGatedIdentitySwitch } from "../pipeline/system-identity-switch.js";
@@ -967,8 +968,9 @@ export function createJobRunner(options) {
967
968
  const crossTenantReads = tenantScopedDb && crossTenantReadsReason !== undefined
968
969
  ? createCrossTenantReads(tenantScopedDb, crossTenantReadsReason, reportEscapeHatch)
969
970
  : undefined;
970
- const config = context._configAccessorFactory && tenantScopedDb
971
- ? context._configAccessorFactory({
971
+ const configFactory = configAccessorFactoryOf(context);
972
+ const config = configFactory && tenantScopedDb
973
+ ? configFactory({
972
974
  user: { id: jobSystemUser.id, tenantId },
973
975
  db: tenantScopedDb,
974
976
  secrets: context.secrets,
@@ -0,0 +1,3 @@
1
+ import type { ConfigAccessorFactory } from "@cosmicdrift/kumiko-types/config";
2
+ import type { AppContext } from "@cosmicdrift/kumiko-types/handlers";
3
+ export declare function configAccessorFactoryOf(context: Pick<AppContext, "configAccessorFactory" | "_configAccessorFactory">): ConfigAccessorFactory | undefined;
@@ -0,0 +1,3 @@
1
+ export function configAccessorFactoryOf(context) {
2
+ return context.configAccessorFactory ?? context._configAccessorFactory;
3
+ }
@@ -22,6 +22,7 @@ import { isPlainObject } from "../utils/is-plain-object.js";
22
22
  import { INTERACTIVE_SIGN_IN_POLICY, resolveActiveMembershipFn } from "./active-membership.js";
23
23
  import { appendDomainEventCore } from "./append-event-core.js";
24
24
  import { resolveAuthClaims as runAuthClaimsResolver } from "./auth-claims-resolver.js";
25
+ import { configAccessorFactoryOf } from "./config-accessor-factory.js";
25
26
  import { runBatch, unwrapSingle } from "./dispatch-batch.js";
26
27
  import { executeQuery } from "./dispatch-query.js";
27
28
  import { executeWrite } from "./dispatch-write.js";
@@ -393,8 +394,9 @@ readOnlyForTenant = false) {
393
394
  });
394
395
  }
395
396
  // Mirror notify: only built when the config feature wired its factory.
396
- const config = context._configAccessorFactory && db
397
- ? context._configAccessorFactory({
397
+ const configFactory = configAccessorFactoryOf(context);
398
+ const config = configFactory && db
399
+ ? configFactory({
398
400
  user: { id: user.id, tenantId: user.tenantId },
399
401
  db,
400
402
  secrets: context.secrets,
@@ -404,7 +406,6 @@ readOnlyForTenant = false) {
404
406
  // identity is the target tenant's system user so the caller's own user-scope
405
407
  // values never resolve into another tenant's config; the db is tenant-mode
406
408
  // scoped to the target.
407
- const configFactory = context._configAccessorFactory;
408
409
  const configFor = configFactory && dbSource
409
410
  ? (targetTenantId) => {
410
411
  if (targetTenantId === user.tenantId && config)
@@ -1268,8 +1269,9 @@ function buildAuthClaimsContext(ctx, user) {
1268
1269
  projectionRegistry: ctx.registry,
1269
1270
  },
1270
1271
  });
1271
- const configAccessor = context._configAccessorFactory
1272
- ? context._configAccessorFactory({
1272
+ const authClaimsConfigFactory = configAccessorFactoryOf(context);
1273
+ const configAccessor = authClaimsConfigFactory
1274
+ ? authClaimsConfigFactory({
1273
1275
  user: { id: user.id, tenantId: user.tenantId },
1274
1276
  db,
1275
1277
  secrets: context.secrets,
@@ -41,3 +41,4 @@ export type { ProjectionStatus } from "./projection-state.ts";
41
41
  export { createProjectionStateTable, ProjectionStatuses, projectionStateTable, } from "./projection-state.ts";
42
42
  export { runProjectionsForEvent } from "./projections-runner.ts";
43
43
  export { ACCESS_INVALIDATION_CONSUMER_NAME, createAccessInvalidationEventConsumer, createSearchEventConsumer, createSseBroadcastEventConsumer, SEARCH_CONSUMER_NAME, SSE_BROADCAST_CONSUMER_NAME, } from "./system-hooks.ts";
44
+ export { TENANT_TIMEZONE_CONFIG_KEY } from "./tenant-timezone-config-key.ts";
@@ -23,3 +23,4 @@ export { getAllProjectionProgress, getProjectionState, listProjectionsWithState,
23
23
  export { createProjectionStateTable, ProjectionStatuses, projectionStateTable, } from "./projection-state.js";
24
24
  export { runProjectionsForEvent } from "./projections-runner.js";
25
25
  export { ACCESS_INVALIDATION_CONSUMER_NAME, createAccessInvalidationEventConsumer, createSearchEventConsumer, createSseBroadcastEventConsumer, SEARCH_CONSUMER_NAME, SSE_BROADCAST_CONSUMER_NAME, } from "./system-hooks.js";
26
+ export { TENANT_TIMEZONE_CONFIG_KEY } from "./tenant-timezone-config-key.js";
@@ -9,7 +9,7 @@ export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefin
9
9
  export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.ts";
10
10
  export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.ts";
11
11
  export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav.ts";
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, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EmbeddedListEditSpec, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, EntityWriteExcludedFields, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListEmptyStateSpec, 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.ts";
12
+ export type { ActionFormFooterAction, ActionFormLoad, ActionFormRedirect, ActionFormScreenDefinition, ActionFormSuccessFact, ActionFormSuccessLink, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardChartTone, DashboardCurrencyFormat, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelGate, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardProgressTone, DashboardRecordBinding, DashboardRefresh, DashboardRelativeTimeFormat, DashboardRelativeTimeParam, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextBlock, DashboardTextPanel, DashboardTextParam, DashboardTimeRangeDefinition, DashboardValueFormat, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EmbeddedListEditSpec, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, EntityWriteExcludedFields, FieldAffix, FieldCondition, FieldFillAction, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListEmptyStateSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionClientAction, RowActionDisplay, RowActionDrawer, RowActionMode, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.ts";
13
13
  export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.ts";
14
14
  export type { TargetRef } from "../engine/types/target-ref.ts";
15
15
  export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.ts";
@@ -0,0 +1,25 @@
1
+ export type UpgradeChangelogManual = {
2
+ readonly id: string;
3
+ readonly version: string;
4
+ readonly title: string;
5
+ readonly status: "open" | "migrated" | "not-applicable";
6
+ };
7
+ export type UpgradeChangelogSection = {
8
+ readonly from: string;
9
+ readonly to: string;
10
+ /** YYYY-MM-DD. */
11
+ readonly date: string;
12
+ readonly breaking: readonly {
13
+ readonly version: string;
14
+ readonly title: string;
15
+ }[];
16
+ readonly manual: readonly UpgradeChangelogManual[];
17
+ readonly codemods: readonly {
18
+ readonly version: string;
19
+ readonly codemod: string;
20
+ readonly title: string;
21
+ }[];
22
+ };
23
+ export declare function renderUpgradeSection(section: UpgradeChangelogSection): string;
24
+ /** Puts the section on top; a section for the same target version is replaced, so a re-run stays idempotent. */
25
+ export declare function mergeUpgradeSection(existing: string | undefined, section: UpgradeChangelogSection): string;
@@ -0,0 +1,48 @@
1
+ const CHANGELOG_HEADER = `# Changelog
2
+
3
+ Kumiko upgrades of this app, written by \`kumiko-upgrade --apply\`. Newest first.
4
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+ `;
6
+ const SECTION_START = /^## \[/;
7
+ const PACKAGES_LABEL = "@cosmicdrift/kumiko-*";
8
+ function printableLine(text) {
9
+ return text.replace(/\p{Cc}/gu, " ");
10
+ }
11
+ function bulletList(title, lines) {
12
+ return lines.length === 0 ? [] : ["", `### ${title}`, "", ...lines.map((line) => `- ${line}`)];
13
+ }
14
+ export function renderUpgradeSection(section) {
15
+ return [
16
+ `## [${section.to}] - ${section.date}`,
17
+ ...bulletList("Changed", [`${PACKAGES_LABEL}: ${section.from} → ${section.to}`]),
18
+ ...bulletList("Breaking", section.breaking.map((entry) => `${entry.version} · ${printableLine(entry.title)}`)),
19
+ ...bulletList("Manual migrations", section.manual.map((entry) => `[${entry.status}] ${entry.id} · ${entry.version} · ${printableLine(entry.title)}`)),
20
+ ...bulletList("Codemods applied", section.codemods.map((entry) => `${entry.version} · ${entry.codemod} · ${printableLine(entry.title)}`)),
21
+ "",
22
+ ].join("\n");
23
+ }
24
+ function splitSections(content) {
25
+ const lines = content.split("\n");
26
+ const firstSection = lines.findIndex((line) => SECTION_START.test(line));
27
+ if (firstSection === -1)
28
+ return { header: content.replace(/\n*$/, "\n"), sections: [] };
29
+ const header = lines.slice(0, firstSection).join("\n").replace(/\n*$/, "\n");
30
+ const sections = [];
31
+ let current = [];
32
+ for (const line of lines.slice(firstSection)) {
33
+ if (SECTION_START.test(line) && current.length > 0) {
34
+ sections.push(current.join("\n").replace(/\n*$/, "\n"));
35
+ current = [];
36
+ }
37
+ current.push(line);
38
+ }
39
+ sections.push(current.join("\n").replace(/\n*$/, "\n"));
40
+ return { header, sections };
41
+ }
42
+ /** Puts the section on top; a section for the same target version is replaced, so a re-run stays idempotent. */
43
+ export function mergeUpgradeSection(existing, section) {
44
+ const { header, sections } = splitSections(existing ?? CHANGELOG_HEADER);
45
+ const prefix = `## [${section.to}]`;
46
+ const others = sections.filter((candidate) => !candidate.startsWith(prefix));
47
+ return [header, renderUpgradeSection(section), ...others].join("\n");
48
+ }
@@ -4,6 +4,7 @@ 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
+ import { mergeUpgradeSection } from "./upgrade-changelog.js";
7
8
  const SEMVER_RE = /^\d+\.\d+\.\d+$/;
8
9
  const SEMVER_CORE_RE = /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?$/;
9
10
  const CODEMOD_SUBDIR = "scripts/codemod";
@@ -340,12 +341,24 @@ function carryOverManual(previous, newManual) {
340
341
  ...(previous.resolvedManual.length > 0 && { resolvedManual: previous.resolvedManual }),
341
342
  };
342
343
  }
344
+ // The marker records every codemod ever applied, not just the last bump's, so the
345
+ // CI guard and a resumed run see the whole history.
346
+ function accumulateCodemods(previous, ran) {
347
+ const seen = new Set();
348
+ return [...previous, ...ran].filter((entry) => {
349
+ const key = `${entry.version}\0${entry.codemod}`;
350
+ if (seen.has(key))
351
+ return false;
352
+ seen.add(key);
353
+ return true;
354
+ });
355
+ }
343
356
  function writeUpgradeMarker(targetDir, marker) {
344
357
  const dir = join(targetDir, ".kumiko");
345
358
  mkdirSync(dir, { recursive: true });
346
359
  writeFileSync(join(dir, "upgrade-state.json"), `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
347
360
  }
348
- const NO_CARRY = { pendingManual: [], resolvedManual: [] };
361
+ const NO_CARRY = { codemods: [], pendingManual: [], resolvedManual: [] };
349
362
  function isRecord(value) {
350
363
  return typeof value === "object" && value !== null && !Array.isArray(value);
351
364
  }
@@ -427,16 +440,52 @@ function carryOf(read) {
427
440
  function printable(text) {
428
441
  return text.replace(/\p{Cc}/gu, "");
429
442
  }
430
- function logOpenManual(out, open) {
443
+ function manualDetailsOf(entries) {
444
+ return new Map(entries.map((entry) => [
445
+ manualEntryId(entry.version, entry.title),
446
+ { detail: entry.detail, migration: entry.migration },
447
+ ]));
448
+ }
449
+ function logOpenManual(out, open, details) {
431
450
  // skip: no open manual migrations, nothing to report
432
451
  if (open.length === 0)
433
452
  return;
434
453
  out.log(` ⚠ ${open.length} manual migration(s) still open:`);
435
454
  for (const entry of open) {
436
455
  out.log(` ${printable(entry.id)} · ${printable(entry.version)} · ${printable(entry.title)}`);
456
+ const entryDetails = details?.get(entry.id);
457
+ if (entryDetails?.detail)
458
+ out.log(` ${printable(entryDetails.detail.replace(/\s*\n\s*/g, " "))}`);
459
+ if (entryDetails?.migration)
460
+ out.log(` → Migration: ${printable(entryDetails.migration)}`);
437
461
  }
438
462
  out.log(' Migrate them by hand, then mark them done: kumiko-upgrade --resolve <id|version> --reason "<what you did>" [--not-applicable]');
439
463
  }
464
+ function manualWithStatus(manualEntries, previous) {
465
+ return manualEntries.map((entry) => {
466
+ const id = manualEntryId(entry.version, entry.title);
467
+ const resolved = previous.resolvedManual.find((candidate) => candidate.id === id);
468
+ return {
469
+ id,
470
+ version: entry.version,
471
+ title: entry.title,
472
+ status: resolved === undefined ? "open" : resolved.resolution,
473
+ };
474
+ });
475
+ }
476
+ // Runs only for a real bump: an app that is already current keeps its CHANGELOG.md untouched.
477
+ function writeUpgradeChangelog(targetDir, bump) {
478
+ const path = join(targetDir, "CHANGELOG.md");
479
+ const existing = existsSync(path) ? readFileSync(path, "utf-8") : undefined;
480
+ writeFileSync(path, mergeUpgradeSection(existing, {
481
+ from: bump.from,
482
+ to: bump.to,
483
+ date: Temporal.Now.instant().toString().slice(0, 10),
484
+ breaking: bump.pending.filter((entry) => entry.type === "breaking"),
485
+ manual: manualWithStatus(bump.manualEntries, bump.previous),
486
+ codemods: bump.ran,
487
+ }), "utf-8");
488
+ }
440
489
  // Records the codemods that already wrote files before a later one failed, so
441
490
  // a re-run resumes after them instead of replaying non-idempotent codemods.
442
491
  function writePartialMarker(out, targetDir, pending, manualEntries, failedVersion, ran, previous) {
@@ -453,7 +502,7 @@ function writePartialMarker(out, targetDir, pending, manualEntries, failedVersio
453
502
  writeUpgradeMarker(targetDir, {
454
503
  version,
455
504
  appliedAt: Temporal.Now.instant().toString(),
456
- codemods: ran,
505
+ codemods: accumulateCodemods(previous.codemods, ran),
457
506
  ...carryOverManual(previous, manualEntries.map(toMarkerManual)),
458
507
  });
459
508
  out.err(` ⚠ ${ran.length} codemod(s) already applied; wrote marker at ${version} so a re-run resumes at the failed one.`);
@@ -469,7 +518,7 @@ async function applyCodemods(out, pending, repoRoot, targetDir, dryRun,
469
518
  // Installed version for the zero-pending bootstrap marker — never a
470
519
  // `--from` filter override (that would permanently skip real pending
471
520
  // codemods once the CI guard compares against the fake marker).
472
- markerVersion, previous) {
521
+ markerVersion, previous, fromVersion, manualDetails) {
473
522
  if (pending.length === 0) {
474
523
  out.log(" ✓ Nothing new since your version.");
475
524
  if (!dryRun) {
@@ -477,11 +526,11 @@ markerVersion, previous) {
477
526
  writeUpgradeMarker(targetDir, {
478
527
  version: markerVersion,
479
528
  appliedAt: Temporal.Now.instant().toString(),
480
- codemods: [],
529
+ codemods: previous.codemods,
481
530
  ...carried,
482
531
  });
483
532
  out.log(` ✓ Applied 0 codemod(s). Wrote ${join(targetDir, ".kumiko/upgrade-state.json")}`);
484
- logOpenManual(out, carried.pendingManual ?? []);
533
+ logOpenManual(out, carried.pendingManual ?? [], manualDetails);
485
534
  }
486
535
  return 0;
487
536
  }
@@ -504,11 +553,19 @@ markerVersion, previous) {
504
553
  writeUpgradeMarker(targetDir, {
505
554
  version: markerVersion,
506
555
  appliedAt: Temporal.Now.instant().toString(),
507
- codemods: [],
556
+ codemods: previous.codemods,
508
557
  ...carried,
509
558
  });
559
+ writeUpgradeChangelog(targetDir, {
560
+ from: fromVersion,
561
+ to: markerVersion,
562
+ pending,
563
+ manualEntries,
564
+ ran: [],
565
+ previous,
566
+ });
510
567
  out.log(` ✓ Applied 0 codemod(s). Wrote ${join(targetDir, ".kumiko/upgrade-state.json")}`);
511
- logOpenManual(out, carried.pendingManual ?? []);
568
+ logOpenManual(out, carried.pendingManual ?? [], manualDetails);
512
569
  }
513
570
  return 0;
514
571
  }
@@ -546,11 +603,19 @@ markerVersion, previous) {
546
603
  writeUpgradeMarker(targetDir, {
547
604
  version: markerVersion,
548
605
  appliedAt: Temporal.Now.instant().toString(),
549
- codemods: ran,
606
+ codemods: accumulateCodemods(previous.codemods, ran),
550
607
  ...carried,
551
608
  });
609
+ writeUpgradeChangelog(targetDir, {
610
+ from: fromVersion,
611
+ to: markerVersion,
612
+ pending,
613
+ manualEntries,
614
+ ran,
615
+ previous,
616
+ });
552
617
  out.log(` ✓ Applied ${ran.length} codemod(s). Wrote ${join(targetDir, ".kumiko/upgrade-state.json")}`);
553
- logOpenManual(out, carried.pendingManual ?? []);
618
+ logOpenManual(out, carried.pendingManual ?? [], manualDetails);
554
619
  return 0;
555
620
  }
556
621
  function parseResolveInput(args) {
@@ -751,7 +816,7 @@ export async function runUpgradeCli(argv, cwd, out, options) {
751
816
  return 1;
752
817
  }
753
818
  out.log("");
754
- const code = await applyCodemods(out, pending, repoRoot, targetDir, dryRun, markerVersion ?? currentVersion, carryOf(markerRead));
819
+ const code = await applyCodemods(out, pending, repoRoot, targetDir, dryRun, markerVersion ?? currentVersion, carryOf(markerRead), currentVersion, verbose ? manualDetailsOf(allEntries) : undefined);
755
820
  out.log("");
756
821
  return code;
757
822
  }
@@ -774,7 +839,7 @@ export async function runUpgradeCli(argv, cwd, out, options) {
774
839
  if (pending.length === 0) {
775
840
  out.log(" ✓ Nothing new since your version.");
776
841
  out.log("");
777
- logOpenManual(out, openManual);
842
+ logOpenManual(out, openManual, verbose ? manualDetailsOf(allEntries) : undefined);
778
843
  return 0;
779
844
  }
780
845
  if (breaking.length > 0) {
@@ -819,6 +884,6 @@ export async function runUpgradeCli(argv, cwd, out, options) {
819
884
  out.log(" Run with --verbose for full migration details.");
820
885
  out.log("");
821
886
  }
822
- logOpenManual(out, openManual);
887
+ logOpenManual(out, openManual, verbose ? manualDetailsOf(allEntries) : undefined);
823
888
  return 0;
824
889
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.373.0",
3
+ "version": "0.377.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>",
@@ -199,8 +199,7 @@
199
199
  "./package.json": "./package.json"
200
200
  },
201
201
  "dependencies": {
202
- "@cosmicdrift/kumiko-http": "0.373.0",
203
- "@cosmicdrift/kumiko-types": "0.373.0",
202
+ "@cosmicdrift/kumiko-types": "0.377.0",
204
203
  "bullmq": "^5.76.7",
205
204
  "bun-types": "^1.3.13",
206
205
  "hono": "^4.13.1",
@@ -216,8 +215,7 @@
216
215
  "zod": "^4.4.3"
217
216
  },
218
217
  "devDependencies": {
219
- "@cosmicdrift/kumiko-dispatcher-live": "0.373.0",
220
- "@cosmicdrift/kumiko-testing": "0.373.0",
218
+ "@cosmicdrift/kumiko-testing": "0.377.0",
221
219
  "bun-types": "^1.3.13",
222
220
  "pino-pretty": "^13.1.3"
223
221
  },
@@ -62,6 +62,14 @@
62
62
  }
63
63
  },
64
64
  "overrides": [
65
+ {
66
+ "includes": ["bin/**", "scripts/**", "**/bin/**", "**/scripts/**"],
67
+ "linter": {
68
+ "rules": {
69
+ "suspicious": { "noConsole": "off" }
70
+ }
71
+ }
72
+ },
65
73
  {
66
74
  "includes": [
67
75
  "**/*.test.ts",
package/src/changes.json CHANGED
@@ -1,4 +1,136 @@
1
1
  [
2
+ {
3
+ "version": "0.377.0",
4
+ "type": "improvement",
5
+ "title": "design decisions log",
6
+ "detail": "docs/reference/decisions.md records lasting design decisions with their reasons. It is documentation only and changes no behavior."
7
+ },
8
+ {
9
+ "version": "0.377.0",
10
+ "type": "improvement",
11
+ "title": "Env hint links to the infra deploy runbook",
12
+ "detail": "The boot env report ends with a link to docs/runbooks/standard-deploy-app.md in the infra repo instead of a path in kumiko-platform that no longer exists."
13
+ },
14
+ {
15
+ "version": "0.377.0",
16
+ "type": "improvement",
17
+ "title": "Replay parser and migration splitter cover dollar quotes, RENAME, CREATE TABLE AS and DO",
18
+ "detail": "splitSqlStatements keeps dollar-quoted bodies whole and throws on an unterminated one. The replay parser follows RENAME TO, RENAME COLUMN, CREATE TABLE AS and DO blocks; tables with columns it cannot derive are skipped in the schema diff. The Bun.SQL wording in db/query.ts errors and logger names now says postgres-js."
19
+ },
20
+ {
21
+ "version": "0.377.0",
22
+ "type": "improvement",
23
+ "title": "providerPluginContextFromHandler and providerPluginContextFromJob",
24
+ "detail": "Build a ProviderPluginContext from a HandlerContext or JobContext (jobs use the system user and have no dbOutsideTransaction)."
25
+ },
26
+ {
27
+ "version": "0.377.0",
28
+ "type": "improvement",
29
+ "title": "hasOutsideTransactionDb and requireOutsideTransactionDb",
30
+ "detail": "Public helpers in the db barrel. hasOutsideTransactionDb(ctx) narrows ctx.dbOutsideTransaction to TenantDb; requireOutsideTransactionDb(ctx) returns it or throws an InternalError."
31
+ },
32
+ {
33
+ "version": "0.377.0",
34
+ "type": "breaking",
35
+ "title": "Provider contexts expose auditUserId instead of _userId",
36
+ "detail": "FileProviderContext, MailTransportContext, InboundMailContext and the context read by requireSecretsContext name the audit identity auditUserId. The old underscore field is gone.",
37
+ "migration": "Rename _userId to auditUserId wherever a provider context is built by hand (tests, custom resolvers, worker wiring). requireSecretsContext still falls back to ctx.user.id when it is unset."
38
+ },
39
+ {
40
+ "version": "0.377.0",
41
+ "type": "improvement",
42
+ "title": "Boot always sets configAccessorFactory and overrides an app value",
43
+ "detail": "addConfigAccessorFactory writes configAccessorFactory and the _configAccessorFactory alias from the effective configResolver, replacing any app-supplied factory. The replay parser also rejects DO blocks without a dollar-quoted body and accepts tags like $do1$."
44
+ },
45
+ {
46
+ "version": "0.377.0",
47
+ "type": "improvement",
48
+ "title": "http moved into framework",
49
+ "detail": "egress(), the host policy helpers and their errors are implemented in @cosmicdrift/kumiko-framework/http. @cosmicdrift/kumiko-http now only re-exports that subpath and is removed after the consumer pass of wave 4.",
50
+ "migration": "Run bun kumiko-upgrade --apply, or bun kumiko codemod package-merge . --write. It rewrites imports of @cosmicdrift/kumiko-http to @cosmicdrift/kumiko-framework/http and replaces the kumiko-http dependency in package.json with kumiko-framework.",
51
+ "codemod": "scripts/codemod/package-merge.ts"
52
+ },
53
+ {
54
+ "version": "0.377.0",
55
+ "type": "improvement",
56
+ "title": "package-merge codemod covers headless and dispatcher-live",
57
+ "detail": "The mapping table of the package-merge codemod gained the headless and dispatcher-live moves into kumiko-renderer.",
58
+ "codemod": "scripts/codemod/package-merge.ts"
59
+ },
60
+ {
61
+ "version": "0.377.0",
62
+ "type": "improvement",
63
+ "title": "package-merge codemod covers renderer-web and CSS files",
64
+ "detail": "The mapping table of the package-merge codemod gained the renderer-web move into kumiko-renderer/web, and the codemod now also rewrites .css files (@import, @plugin, @config). A @source path into the old package is reported for manual follow-up.",
65
+ "codemod": "scripts/codemod/package-merge.ts"
66
+ },
67
+ {
68
+ "version": "0.376.0",
69
+ "type": "improvement",
70
+ "title": "kumiko-upgrade writes CHANGELOG.md and shows manual entries with --verbose",
71
+ "detail": "--apply adds one section per bump to CHANGELOG.md in the app (changed packages, breaking titles, manual migrations with status, codemods applied); a re-run of the same bump replaces its section. --verbose prints detail and migration of the open manual entries. A second --apply keeps the codemods already recorded in the marker. The biome-app preset allows console in bin and scripts. AppConfig takes an optional includeBundled flag that the managed kumiko/schema.ts uses to compose the bundled auth features."
72
+ },
73
+ {
74
+ "version": "0.374.0",
75
+ "type": "improvement",
76
+ "title": "actionForm successLinks and resetOnSuccess",
77
+ "detail": "successLinks keeps the actionForm open after a successful submit and lists links resolved like redirect; boot rejects it together with redirect, with an unknown target or with a record target without idFrom. resetOnSuccess clears typed values after success. Role projection drops links to screens the role cannot reach."
78
+ },
79
+ {
80
+ "version": "0.374.0",
81
+ "type": "improvement",
82
+ "title": "clientAction row actions",
83
+ "detail": "RowAction gains kind \"clientAction\" with a client-registered handler name; boot rejects a name that is not <feature>:<kebab>, and the surface keys include its confirm, confirmLabel and successMessage."
84
+ },
85
+ {
86
+ "version": "0.374.0",
87
+ "type": "improvement",
88
+ "title": "Boot validation for screen-panel fallback and text panels",
89
+ "detail": "DashboardScreenPanel.fallback needs visibleWhen and a registered, embeddable target without its own visibleWhen. DashboardTextPanel (kind \"text\", exported with DashboardTextBlock) rejects empty blocks and empty lists; its text keys are required translation keys. Extension-selector panels qualify a short fallback ref, and role projection drops a fallback the role cannot reach."
90
+ },
91
+ {
92
+ "version": "0.374.0",
93
+ "type": "improvement",
94
+ "title": "Dashboard record binding, header actions, refresh and relative time",
95
+ "detail": "Dashboards take record, actions and refresh; stat panels and text params gain a relativeTime format; the surface keys include kumiko.dashboard.record.missing."
96
+ },
97
+ {
98
+ "version": "0.374.0",
99
+ "type": "breaking",
100
+ "title": "DashboardChartPanel.valueFormat only takes the currency format",
101
+ "detail": "DashboardValueFormat is now a union of DashboardCurrencyFormat and DashboardRelativeTimeFormat. Stat panels take both, chart panels only the currency one, and boot rejects relativeTime on a chart.",
102
+ "migration": "A chart panel that declares valueFormat keeps working with { kind: \"currency\", currency }. Code that types a chart's valueFormat as DashboardValueFormat should use DashboardCurrencyFormat instead; relativeTime on a chart was never meaningful and now fails boot."
103
+ },
104
+ {
105
+ "version": "0.374.0",
106
+ "type": "improvement",
107
+ "title": "Derived list searchability, actionForm load and drawer rowClick",
108
+ "detail": "projectionList searchable is derived false for entity-convention lists without searchable fields (searchable true is rejected at boot), actionForm.load registers a query checked at boot, and a drawer row action may set rowClick, counted with navigate rowClick and rejected on projectionDetail header actions."
109
+ },
110
+ {
111
+ "version": "0.374.0",
112
+ "type": "improvement",
113
+ "title": "Boot checks for field affixes and fillAction",
114
+ "detail": "Boot fails when a fillAction query is not a registered query handler, or when a { field } affix or a fillAction params source is not a field of the same form."
115
+ },
116
+ {
117
+ "version": "0.374.0",
118
+ "type": "improvement",
119
+ "title": "pagedListSchema and pageRows for in-memory list queries",
120
+ "detail": "pagedListSchema(sortableFields) and pageRows(rows, request, { searchFields, defaultSort }) cover search, sort (missing values last, ties by id), offset paging and an optional total for projectionList queries over merged rows. PagedListRequest and PagedListRowValue are exported; nextCursor is always null."
121
+ },
122
+ {
123
+ "version": "0.374.0",
124
+ "type": "improvement",
125
+ "title": "DashboardProgressTone type and unlimited label key",
126
+ "detail": "New exported type DashboardProgressTone for progress-list row tones. The default translations gain kumiko.dashboard.progress.unlimited."
127
+ },
128
+ {
129
+ "version": "0.374.0",
130
+ "type": "improvement",
131
+ "title": "Dashboard chart panels take a y-axis unit",
132
+ "detail": "DashboardChartPanel.unit (i18n key) is shown once on the y axis of a timeseries chart, with or without valueFormat. The boot validator rejects unit on other chart kinds; an untranslated unit key fails the required-surface key check."
133
+ },
2
134
  {
3
135
  "version": "0.373.0",
4
136
  "type": "improvement",
@@ -140,3 +140,28 @@ bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/remove-engine
140
140
 
141
141
  Runs via `bun kumiko-upgrade --apply`. Without a mode flag it writes, then formats the changed files
142
142
  with Biome. `--check` exits 1 while any rewritable or manual site is left.
143
+
144
+ ## package-merge.ts
145
+
146
+ Packages folded into another package stay as re-export shims for one wave. The codemod rewrites
147
+ imports, re-exports, `import("…")` and `mock.module` specifiers (including subpaths) of the old
148
+ package to the new one, and in `package.json` replaces the old dependency with the target package
149
+ (same version range, same block, skipped when the block already lists it). The moves are the rows
150
+ of `PACKAGE_MERGE_MAPPINGS`: `@cosmicdrift/kumiko-http` to `@cosmicdrift/kumiko-framework/http`,
151
+ `@cosmicdrift/kumiko-repo-manifest` to `@cosmicdrift/kumiko-guards/repo-manifest`,
152
+ `@cosmicdrift/kumiko-headless` to `@cosmicdrift/kumiko-renderer/headless` (subpaths keep their name),
153
+ `@cosmicdrift/kumiko-dispatcher-live` to `@cosmicdrift/kumiko-renderer/live`,
154
+ `@cosmicdrift/kumiko-renderer-web` to `@cosmicdrift/kumiko-renderer/web` (so
155
+ `@import "@cosmicdrift/kumiko-renderer-web/styles.css"` becomes
156
+ `@import "@cosmicdrift/kumiko-renderer/web/styles.css"`).
157
+
158
+ `.css` files are covered too: the specifier of `@import`, `@plugin` and `@config` is rewritten like
159
+ an import. A `@source` path into `node_modules/<old package>/…` has no mechanical target and is
160
+ reported as manual.
161
+
162
+ ```bash
163
+ bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/package-merge.ts <dir> [--check | --dry-run | --write] [--no-format]
164
+ ```
165
+
166
+ Runs via `bun kumiko-upgrade --apply`. Without a mode flag it writes. `--check` exits 1 while any
167
+ site is left. Run your package manager's install afterwards to refresh the lockfile.