@frockbot/configuration-core 0.2.4 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/configuration-core",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -12,8 +12,8 @@
12
12
  "typecheck": "tsc --noEmit -p tsconfig.json"
13
13
  },
14
14
  "dependencies": {
15
- "@frockbot/connection-core": "0.2.4",
16
- "@frockbot/kernel-composition": "0.2.4"
15
+ "@frockbot/connection-core": "0.3.0",
16
+ "@frockbot/kernel-composition": "0.3.0"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/bun": "1.3.6",
package/src/index.test.ts CHANGED
@@ -163,6 +163,90 @@ describe("stored configuration migrations", () => {
163
163
  expect(stored.platformModel).toBeDefined();
164
164
  });
165
165
 
166
+ test("the read-time repair scope touches only platform-owned rows", () => {
167
+ // A User who disables Web keeps Ollama Cloud installed even though its
168
+ // manifest names Web as a dependency: repair is not migration, and a
169
+ // deployment that omits a Package must not retire durable rows either.
170
+ const stored = {
171
+ schemaVersion: 1,
172
+ revision: 40,
173
+ profile: { name: "Current User" },
174
+ packages: [
175
+ { packageId: "web", version: "0.0.1", state: "disabled" as const },
176
+ {
177
+ packageId: "provider",
178
+ version: "0.0.1",
179
+ state: "installed" as const,
180
+ },
181
+ {
182
+ packageId: "omitted-today",
183
+ version: "0.0.1",
184
+ state: "installed" as const,
185
+ },
186
+ { packageId: "shell", version: "0.0.1", state: "disabled" as const },
187
+ ],
188
+ connections: [
189
+ {
190
+ connectionId: "provider-connection",
191
+ packageId: "provider",
192
+ connectionTypeId: "provider-account",
193
+ displayName: "Provider",
194
+ state: "ready" as const,
195
+ safeMetadata: {},
196
+ },
197
+ ],
198
+ platformModel: {
199
+ connectionId: "gone-connection",
200
+ providerModelId: "gone-model",
201
+ },
202
+ };
203
+ const packages = [
204
+ { packageId: "web", version: "0.0.1" },
205
+ {
206
+ packageId: "provider",
207
+ version: "0.0.1",
208
+ dependencies: { web: ">=0.0.1" },
209
+ },
210
+ { packageId: "shell", version: "0.0.1", platformOwned: true },
211
+ { packageId: "ui-theme", version: "0.0.1", platformOwned: true },
212
+ ];
213
+
214
+ const repaired = decodeUserSettingsViewV1(
215
+ migrateStoredUserSettingsV1(stored, packages, "repair"),
216
+ );
217
+ expect(repaired.packages).toEqual([
218
+ { packageId: "web", version: "0.0.1", state: "disabled" },
219
+ { packageId: "provider", version: "0.0.1", state: "installed" },
220
+ { packageId: "omitted-today", version: "0.0.1", state: "installed" },
221
+ {
222
+ packageId: "shell",
223
+ version: "0.0.1",
224
+ state: "installed",
225
+ provenance: "first-party",
226
+ },
227
+ {
228
+ packageId: "ui-theme",
229
+ version: "0.0.1",
230
+ state: "installed",
231
+ provenance: "first-party",
232
+ },
233
+ ]);
234
+ // The platform model is the provider bootstrap's to re-seed, not the
235
+ // repair's to clear: clearing it on a read would race that bootstrap.
236
+ expect(repaired.platformModel).toEqual(stored.platformModel);
237
+
238
+ const migrated = decodeUserSettingsViewV1(
239
+ migrateStoredUserSettingsV1(stored, packages, "migrate"),
240
+ );
241
+ expect(migrated.packages.map((pkg) => [pkg.packageId, pkg.state])).toEqual([
242
+ ["web", "disabled"],
243
+ ["provider", "disabled"],
244
+ ["shell", "installed"],
245
+ ["ui-theme", "installed"],
246
+ ]);
247
+ expect(migrated.platformModel).toBeUndefined();
248
+ });
249
+
166
250
  test("keeps version mismatches and malformed platform bindings visible", () => {
167
251
  const versionMismatch = {
168
252
  schemaVersion: 1,
package/src/index.ts CHANGED
@@ -1871,12 +1871,29 @@ export interface StoredUserSettingsPackageV1 {
1871
1871
  packageId: string;
1872
1872
  version: string;
1873
1873
  dependencies?: Readonly<Record<string, string>>;
1874
+ /** Platform infrastructure is repaired to one enabled first-party row. */
1875
+ platformOwned?: boolean;
1874
1876
  }
1875
1877
 
1878
+ /**
1879
+ * How much of the Catalog-relative pass a reader may apply.
1880
+ *
1881
+ * `migrate` is the one-shot marker upgrade: it retires installations absent
1882
+ * from the Catalog, drops their orphaned Connections, disables dependents whose
1883
+ * dependency is not enabled, and clears a platform binding that no longer
1884
+ * resolves. `repair` runs on every read and touches only platform-owned rows,
1885
+ * because a deployment that temporarily omits a Package must not erase current
1886
+ * durable state, and a User disabling one Package must not silently disable
1887
+ * another that merely declares it as a dependency.
1888
+ */
1889
+ export type StoredUserSettingsMigrationScopeV1 = "migrate" | "repair";
1890
+
1876
1891
  function migrateCatalogRelativeUserSettingsV1(
1877
1892
  settings: Record<string, unknown>,
1878
1893
  packages: readonly StoredUserSettingsPackageV1[],
1894
+ scope: StoredUserSettingsMigrationScopeV1,
1879
1895
  ): Record<string, unknown> {
1896
+ const migrate = scope === "migrate";
1880
1897
  const availableVersions = new Map(
1881
1898
  packages.map((pkg) => [`${pkg.packageId}\u0000${pkg.version}`, pkg]),
1882
1899
  );
@@ -1905,15 +1922,73 @@ function migrateCatalogRelativeUserSettingsV1(
1905
1922
  // A different immutable version remains a visible, repairable Catalog
1906
1923
  // mismatch. Only an id absent from the Catalog proves that the Package was
1907
1924
  // retired and that its row is now orphaned durable state.
1908
- const retained = availablePackageIds.has(packageId);
1925
+ const retained = !migrate || availablePackageIds.has(packageId);
1909
1926
  if (!retained) retiredFirstPartyPackageIds.add(packageId);
1910
1927
  changed ||= !retained;
1911
1928
  return retained;
1912
1929
  });
1913
1930
 
1931
+ // Platform infrastructure is not a User preference. Repair it on every
1932
+ // catalog-relative read, including records that already carry the latest
1933
+ // one-shot bootstrap marker, and collapse any duplicate rows to one current
1934
+ // first-party installation.
1935
+ const platformPackages = new Map(
1936
+ packages
1937
+ .filter((pkg) => pkg.platformOwned)
1938
+ .map((pkg) => [pkg.packageId, pkg]),
1939
+ );
1940
+ const repairedPlatformPackageIds = new Set<string>();
1941
+ installations = installations.flatMap((storedInstallation) => {
1942
+ const installation = storedPlainRecordV1(storedInstallation);
1943
+ const packageId = installation
1944
+ ? storedDataValueV1(installation, "packageId")
1945
+ : undefined;
1946
+ const platformPackage =
1947
+ typeof packageId === "string"
1948
+ ? platformPackages.get(packageId)
1949
+ : undefined;
1950
+ if (!installation || !platformPackage) return [storedInstallation];
1951
+ if (repairedPlatformPackageIds.has(platformPackage.packageId)) {
1952
+ changed = true;
1953
+ return [];
1954
+ }
1955
+ repairedPlatformPackageIds.add(platformPackage.packageId);
1956
+ const needsRepair =
1957
+ storedDataValueV1(installation, "version") !== platformPackage.version ||
1958
+ storedDataValueV1(installation, "state") !== "installed" ||
1959
+ storedDataValueV1(installation, "provenance") !== "first-party" ||
1960
+ storedDataValueV1(installation, "failure") !== undefined ||
1961
+ Object.hasOwn(installation, "catalogId") ||
1962
+ Object.hasOwn(installation, "catalogGeneration") ||
1963
+ Object.hasOwn(installation, "contentHash");
1964
+ if (!needsRepair) return [storedInstallation];
1965
+ changed = true;
1966
+ return [
1967
+ cloneStoredRecordV1(
1968
+ installation,
1969
+ {
1970
+ version: platformPackage.version,
1971
+ state: "installed",
1972
+ provenance: "first-party",
1973
+ },
1974
+ ["failure", "catalogId", "catalogGeneration", "contentHash"],
1975
+ ),
1976
+ ];
1977
+ });
1978
+ for (const platformPackage of platformPackages.values()) {
1979
+ if (repairedPlatformPackageIds.has(platformPackage.packageId)) continue;
1980
+ changed = true;
1981
+ installations.push({
1982
+ packageId: platformPackage.packageId,
1983
+ version: platformPackage.version,
1984
+ state: "installed",
1985
+ provenance: "first-party",
1986
+ });
1987
+ }
1988
+
1914
1989
  // Disable inconsistent installed dependents to the least-authority state.
1915
1990
  // Iterate to a fixed point so A -> B -> missing C leaves both A and B off.
1916
- for (;;) {
1991
+ while (migrate) {
1917
1992
  const enabledIds = new Set(
1918
1993
  installations.flatMap((storedInstallation) => {
1919
1994
  const installation = storedPlainRecordV1(storedInstallation);
@@ -1971,7 +2046,7 @@ function migrateCatalogRelativeUserSettingsV1(
1971
2046
  });
1972
2047
 
1973
2048
  let platformModel = storedDataValueV1(settings, "platformModel");
1974
- if (platformModel !== undefined) {
2049
+ if (migrate && platformModel !== undefined) {
1975
2050
  let binding: ModelBindingV1 | undefined;
1976
2051
  try {
1977
2052
  binding = decodeModelBindingV1(platformModel);
@@ -2042,6 +2117,7 @@ function migrateCatalogRelativeUserSettingsV1(
2042
2117
  export function migrateStoredUserSettingsV1(
2043
2118
  stored: unknown,
2044
2119
  packages?: readonly StoredUserSettingsPackageV1[],
2120
+ scope: StoredUserSettingsMigrationScopeV1 = "migrate",
2045
2121
  ): unknown {
2046
2122
  const settings = storedPlainRecordV1(stored);
2047
2123
  if (!settings || storedDataValueV1(settings, "schemaVersion") !== 1) {
@@ -2091,7 +2167,7 @@ export function migrateStoredUserSettingsV1(
2091
2167
  )
2092
2168
  : settings;
2093
2169
  return packages
2094
- ? migrateCatalogRelativeUserSettingsV1(migrated, packages)
2170
+ ? migrateCatalogRelativeUserSettingsV1(migrated, packages, scope)
2095
2171
  : migrated;
2096
2172
  }
2097
2173