@bedrock-rbx/core 0.1.0-beta.12 → 0.1.0-beta.14

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.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as ConfigError, S as validateConfig, _ as StateConfig, a as ConfigEnvironmentUniverseId, b as UniverseOverlayWithoutId, c as DisplayNamePrefixConfig, d as GistStateConfig, f as PlaceEntry, g as ResourceEntryByKind, h as ResolvedUniverseEntry, i as Config, l as EnvironmentEntry, m as ResolvedPlaceEntry, n as ConfigInput, o as ConfigRootUniverseId, p as ResolvedConfig, r as defineConfig, s as DeveloperProductEntry, t as ConfigContext, u as GamePassEntry, v as UniverseEntry, w as ConfigValidationIssue, x as isGistStateConfig, y as UniverseOverlayWithId } from "./define-config-87u2jqjM.mjs";
1
+ import { C as UniverseOverlayWithId, D as ConfigError, E as validateConfig, O as ConfigValidationIssue, S as UniverseEntry, T as isGistStateConfig, _ as ResolvedConfig, a as ConfigEnvironmentUniverseId, b as ResourceEntryByKind, c as DisplayNamePrefixConfig, d as GistStateConfig, f as PlaceEntry, g as RedactedPlaceOverride, h as RedactedGamePassOverride, i as Config, l as EnvironmentEntry, m as RedactedEnvironmentOverride, n as ConfigInput, o as ConfigRootUniverseId, p as RedactedDeveloperProductOverride, r as defineConfig, s as DeveloperProductEntry, t as ConfigContext, u as GamePassEntry, v as ResolvedPlaceEntry, w as UniverseOverlayWithoutId, x as StateConfig, y as ResolvedUniverseEntry } from "./define-config-C2cOtDpP.mjs";
2
2
  import { OpenCloudError, OpenCloudError as OpenCloudError$1, Result, Result as Result$1 } from "@bedrock-rbx/ocale";
3
3
  import { Type as Type$1 } from "arktype";
4
4
  import { DeveloperProductsClient } from "@bedrock-rbx/ocale/developer-products";
@@ -1227,8 +1227,29 @@ interface IncompleteUniverseEntryError {
1227
1227
  /** Field that the merged entry lacks. V1 only surfaces `"universeId"`. */
1228
1228
  readonly missingField: "universeId";
1229
1229
  }
1230
+ /**
1231
+ * Failure surfaced when a merged `passes` entry is missing a required
1232
+ * field. The most common path here is an overlay-only pass declared
1233
+ * under `environments.X.passes` with no matching root entry: the overlay
1234
+ * shape is `Partial<GamePassEntry>`, so a typo on the ResourceKey
1235
+ * silently produces an incomplete entry that would otherwise be filled
1236
+ * in by `applyRedaction` (when `redacted: true` is set) and pushed as a
1237
+ * phantom placeholder pass. Surfacing the missing field at the
1238
+ * resolution boundary keeps that case attributable instead of letting
1239
+ * normalize fail later with a less specific error.
1240
+ */
1241
+ interface IncompletePassEntryError {
1242
+ /** ResourceKey of the pass entry that is missing a required field. */
1243
+ readonly key: string;
1244
+ /** Environment whose overlay was projected onto the config. */
1245
+ readonly environment: string;
1246
+ /** Literal discriminator for narrowing. */
1247
+ readonly kind: "incompletePassEntry";
1248
+ /** Field that the merged entry lacks. */
1249
+ readonly missingField: "description" | "icon" | "name";
1250
+ }
1230
1251
  /** Failure modes returned by {@link selectEnvironment}. */
1231
- type SelectEnvironmentError = IncompletePlaceEntryError | IncompleteUniverseEntryError | UnknownEnvironmentError;
1252
+ type SelectEnvironmentError = IncompletePassEntryError | IncompletePlaceEntryError | IncompleteUniverseEntryError | UnknownEnvironmentError;
1232
1253
  /**
1233
1254
  * Project a validated `Config` onto a single environment. Looks up the
1234
1255
  * matching `environments[environment]` entry, deep-merges its resource
@@ -1308,127 +1329,6 @@ type SelectEnvironmentError = IncompletePlaceEntryError | IncompleteUniverseEntr
1308
1329
  */
1309
1330
  declare function selectEnvironment(config: Config, environment: string): Result$1<ResolvedConfig, SelectEnvironmentError>;
1310
1331
  //#endregion
1311
- //#region src/ports/resource-driver.d.ts
1312
- /**
1313
- * Plugin contract for a resource adapter: the interface a third-party author
1314
- * implements to teach Bedrock how to reconcile one {@link ResourceKind} against
1315
- * its upstream API.
1316
- *
1317
- * `ResourceDriver<K>` is a *driven* (secondary) port in hexagonal terms; the
1318
- * name "driver" follows Terraform, Pulumi, and Mantle IaC convention for a
1319
- * component that talks to a specific resource API.
1320
- *
1321
- * @template K - The {@link ResourceKind} discriminator this driver handles.
1322
- *
1323
- * @example
1324
- *
1325
- * ```ts
1326
- * import {
1327
- * asResourceKey,
1328
- * asRobloxAssetId,
1329
- * asSha256Hex,
1330
- * type ResourceDriver,
1331
- * } from "@bedrock-rbx/core";
1332
- *
1333
- * const gamePassDriver: ResourceDriver<"gamePass"> = {
1334
- * async create(desired) {
1335
- * return {
1336
- * data: {
1337
- * ...desired,
1338
- * outputs: {
1339
- * assetId: asRobloxAssetId("9876543210"),
1340
- * iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
1341
- * },
1342
- * },
1343
- * success: true,
1344
- * };
1345
- * },
1346
- * };
1347
- *
1348
- * return gamePassDriver
1349
- * .create({
1350
- * description: "Grants VIP perks.",
1351
- * icon: { "en-us": "assets/vip-icon.png" },
1352
- * iconFileHashes: {
1353
- * "en-us": asSha256Hex(
1354
- * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1355
- * ),
1356
- * },
1357
- * key: asResourceKey("vip-pass"),
1358
- * kind: "gamePass",
1359
- * name: "VIP Pass",
1360
- * price: undefined,
1361
- * })
1362
- * .then((result) => {
1363
- * expect(result.success).toBeTrue();
1364
- * if (result.success) {
1365
- * expect(result.data.outputs.assetId).toBe("9876543210");
1366
- * }
1367
- * });
1368
- * ```
1369
- */
1370
- interface ResourceDriver<K extends ResourceKind> {
1371
- /**
1372
- * Create the resource upstream from its desired state and return the
1373
- * resulting current state (desired fields + Roblox-assigned outputs).
1374
- */
1375
- create(desired: Extract<ResourceDesiredState, {
1376
- kind: K;
1377
- }>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
1378
- /**
1379
- * Reconcile an upstream resource whose managed content has drifted from its
1380
- * desired state. Receives the last-known current state so the driver can
1381
- * compute a minimal patch (or no-op upstream, for file-backed kinds where
1382
- * republishing is unconditional).
1383
- *
1384
- * Optional. Drivers whose upstream API has no update operation omit this
1385
- * method; `applyOps` surfaces an `updateUnsupported` error at dispatch time
1386
- * instead.
1387
- */
1388
- update?(current: ResourceCurrentState<K>, desired: Extract<ResourceDesiredState, {
1389
- kind: K;
1390
- }>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
1391
- }
1392
- /**
1393
- * Polymorphic dispatch table keyed by {@link ResourceKind}, mapping each kind
1394
- * to the {@link ResourceDriver} that handles it. `applyOps` indexes the
1395
- * registry by `op.desired.kind` to reach the matching driver with full type
1396
- * safety: adding a new kind to `ResourceDesiredState` is a compile error until
1397
- * a matching registry entry is supplied.
1398
- *
1399
- * @example
1400
- *
1401
- * ```ts
1402
- * import { OpenCloudError, type DriverRegistry } from "@bedrock-rbx/core";
1403
- *
1404
- * const registry: DriverRegistry = {
1405
- * gamePass: {
1406
- * async create() {
1407
- * return { err: new OpenCloudError("not implemented"), success: false };
1408
- * },
1409
- * },
1410
- * place: {
1411
- * async create() {
1412
- * return { err: new OpenCloudError("not implemented"), success: false };
1413
- * },
1414
- * },
1415
- * universe: {
1416
- * async create() {
1417
- * return { err: new OpenCloudError("not implemented"), success: false };
1418
- * },
1419
- * },
1420
- * developerProduct: {
1421
- * async create() {
1422
- * return { err: new OpenCloudError("not implemented"), success: false };
1423
- * },
1424
- * },
1425
- * };
1426
- *
1427
- * expect(registry.gamePass).toBeObject();
1428
- * ```
1429
- */
1430
- type DriverRegistry = { [K in ResourceKind]: ResourceDriver<K> };
1431
- //#endregion
1432
1332
  //#region src/core/operations.d.ts
1433
1333
  /**
1434
1334
  * Fields shared by every operation variant.
@@ -1540,11 +1440,13 @@ interface CreateOperation extends BaseOperation {
1540
1440
  * name: "VIP Pass",
1541
1441
  * price: 750,
1542
1442
  * },
1443
+ * changedFields: ["description", "price"],
1543
1444
  * key: asResourceKey("vip-pass"),
1544
1445
  * type: "update",
1545
1446
  * };
1546
1447
  *
1547
1448
  * expect(op.type).toBe("update");
1449
+ * expect(op.changedFields).toStrictEqual(["description", "price"]);
1548
1450
  * if (op.desired.kind === "gamePass") {
1549
1451
  * expect(op.desired.price).toBe(750);
1550
1452
  * }
@@ -1554,6 +1456,13 @@ interface CreateOperation extends BaseOperation {
1554
1456
  * ```
1555
1457
  */
1556
1458
  interface UpdateOperation extends BaseOperation {
1459
+ /**
1460
+ * Top-level field names that differ between `current` and `desired`,
1461
+ * populated by `diff` from the kind module's `changedFieldsBetween`.
1462
+ * Plan and apply renderers consume this as the single source of truth
1463
+ * for "what changed" on this op; never empty for an `update` variant.
1464
+ */
1465
+ readonly changedFields: ReadonlyArray<string>;
1557
1466
  /** Last-known live state; the driver computes a patch against `desired`. */
1558
1467
  readonly current: ResourceCurrentState;
1559
1468
  /** Declared desired state to converge toward. */
@@ -1566,9 +1475,9 @@ interface UpdateOperation extends BaseOperation {
1566
1475
  * entry matches its `current` entry exactly. The driver performs no I/O for
1567
1476
  * this variant.
1568
1477
  *
1569
- * Bare by design: the operation carries only `key` and `type` because no
1570
- * payload is needed at apply time. Callers that need the matching desired or
1571
- * current state look it up in the snapshots passed to `diff`.
1478
+ * Carries `key` and `kind` so progress renderers and adapters can describe the
1479
+ * unchanged resource without re-looking it up in the desired or current
1480
+ * snapshots passed to `diff`.
1572
1481
  *
1573
1482
  * @example
1574
1483
  *
@@ -1577,14 +1486,18 @@ interface UpdateOperation extends BaseOperation {
1577
1486
  *
1578
1487
  * const op: NoopOperation = {
1579
1488
  * key: asResourceKey("vip-pass"),
1489
+ * kind: "gamePass",
1580
1490
  * type: "noop",
1581
1491
  * };
1582
1492
  *
1583
1493
  * expect(op.type).toBe("noop");
1494
+ * expect(op.kind).toBe("gamePass");
1584
1495
  * expect(op.key).toBe("vip-pass");
1585
1496
  * ```
1586
1497
  */
1587
1498
  interface NoopOperation extends BaseOperation {
1499
+ /** Resource-kind discriminator copied from the matching desired/current entry. */
1500
+ readonly kind: ResourceKind;
1588
1501
  /** Discriminator tag for the `Operation` union. */
1589
1502
  readonly type: "noop";
1590
1503
  }
@@ -1617,6 +1530,7 @@ interface NoopOperation extends BaseOperation {
1617
1530
  *
1618
1531
  * const op: Operation = {
1619
1532
  * key: asResourceKey("vip-pass"),
1533
+ * kind: "gamePass",
1620
1534
  * type: "noop",
1621
1535
  * };
1622
1536
  *
@@ -1625,15 +1539,145 @@ interface NoopOperation extends BaseOperation {
1625
1539
  */
1626
1540
  type Operation = CreateOperation | NoopOperation | UpdateOperation;
1627
1541
  //#endregion
1542
+ //#region src/ports/resource-driver.d.ts
1543
+ /**
1544
+ * Plugin contract for a resource adapter: the interface a third-party author
1545
+ * implements to teach Bedrock how to reconcile one {@link ResourceKind} against
1546
+ * its upstream API.
1547
+ *
1548
+ * `ResourceDriver<K>` is a *driven* (secondary) port in hexagonal terms; the
1549
+ * name "driver" follows Terraform, Pulumi, and Mantle IaC convention for a
1550
+ * component that talks to a specific resource API.
1551
+ *
1552
+ * @template K - The {@link ResourceKind} discriminator this driver handles.
1553
+ *
1554
+ * @example
1555
+ *
1556
+ * ```ts
1557
+ * import {
1558
+ * asResourceKey,
1559
+ * asRobloxAssetId,
1560
+ * asSha256Hex,
1561
+ * type ResourceDriver,
1562
+ * } from "@bedrock-rbx/core";
1563
+ *
1564
+ * const gamePassDriver: ResourceDriver<"gamePass"> = {
1565
+ * async create(desired) {
1566
+ * return {
1567
+ * data: {
1568
+ * ...desired,
1569
+ * outputs: {
1570
+ * assetId: asRobloxAssetId("9876543210"),
1571
+ * iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
1572
+ * },
1573
+ * },
1574
+ * success: true,
1575
+ * };
1576
+ * },
1577
+ * };
1578
+ *
1579
+ * return gamePassDriver
1580
+ * .create({
1581
+ * description: "Grants VIP perks.",
1582
+ * icon: { "en-us": "assets/vip-icon.png" },
1583
+ * iconFileHashes: {
1584
+ * "en-us": asSha256Hex(
1585
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1586
+ * ),
1587
+ * },
1588
+ * key: asResourceKey("vip-pass"),
1589
+ * kind: "gamePass",
1590
+ * name: "VIP Pass",
1591
+ * price: undefined,
1592
+ * })
1593
+ * .then((result) => {
1594
+ * expect(result.success).toBeTrue();
1595
+ * if (result.success) {
1596
+ * expect(result.data.outputs.assetId).toBe("9876543210");
1597
+ * }
1598
+ * });
1599
+ * ```
1600
+ */
1601
+ interface ResourceDriver<K extends ResourceKind> {
1602
+ /**
1603
+ * Create the resource upstream from its desired state and return the
1604
+ * resulting current state (desired fields + Roblox-assigned outputs).
1605
+ */
1606
+ create(desired: Extract<ResourceDesiredState, {
1607
+ kind: K;
1608
+ }>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
1609
+ /**
1610
+ * Reconcile an upstream resource whose managed content has drifted from its
1611
+ * desired state. Receives the last-known current state so the driver can
1612
+ * compute a minimal patch (or no-op upstream, for file-backed kinds where
1613
+ * republishing is unconditional).
1614
+ *
1615
+ * Optional. Drivers whose upstream API has no update operation omit this
1616
+ * method; `applyOps` surfaces an `updateUnsupported` error at dispatch time
1617
+ * instead.
1618
+ */
1619
+ update?(current: ResourceCurrentState<K>, desired: Extract<ResourceDesiredState, {
1620
+ kind: K;
1621
+ }>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
1622
+ }
1623
+ /**
1624
+ * Polymorphic dispatch table keyed by {@link ResourceKind}, mapping each kind
1625
+ * to the {@link ResourceDriver} that handles it. `applyOps` indexes the
1626
+ * registry by `op.desired.kind` to reach the matching driver with full type
1627
+ * safety: adding a new kind to `ResourceDesiredState` is a compile error until
1628
+ * a matching registry entry is supplied.
1629
+ *
1630
+ * @example
1631
+ *
1632
+ * ```ts
1633
+ * import { OpenCloudError, type DriverRegistry } from "@bedrock-rbx/core";
1634
+ *
1635
+ * const registry: DriverRegistry = {
1636
+ * gamePass: {
1637
+ * async create() {
1638
+ * return { err: new OpenCloudError("not implemented"), success: false };
1639
+ * },
1640
+ * },
1641
+ * place: {
1642
+ * async create() {
1643
+ * return { err: new OpenCloudError("not implemented"), success: false };
1644
+ * },
1645
+ * },
1646
+ * universe: {
1647
+ * async create() {
1648
+ * return { err: new OpenCloudError("not implemented"), success: false };
1649
+ * },
1650
+ * },
1651
+ * developerProduct: {
1652
+ * async create() {
1653
+ * return { err: new OpenCloudError("not implemented"), success: false };
1654
+ * },
1655
+ * },
1656
+ * };
1657
+ *
1658
+ * expect(registry.gamePass).toBeObject();
1659
+ * ```
1660
+ */
1661
+ type DriverRegistry = { [K in ResourceKind]: ResourceDriver<K> };
1662
+ //#endregion
1628
1663
  //#region src/shell/apply-ops.d.ts
1629
1664
  /**
1665
+ * Optional wiring `applyOps` uses to emit per-resource and aggregate progress
1666
+ * events. When omitted, `applyOps` runs silently (backward-compatible with
1667
+ * pre-progress callers).
1668
+ */
1669
+ interface ApplyOpsReporting {
1670
+ /** Environment name stamped on every emitted event. */
1671
+ readonly environment: string;
1672
+ /** Sink the apply pipeline pushes events into. */
1673
+ readonly progress: ProgressPort;
1674
+ }
1675
+ /**
1630
1676
  * Failure surfaced by `applyOps` when an operation cannot be applied.
1631
1677
  * Plain-data discriminated union; narrow on `kind`, do not `instanceof` it.
1632
- *
1633
- * `appliedSoFar` carries the driver outputs from operations that succeeded
1634
- * before the failing one, in dispatched order. Callers persist this so a
1635
- * follow-up reconcile does not duplicate Roblox-side resources that have
1636
- * already been created or updated.
1678
+ * One `ApplyError` describes one failing op; the surrounding
1679
+ * `AggregateApplyError` carries the full batch outcome (every survivor and
1680
+ * every failure).
1637
1681
  *
1638
1682
  * @example
1639
1683
  *
@@ -1645,6 +1689,9 @@ type Operation = CreateOperation | NoopOperation | UpdateOperation;
1645
1689
  * case "driverFailure": {
1646
1690
  * return `driver failed for ${err.key}: ${err.cause.message}`;
1647
1691
  * }
1692
+ * case "unexpectedThrow": {
1693
+ * return `unexpected error for ${err.key}`;
1694
+ * }
1648
1695
  * case "updateUnsupported": {
1649
1696
  * return `update not supported for ${err.key}`;
1650
1697
  * }
@@ -1653,7 +1700,6 @@ type Operation = CreateOperation | NoopOperation | UpdateOperation;
1653
1700
  *
1654
1701
  * const err: ApplyError = {
1655
1702
  * key: asResourceKey("vip-pass"),
1656
- * appliedSoFar: [],
1657
1703
  * kind: "updateUnsupported",
1658
1704
  * };
1659
1705
  *
@@ -1661,126 +1707,290 @@ type Operation = CreateOperation | NoopOperation | UpdateOperation;
1661
1707
  * ```
1662
1708
  */
1663
1709
  type ApplyError = {
1664
- readonly appliedSoFar: ReadonlyArray<ResourceCurrentState>;
1665
1710
  readonly cause: OpenCloudError$1;
1666
1711
  readonly key: ResourceKey;
1667
1712
  readonly kind: "driverFailure";
1668
1713
  } | {
1669
- readonly appliedSoFar: ReadonlyArray<ResourceCurrentState>;
1714
+ readonly cause: unknown;
1715
+ readonly key: ResourceKey;
1716
+ readonly kind: "unexpectedThrow";
1717
+ } | {
1670
1718
  readonly key: ResourceKey;
1671
1719
  readonly kind: "updateUnsupported";
1672
1720
  };
1673
1721
  /**
1674
- * Dispatch each reconciliation operation to the matching resource driver
1675
- * with first-fail semantics: on the first `Err` (driver failure or
1676
- * `updateUnsupported`), the remaining operations are skipped and the error
1677
- * is returned verbatim.
1722
+ * Aggregate outcome returned by `applyOps` when one or more ops fail.
1723
+ * `applied` is the survivor set in Phase 1 then Phase 2 input order.
1724
+ * `failures` is the non-empty list of `ApplyError`s, one per failing op,
1725
+ * grouped the same way.
1726
+ *
1727
+ * @example
1728
+ *
1729
+ * ```ts
1730
+ * import { asResourceKey, type AggregateApplyError } from "@bedrock-rbx/core";
1731
+ *
1732
+ * function summarize(err: AggregateApplyError): string {
1733
+ * return `${err.applied.length} survived, ${err.failures.length} failed`;
1734
+ * }
1735
+ *
1736
+ * const err: AggregateApplyError = {
1737
+ * applied: [],
1738
+ * failures: [{ key: asResourceKey("vip-pass"), kind: "updateUnsupported" }],
1739
+ * };
1740
+ *
1741
+ * expect(summarize(err)).toBe("0 survived, 1 failed");
1742
+ * ```
1743
+ */
1744
+ interface AggregateApplyError {
1745
+ /** Survivors persisted to state, in Phase 1 then Phase 2 input order. */
1746
+ readonly applied: ReadonlyArray<ResourceCurrentState>;
1747
+ /** Per-op failures, at least one, in Phase 1 then Phase 2 input order. */
1748
+ readonly failures: readonly [ApplyError, ...ReadonlyArray<ApplyError>];
1749
+ }
1750
+ /**
1751
+ * Dispatch reconciliation operations to their matching drivers in two phases
1752
+ * with continue-on-failure semantics. Phase 1 runs universe ops sequentially
1753
+ * (singleton per environment; sequencing it before everything else avoids the
1754
+ * `displayName` race against the root `Place`). Phase 2 dispatches every
1755
+ * remaining non-noop op concurrently via `Promise.all`; every op is
1756
+ * attempted regardless of earlier failures.
1678
1757
  *
1679
1758
  * Behaviour:
1680
- * - `create` operations are routed to `registry[op.desired.kind].create`.
1681
- * - `update` operations are routed to `registry[op.desired.kind].update`
1682
- * when the driver exposes it; otherwise they short-circuit to an
1683
- * `updateUnsupported` Err without invoking the driver.
1759
+ * - `create` operations route to `registry[op.desired.kind].create`.
1760
+ * - `update` operations route to `registry[op.desired.kind].update` when the
1761
+ * driver exposes it; otherwise they yield an `updateUnsupported`
1762
+ * `ApplyError` without invoking the driver.
1684
1763
  * - `noop` operations are skipped entirely (no I/O, no dispatch).
1764
+ * - A driver that throws outside its `Result` contract is caught at the
1765
+ * dispatch boundary and translated to an `unexpectedThrow` `ApplyError`
1766
+ * scoped to that op alone; the rest of the batch keeps running.
1767
+ *
1768
+ * On Ok the returned array carries driver outputs for every non-noop op
1769
+ * in phase order: Phase 1 universe entries first, then Phase 2 entries in
1770
+ * their input order. Noops are not represented; callers needing a full
1771
+ * post-apply snapshot merge with the pre-apply current state keyed by
1772
+ * `ResourceKey`.
1773
+ *
1774
+ * On Err the aggregate carries every survivor in `applied` (Phase 1 first,
1775
+ * then Phase 2 input order) and every failure in `failures` with the same
1776
+ * grouping. Neither array reflects completion order.
1777
+ *
1778
+ * @param ops - Reconciliation operations produced by `diff`, applied in
1779
+ * declaration order.
1780
+ * @param registry - Per-kind driver table; dispatch uses `op.desired.kind`
1781
+ * as the index.
1782
+ * @param reporting - Optional progress wiring. When supplied, `applyOps`
1783
+ * emits one `resourceOpStarted` and one terminal event per non-noop op,
1784
+ * one `resourceOpNoop` per noop op, and a final `applySummary` carrying
1785
+ * the per-type counts and the wall-clock apply duration. When omitted,
1786
+ * no events fire.
1787
+ * @returns `Ok(state)` when every op succeeded; otherwise
1788
+ * `Err(AggregateApplyError)` with the survivors and the non-empty
1789
+ * failures tuple.
1790
+ * @example
1791
+ *
1792
+ * ```ts
1793
+ * import { applyOps, type DriverRegistry } from "@bedrock-rbx/core";
1794
+ *
1795
+ * const noopRegistry: DriverRegistry = {
1796
+ * developerProduct: { create: async () => ({ err: new Error("stub") as never, success: false }) },
1797
+ * gamePass: { create: async () => ({ err: new Error("stub") as never, success: false }) },
1798
+ * place: { create: async () => ({ err: new Error("stub") as never, success: false }) },
1799
+ * universe: { create: async () => ({ err: new Error("stub") as never, success: false }) },
1800
+ * };
1801
+ *
1802
+ * return applyOps([], noopRegistry).then((result) => {
1803
+ * expect(result).toStrictEqual({ data: [], success: true });
1804
+ * });
1805
+ * ```
1806
+ */
1807
+ declare function applyOps(ops: ReadonlyArray<Operation>, registry: DriverRegistry, reporting?: ApplyOpsReporting): Promise<Result$1<ReadonlyArray<ResourceCurrentState>, AggregateApplyError>>;
1808
+ //#endregion
1809
+ //#region src/ports/progress-port.d.ts
1810
+ /**
1811
+ * Per-environment outcome event emitted after a deploy completes
1812
+ * successfully. Carries the environment name and the count of resources
1813
+ * present in the persisted state snapshot.
1814
+ */
1815
+ interface DeploySuccessEvent {
1816
+ /** The environment that finished reconciling. */
1817
+ readonly environment: string;
1818
+ /** Discriminator tag. */
1819
+ readonly kind: "deploySuccess";
1820
+ /** Number of resources in the post-deploy state snapshot. */
1821
+ readonly resourceCount: number;
1822
+ }
1823
+ /**
1824
+ * Per-environment outcome event emitted when a deploy fails. Carries the
1825
+ * environment name and the full {@link DeployError} so a renderer can
1826
+ * delegate to the existing diagnostic helpers.
1827
+ */
1828
+ interface DeployFailureEvent {
1829
+ /** The environment whose deploy failed. */
1830
+ readonly environment: string;
1831
+ /** Stage-tagged failure reason returned by the shell `deploy` function. */
1832
+ readonly error: DeployError;
1833
+ /** Discriminator tag. */
1834
+ readonly kind: "deployFailure";
1835
+ }
1836
+ /**
1837
+ * Per-resource event emitted immediately before `applyOps` dispatches a
1838
+ * non-noop op to its driver. Adapters may render a "starting" line or
1839
+ * stay silent; the matching terminal event ({@link ResourceOpSucceededEvent}
1840
+ * or {@link ResourceOpFailedEvent}) fires when the driver settles.
1841
+ */
1842
+ interface ResourceOpStartedEvent {
1843
+ /** User-supplied resource key. */
1844
+ readonly key: ResourceKey;
1845
+ /** Environment whose reconcile is running. */
1846
+ readonly environment: string;
1847
+ /** Discriminator tag. */
1848
+ readonly kind: "resourceOpStarted";
1849
+ /** Operation type being dispatched. Noops never fire this event. */
1850
+ readonly opType: "create" | "update";
1851
+ /** Resource-kind discriminator (`gamePass`, `place`, ...). */
1852
+ readonly resourceKind: ResourceKind;
1853
+ }
1854
+ /**
1855
+ * Terminal event for a successful create op. The `resourceKind` discriminator
1856
+ * narrows `outputs` to the matching `ResourceOutputs<K>` shape so renderers
1857
+ * can read Roblox-assigned IDs without casts.
1858
+ */
1859
+ type ResourceOpSucceededCreateEvent = { [K in ResourceKind]: Readonly<{
1860
+ environment: string;
1861
+ key: ResourceKey;
1862
+ kind: "resourceOpSucceeded";
1863
+ opType: "create";
1864
+ outputs: ResourceOutputs<K>;
1865
+ resourceKind: K;
1866
+ }> }[ResourceKind];
1867
+ /**
1868
+ * Terminal event for a successful update op. Carries the list of top-level
1869
+ * fields the diff flagged as changed so renderers can attribute the update.
1870
+ */
1871
+ interface ResourceOpSucceededUpdateEvent {
1872
+ /** User-supplied resource key. */
1873
+ readonly key: ResourceKey;
1874
+ /** Top-level field names whose values differed between desired and current. */
1875
+ readonly changedFields: ReadonlyArray<string>;
1876
+ /** Environment whose reconcile is running. */
1877
+ readonly environment: string;
1878
+ /** Discriminator tag. */
1879
+ readonly kind: "resourceOpSucceeded";
1880
+ /** Operation type. */
1881
+ readonly opType: "update";
1882
+ /** Resource-kind discriminator. */
1883
+ readonly resourceKind: ResourceKind;
1884
+ }
1885
+ /**
1886
+ * Terminal event for a successful non-noop op. Sub-discriminated by `opType`
1887
+ * so a renderer can extract `outputs` (creates) or `changedFields` (updates)
1888
+ * without losing type narrowing.
1889
+ */
1890
+ type ResourceOpSucceededEvent = ResourceOpSucceededCreateEvent | ResourceOpSucceededUpdateEvent;
1891
+ /**
1892
+ * Per-resource event emitted for each op the diff produced as a noop.
1893
+ * Noops never fire a `started`/terminal pair; this single event stands in
1894
+ * for the entire op so adapters can render a "unchanged" line.
1895
+ */
1896
+ interface ResourceOpNoopEvent {
1897
+ /** User-supplied resource key. */
1898
+ readonly key: ResourceKey;
1899
+ /** Environment whose reconcile is running. */
1900
+ readonly environment: string;
1901
+ /** Discriminator tag. */
1902
+ readonly kind: "resourceOpNoop";
1903
+ /** Resource-kind discriminator. */
1904
+ readonly resourceKind: ResourceKind;
1905
+ }
1906
+ /**
1907
+ * Terminal event for a failed non-noop op. Carries the {@link ApplyError}
1908
+ * so a renderer can delegate to the existing apply-cause diagnostic helper.
1909
+ */
1910
+ interface ResourceOpFailedEvent {
1911
+ /** User-supplied resource key. */
1912
+ readonly key: ResourceKey;
1913
+ /** Environment whose reconcile is running. */
1914
+ readonly environment: string;
1915
+ /** Apply error returned by `dispatchOp`. */
1916
+ readonly error: ApplyError;
1917
+ /** Discriminator tag. */
1918
+ readonly kind: "resourceOpFailed";
1919
+ /** Operation type that was being attempted. */
1920
+ readonly opType: "create" | "update";
1921
+ /** Resource-kind discriminator. */
1922
+ readonly resourceKind: ResourceKind;
1923
+ }
1924
+ /**
1925
+ * Aggregate footer event emitted after `applyOps` finishes (Phase 2 settled).
1926
+ * Fires unconditionally, including on partial failure; `durationMs` measures
1927
+ * apply time only (state-write time excluded).
1928
+ */
1929
+ interface ApplySummaryEvent {
1930
+ /** Count of successful create ops. */
1931
+ readonly created: number;
1932
+ /** Wall-clock duration between `applyOps` entry and Phase 2 resolution, in milliseconds. */
1933
+ readonly durationMs: number;
1934
+ /** Environment whose reconcile is running. */
1935
+ readonly environment: string;
1936
+ /** Count of failed ops (any opType). */
1937
+ readonly failed: number;
1938
+ /** Discriminator tag. */
1939
+ readonly kind: "applySummary";
1940
+ /** Count of noop ops. */
1941
+ readonly noop: number;
1942
+ /** Count of successful update ops. */
1943
+ readonly updated: number;
1944
+ }
1945
+ /**
1946
+ * Per-environment event emitted after `statePort.write` returns `Ok`.
1947
+ * Not emitted on write failure: the existing `deployFailure` event with
1948
+ * `kind: "stateWriteFailed"` runs the existing failure flow. The payload
1949
+ * carries no backend identity; renderers read the backend label from the
1950
+ * project config when constructing the rendered line.
1951
+ */
1952
+ interface StateWrittenEvent {
1953
+ /** Environment whose state snapshot was just persisted. */
1954
+ readonly environment: string;
1955
+ /** Discriminator tag. */
1956
+ readonly kind: "stateWritten";
1957
+ }
1958
+ /**
1959
+ * Discriminated union of progress events the CLI emits while a deploy
1960
+ * runs. The variant set is additive: future per-stage and per-resource
1961
+ * events land as new `kind` values without breaking existing adapters.
1962
+ */
1963
+ type ProgressEvent = ApplySummaryEvent | DeployFailureEvent | DeploySuccessEvent | ResourceOpFailedEvent | ResourceOpNoopEvent | ResourceOpStartedEvent | ResourceOpSucceededEvent | StateWrittenEvent;
1964
+ /**
1965
+ * Plugin contract for receiving deploy outcomes: the interface an adapter
1966
+ * (clack renderer, JSON logger, custom UI) implements to let the CLI hand
1967
+ * off events without re-implementing rendering logic.
1685
1968
  *
1686
- * On success the returned array carries the driver outputs for every
1687
- * non-noop op, in dispatched order. Noops are not represented; callers
1688
- * needing a full post-apply snapshot merge with the pre-apply current
1689
- * state keyed by `ResourceKey`.
1969
+ * `ProgressPort` is a *driven* (secondary) port in hexagonal terms.
1690
1970
  *
1691
- * @param ops - Reconciliation operations produced by `diff`, applied in order.
1692
- * @param registry - Per-kind driver table; dispatch uses `op.desired.kind` as the index.
1693
- * @returns `Ok(state)` when every operation succeeds, where `state` holds
1694
- * driver outputs for each non-noop op in dispatched order; or the first
1695
- * failure encountered.
1696
- * @throws Whatever the dispatched driver rejects with outside its `Result`
1697
- * return. A driver whose injected I/O (file reads, network calls, etc.)
1698
- * throws will surface that rejection here rather than translating it into
1699
- * a `Result` failure; wrap the call site in a try/catch when drivers are
1700
- * not trusted to contain their own rejections.
1701
1971
  * @example
1702
1972
  *
1703
1973
  * ```ts
1704
- * import {
1705
- * applyOps,
1706
- * asResourceKey,
1707
- * asRobloxAssetId,
1708
- * asSha256Hex,
1709
- * type DriverRegistry,
1710
- * type Operation,
1711
- * } from "@bedrock-rbx/core";
1974
+ * import type { ProgressEvent, ProgressPort } from "@bedrock-rbx/core";
1712
1975
  *
1713
- * const registry: DriverRegistry = {
1714
- * gamePass: {
1715
- * async create(desired) {
1716
- * return {
1717
- * data: {
1718
- * ...desired,
1719
- * outputs: {
1720
- * assetId: asRobloxAssetId("9876543210"),
1721
- * iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
1722
- * },
1723
- * },
1724
- * success: true,
1725
- * };
1726
- * },
1727
- * },
1728
- * place: {
1729
- * async create(desired) {
1730
- * return {
1731
- * data: { ...desired, outputs: { versionNumber: 1 } },
1732
- * success: true,
1733
- * };
1734
- * },
1735
- * },
1736
- * universe: {
1737
- * async create(desired) {
1738
- * return {
1739
- * data: { ...desired, outputs: { rootPlaceId: asRobloxAssetId("4711") } },
1740
- * success: true,
1741
- * };
1742
- * },
1743
- * },
1744
- * developerProduct: {
1745
- * async create(desired) {
1746
- * return {
1747
- * data: {
1748
- * ...desired,
1749
- * outputs: { productId: asRobloxAssetId("8172635495") },
1750
- * },
1751
- * success: true,
1752
- * };
1753
- * },
1976
+ * let received: ReadonlyArray<ProgressEvent> = [];
1977
+ * const port: ProgressPort = {
1978
+ * emit(event) {
1979
+ * received = [...received, event];
1754
1980
  * },
1755
1981
  * };
1756
1982
  *
1757
- * const ops: ReadonlyArray<Operation> = [
1758
- * {
1759
- * key: asResourceKey("vip-pass"),
1760
- * type: "create",
1761
- * desired: {
1762
- * key: asResourceKey("vip-pass"),
1763
- * name: "VIP Pass",
1764
- * description: "Grants VIP perks.",
1765
- * icon: { "en-us": "assets/vip-icon.png" },
1766
- * iconFileHashes: {
1767
- * "en-us": asSha256Hex(
1768
- * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1769
- * ),
1770
- * },
1771
- * kind: "gamePass",
1772
- * price: 500,
1773
- * },
1774
- * },
1775
- * ];
1983
+ * port.emit({ environment: "production", kind: "deploySuccess", resourceCount: 3 });
1776
1984
  *
1777
- * return applyOps(ops, registry).then((result) => {
1778
- * expect(result.success).toBe(true);
1779
- * expect(result.success && result.data).toHaveLength(1);
1780
- * });
1985
+ * expect(received).toEqual([
1986
+ * { environment: "production", kind: "deploySuccess", resourceCount: 3 },
1987
+ * ]);
1781
1988
  * ```
1782
1989
  */
1783
- declare function applyOps(ops: ReadonlyArray<Operation>, registry: DriverRegistry): Promise<Result$1<ReadonlyArray<ResourceCurrentState>, ApplyError>>;
1990
+ interface ProgressPort {
1991
+ /** Hand a single progress event to the adapter for rendering or logging. */
1992
+ emit(event: ProgressEvent): void;
1993
+ }
1784
1994
  //#endregion
1785
1995
  //#region src/shell/build-default-registry.d.ts
1786
1996
  /**
@@ -2065,16 +2275,21 @@ declare function flattenConfig(config: ResolvedConfig): ReadonlyArray<ResourceDe
2065
2275
  //#endregion
2066
2276
  //#region src/core/kinds/module.d.ts
2067
2277
  /**
2068
- * Failure surfaced during desired-state preparation. Two variants today:
2278
+ * Failure surfaced during desired-state preparation. Three variants today:
2069
2279
  *
2070
2280
  * - `fileReadFailed`: a kind module's `normalize` could not read a file
2071
2281
  * the input declared (e.g. An icon path that is missing on disk).
2072
2282
  * - `iconRemovalRejected`: `validatePlan` saw a kind whose prior current
2073
2283
  * state recorded an icon that the desired state no longer declares,
2074
2284
  * and the kind has no documented unset path on the upstream API.
2285
+ * - `redactedNameCollision`: `validatePlan` saw two developer-products in
2286
+ * the same plan that resolve to the same wire `name`. The upstream
2287
+ * Roblox API enforces per-universe uniqueness on developer-product
2288
+ * names and would reject the second PATCH with `DuplicateProductName`.
2075
2289
  *
2076
- * Both variants carry the offending `key` so the CLI can attribute the
2077
- * failure to a single resource entry.
2290
+ * The single-resource variants carry the offending `key` so the CLI can
2291
+ * attribute the failure to one entry; `redactedNameCollision` carries
2292
+ * both colliding keys and the resolved name.
2078
2293
  *
2079
2294
  * @example
2080
2295
  *
@@ -2100,6 +2315,11 @@ type BuildDesiredError = {
2100
2315
  /** ResourceKey of the entry whose icon is being removed. */readonly key: ResourceKey; /** Literal discriminator for narrowing. */
2101
2316
  readonly kind: "iconRemovalRejected"; /** Human-readable explanation naming the resource and the invariant. */
2102
2317
  readonly message: string;
2318
+ } | {
2319
+ /** The two developer-product keys whose desired `name` resolves to the same string. */readonly keys: readonly [ResourceKey, ResourceKey]; /** Literal discriminator for narrowing. */
2320
+ readonly kind: "redactedNameCollision"; /** Human-readable explanation naming both keys and the override remedy. */
2321
+ readonly message: string; /** The wire `name` value both products resolve to. */
2322
+ readonly resolvedName: string;
2103
2323
  };
2104
2324
  /**
2105
2325
  * I/O surface the shell injects into kind-module `normalize` calls. Carries
@@ -2163,6 +2383,8 @@ interface KindIo {
2163
2383
  * },
2164
2384
  * success: true,
2165
2385
  * }),
2386
+ * changedFieldsBetween: (desired, current) =>
2387
+ * desired.name === current.name ? [] : ["name"],
2166
2388
  * fieldsEqual: (desired, current) => desired.name === current.name,
2167
2389
  * };
2168
2390
  *
@@ -2179,6 +2401,15 @@ interface ResourceKindModule<K extends ResourceKind> {
2179
2401
  * invariants omit this hook.
2180
2402
  */
2181
2403
  readonly assertReconcilable?: (current: ResourceCurrentState<K>, desired: DesiredFor<K>) => Result$1<undefined, BuildDesiredError>;
2404
+ /**
2405
+ * Top-level field names that differ between `desired` and `current`,
2406
+ * in a kind-specific deterministic order. Mirrors `fieldsEqual`
2407
+ * semantics, so `fieldsEqual(d, c)` is `true` iff this returns `[]`.
2408
+ * Granularity is top-level: `discordSocialLink`, not
2409
+ * `discordSocialLink.uri`. Plan and apply renderers consume this as
2410
+ * the single source of truth for "what changed" on an update op.
2411
+ */
2412
+ changedFieldsBetween(desired: DesiredFor<K>, current: ResourceCurrentState<K>): ReadonlyArray<string>;
2182
2413
  /** ArkType schema for the authored entry body of this kind. */
2183
2414
  readonly entrySchema: Type$1<ResourceEntryByKind[K]>;
2184
2415
  /**
@@ -2362,6 +2593,13 @@ interface DeployOptions {
2362
2593
  readonly getEnv?: (name: string) => string | undefined;
2363
2594
  /** Loader invoked when `config` is omitted; defaults to `loadConfig` from this package. */
2364
2595
  readonly loadConfig?: (options?: LoadConfigOptions) => Promise<Result$1<Config, ConfigError>>;
2596
+ /**
2597
+ * Optional sink for per-resource and aggregate progress events. When
2598
+ * supplied, `applyOps` emits one started/terminal pair per non-noop op
2599
+ * (plus per-noop and summary events), and `deploy` emits `stateWritten`
2600
+ * after a successful state-write. Omit to run silently.
2601
+ */
2602
+ readonly progress?: ProgressPort;
2365
2603
  /** Reads file bytes for resources that have file-backed inputs. Defaults to `node:fs/promises.readFile`. */
2366
2604
  readonly readFile?: (path: string) => Promise<Uint8Array>;
2367
2605
  /** Per-kind driver table consulted for create / update dispatch. Default-constructed from `BEDROCK_API_KEY` when omitted. */
@@ -2377,8 +2615,8 @@ interface DeployOptions {
2377
2615
  * `incompletePlaceEntry`, `incompleteUniverseEntry`, `missingCredential`,
2378
2616
  * `unsupportedBackend`, `registryConfigMissing`).
2379
2617
  */
2380
- type DeployError = IncompletePlaceEntryError | IncompleteUniverseEntryError | MissingCredentialError | RegistryConfigError | StateNotConfiguredError | UnknownEnvironmentError | UnsupportedBackendError | {
2381
- readonly cause: ApplyError;
2618
+ type DeployError = IncompletePassEntryError | IncompletePlaceEntryError | IncompleteUniverseEntryError | MissingCredentialError | RegistryConfigError | StateNotConfiguredError | UnknownEnvironmentError | UnsupportedBackendError | {
2619
+ readonly cause: AggregateApplyError;
2382
2620
  readonly kind: "applyFailed";
2383
2621
  } | {
2384
2622
  readonly cause: BuildDesiredError;
@@ -2500,70 +2738,6 @@ interface ClackPort {
2500
2738
  outro(message: string): void;
2501
2739
  }
2502
2740
  //#endregion
2503
- //#region src/ports/progress-port.d.ts
2504
- /**
2505
- * Per-environment outcome event emitted after a deploy completes
2506
- * successfully. Carries the environment name and the count of resources
2507
- * present in the persisted state snapshot.
2508
- */
2509
- interface DeploySuccessEvent {
2510
- /** The environment that finished reconciling. */
2511
- readonly environment: string;
2512
- /** Discriminator tag. */
2513
- readonly kind: "deploySuccess";
2514
- /** Number of resources in the post-deploy state snapshot. */
2515
- readonly resourceCount: number;
2516
- }
2517
- /**
2518
- * Per-environment outcome event emitted when a deploy fails. Carries the
2519
- * environment name and the full {@link DeployError} so a renderer can
2520
- * delegate to the existing diagnostic helpers.
2521
- */
2522
- interface DeployFailureEvent {
2523
- /** The environment whose deploy failed. */
2524
- readonly environment: string;
2525
- /** Stage-tagged failure reason returned by the shell `deploy` function. */
2526
- readonly error: DeployError;
2527
- /** Discriminator tag. */
2528
- readonly kind: "deployFailure";
2529
- }
2530
- /**
2531
- * Discriminated union of progress events the CLI emits while a deploy
2532
- * runs. The variant set is additive: future per-stage and per-resource
2533
- * events land as new `kind` values without breaking existing adapters.
2534
- */
2535
- type ProgressEvent = DeployFailureEvent | DeploySuccessEvent;
2536
- /**
2537
- * Plugin contract for receiving deploy outcomes: the interface an adapter
2538
- * (clack renderer, JSON logger, custom UI) implements to let the CLI hand
2539
- * off events without re-implementing rendering logic.
2540
- *
2541
- * `ProgressPort` is a *driven* (secondary) port in hexagonal terms.
2542
- *
2543
- * @example
2544
- *
2545
- * ```ts
2546
- * import type { ProgressEvent, ProgressPort } from "@bedrock-rbx/core";
2547
- *
2548
- * let received: ReadonlyArray<ProgressEvent> = [];
2549
- * const port: ProgressPort = {
2550
- * emit(event) {
2551
- * received = [...received, event];
2552
- * },
2553
- * };
2554
- *
2555
- * port.emit({ environment: "production", kind: "deploySuccess", resourceCount: 3 });
2556
- *
2557
- * expect(received).toEqual([
2558
- * { environment: "production", kind: "deploySuccess", resourceCount: 3 },
2559
- * ]);
2560
- * ```
2561
- */
2562
- interface ProgressPort {
2563
- /** Hand a single progress event to the adapter for rendering or logging. */
2564
- emit(event: ProgressEvent): void;
2565
- }
2566
- //#endregion
2567
2741
  //#region src/adapters/clack-progress-adapter.d.ts
2568
2742
  /**
2569
2743
  * Configuration for {@link createClackProgressAdapter}.
@@ -2571,12 +2745,18 @@ interface ProgressPort {
2571
2745
  interface ClackProgressAdapterDeps {
2572
2746
  /** Output port the events are rendered through. */
2573
2747
  readonly clack: ClackPort;
2748
+ /**
2749
+ * Loaded project config; the `stateWritten` case resolves the per-environment
2750
+ * `StateConfig` against this to format the backend label. When omitted,
2751
+ * `stateWritten` renders the generic `"state"` placeholder.
2752
+ */
2753
+ readonly config?: Config;
2574
2754
  }
2575
2755
  /**
2576
2756
  * Build a {@link ProgressPort} that renders events through a `ClackPort`.
2577
- * Pattern-matches on the event `kind`: `deploySuccess` becomes a single
2578
- * success line and `deployFailure` delegates to the package's deploy-error
2579
- * rendering helper.
2757
+ * Pattern-matches on the event `kind`: per-resource events render one line each,
2758
+ * the aggregate `applySummary` becomes the deploy footer, and `stateWritten`
2759
+ * names the persistence backend resolved from the loaded `Config`.
2580
2760
  *
2581
2761
  * @example
2582
2762
  *
@@ -2595,12 +2775,12 @@ interface ClackProgressAdapterDeps {
2595
2775
  *
2596
2776
  * const port = createClackProgressAdapter({ clack });
2597
2777
  *
2598
- * port.emit({ environment: "production", kind: "deploySuccess", resourceCount: 3 });
2778
+ * port.emit({ environment: "production", kind: "stateWritten" });
2599
2779
  *
2600
- * expect(lines).toEqual(["ok: production: 3 resources reconciled"]);
2780
+ * expect(lines).toEqual(["log: State written to state"]);
2601
2781
  * ```
2602
2782
  *
2603
- * @param deps - The clack port the adapter renders through.
2783
+ * @param deps - The clack port and optional config the adapter renders through.
2604
2784
  * @returns A `ProgressPort` that renders via clack.
2605
2785
  */
2606
2786
  declare function createClackProgressAdapter(deps: ClackProgressAdapterDeps): ProgressPort;
@@ -3163,8 +3343,12 @@ declare function derivePriceFields(desired: {
3163
3343
  * `update` op if any declared field differs or a `noop` op if every field
3164
3344
  * matches.
3165
3345
  *
3166
- * Ops appear in the order their desired entries appear in the input array so
3167
- * callers can rely on declaration order when logging or applying ops.
3346
+ * Ops appear in the order their desired entries appear in the input array.
3347
+ * `applyOps` regroups them into Phase 1 (universe) and Phase 2 (everything
3348
+ * else) when dispatching; the execution order within Phase 2 is not
3349
+ * guaranteed because Phase 2 dispatches concurrently. Persisted state-file
3350
+ * order is determined by the merge in `deploy.runReconcile` (which retains
3351
+ * prior-snapshot positions for unchanged keys), not by this diff output.
3168
3352
  *
3169
3353
  * @param desired - Declared desired state from user config, already normalized
3170
3354
  * (file hashes computed, nullable wire values mapped to `undefined`).
@@ -3228,6 +3412,11 @@ declare function derivePriceFields(desired: {
3228
3412
  * const ops = diff([unchanged, drifted, fresh], current);
3229
3413
  *
3230
3414
  * expect(ops.map((op) => op.type)).toEqual(["noop", "update", "create"]);
3415
+ *
3416
+ * const updateOp = ops[1]!;
3417
+ * if (updateOp.type === "update") {
3418
+ * expect(updateOp.changedFields).toStrictEqual(["name"]);
3419
+ * }
3231
3420
  * ```
3232
3421
  */
3233
3422
  declare function diff(desired: ReadonlyArray<ResourceDesiredState>, current: ReadonlyArray<ResourceCurrentState>): ReadonlyArray<Operation>;
@@ -3611,5 +3800,5 @@ interface MigrateMantleStateDeps {
3611
3800
  */
3612
3801
  declare function migrateMantleState(deps: MigrateMantleStateDeps): Promise<Result$1<MigrationReport, MigrateError>>;
3613
3802
  //#endregion
3614
- export { type ApplyError, type BaseOperation, type BedrockState, type BuildDesiredError, type ClackPort, type ClackProgressAdapterDeps, type Config, type ConfigContext, type ConfigEnvironmentUniverseId, type ConfigError, type ConfigInput, type ConfigRootUniverseId, type ConfigValidationIssue, type CreateOperation, DEFAULT_PREFIX_FORMAT, type DeployError, type DeployFailureEvent, type DeployOptions, type DeploySuccessEvent, type DeveloperProductDesiredInput, type DeveloperProductDesiredState, type DeveloperProductDriverDeps, type DeveloperProductEntry, type DeveloperProductOutputs, type DisplayNamePrefixConfig, type DriverRegistry, type EnvironmentEntry, type GamePassDesiredInput, type GamePassDesiredState, type GamePassDriverDeps, type GamePassEntry, type GamePassOutputs, type GetEnvironmentError, type GistStateAdapterDeps, type GistStateConfig, type IncompletePlaceEntryError, type IncompleteUniverseEntryError, type KindIo, type KindRegistry, type LoadConfigOptions, type MigrateError, type MigrateMantleStateDeps, type MigrationReport, type MigrationSummary, type MigrationWarning, type MissingCredentialError, type NoopOperation, OpenCloudError, type Operation, type PlaceDesiredInput, type PlaceDesiredState, type PlaceDriverDeps, type PlaceEntry, type PlaceOutputs, type PriceFields, type ProgressEvent, type ProgressPort, type RegistryConfigError, type ResolvedConfig, type ResolvedPlaceEntry, type ResolvedUniverseEntry, type ResourceCurrentState, type ResourceDesiredInput, type ResourceDesiredState, type ResourceDriver, type ResourceEntryByKind, type ResourceKey, type ResourceKind, type ResourceKindModule, type ResourceOutputs, type ResourceOutputsByKind, type Result, type RobloxAssetId, SOCIAL_LINK_FIELDS, type SelectEnvironmentError, type Sha256Hex, type SocialLink, type SocialLinkField, type StateConfig, type StateError, type StateNotConfiguredError, type StatePort, type StatesByEnvironment, UNIVERSE_SINGLETON_KEY, type UniverseDesiredInput, type UniverseDesiredState, type UniverseDriverDeps, type UniverseEntry, type UniverseOutputs, type UniverseOverlayWithId, type UniverseOverlayWithoutId, type UnknownEnvironmentError, type UnsupportedBackendError, type UpdateOperation, applyOps, asResourceKey, asRobloxAssetId, asSha256Hex, buildDefaultRegistry, buildDesired, buildStatePort, createClackPort, createClackProgressAdapter, createDeveloperProductDriver, createGamePassDriver, createGistStateAdapter, createNoOpProgressAdapter, createPlaceDriver, createUniverseDriver, defaultKindRegistry, defineConfig, deploy, derivePriceFields, diff, flattenConfig, getEnvironment, isGistStateConfig, isResourceKey, isRobloxAssetId, isSha256Hex, loadConfig, migrateMantleState, parseStateFile, renderDisplayNamePrefix, resolveStateConfig, selectEnvironment, serializeStateFile, shouldReuploadIcon, validateConfig, validateEnvironmentName, validatePlan };
3803
+ export { type AggregateApplyError, type ApplyError, type ApplyOpsReporting, type ApplySummaryEvent, type BaseOperation, type BedrockState, type BuildDesiredError, type ClackPort, type ClackProgressAdapterDeps, type Config, type ConfigContext, type ConfigEnvironmentUniverseId, type ConfigError, type ConfigInput, type ConfigRootUniverseId, type ConfigValidationIssue, type CreateOperation, DEFAULT_PREFIX_FORMAT, type DeployError, type DeployFailureEvent, type DeployOptions, type DeploySuccessEvent, type DeveloperProductDesiredInput, type DeveloperProductDesiredState, type DeveloperProductDriverDeps, type DeveloperProductEntry, type DeveloperProductOutputs, type DisplayNamePrefixConfig, type DriverRegistry, type EnvironmentEntry, type GamePassDesiredInput, type GamePassDesiredState, type GamePassDriverDeps, type GamePassEntry, type GamePassOutputs, type GetEnvironmentError, type GistStateAdapterDeps, type GistStateConfig, type IncompletePlaceEntryError, type IncompleteUniverseEntryError, type KindIo, type KindRegistry, type LoadConfigOptions, type MigrateError, type MigrateMantleStateDeps, type MigrationReport, type MigrationSummary, type MigrationWarning, type MissingCredentialError, type NoopOperation, OpenCloudError, type Operation, type PlaceDesiredInput, type PlaceDesiredState, type PlaceDriverDeps, type PlaceEntry, type PlaceOutputs, type PriceFields, type ProgressEvent, type ProgressPort, type RedactedDeveloperProductOverride, type RedactedEnvironmentOverride, type RedactedGamePassOverride, type RedactedPlaceOverride, type RegistryConfigError, type ResolvedConfig, type ResolvedPlaceEntry, type ResolvedUniverseEntry, type ResourceCurrentState, type ResourceDesiredInput, type ResourceDesiredState, type ResourceDriver, type ResourceEntryByKind, type ResourceKey, type ResourceKind, type ResourceKindModule, type ResourceOpFailedEvent, type ResourceOpNoopEvent, type ResourceOpStartedEvent, type ResourceOpSucceededCreateEvent, type ResourceOpSucceededEvent, type ResourceOpSucceededUpdateEvent, type ResourceOutputs, type ResourceOutputsByKind, type Result, type RobloxAssetId, SOCIAL_LINK_FIELDS, type SelectEnvironmentError, type Sha256Hex, type SocialLink, type SocialLinkField, type StateConfig, type StateError, type StateNotConfiguredError, type StatePort, type StateWrittenEvent, type StatesByEnvironment, UNIVERSE_SINGLETON_KEY, type UniverseDesiredInput, type UniverseDesiredState, type UniverseDriverDeps, type UniverseEntry, type UniverseOutputs, type UniverseOverlayWithId, type UniverseOverlayWithoutId, type UnknownEnvironmentError, type UnsupportedBackendError, type UpdateOperation, applyOps, asResourceKey, asRobloxAssetId, asSha256Hex, buildDefaultRegistry, buildDesired, buildStatePort, createClackPort, createClackProgressAdapter, createDeveloperProductDriver, createGamePassDriver, createGistStateAdapter, createNoOpProgressAdapter, createPlaceDriver, createUniverseDriver, defaultKindRegistry, defineConfig, deploy, derivePriceFields, diff, flattenConfig, getEnvironment, isGistStateConfig, isResourceKey, isRobloxAssetId, isSha256Hex, loadConfig, migrateMantleState, parseStateFile, renderDisplayNamePrefix, resolveStateConfig, selectEnvironment, serializeStateFile, shouldReuploadIcon, validateConfig, validateEnvironmentName, validatePlan };
3615
3804
  //# sourceMappingURL=index.d.mts.map