@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 +3 -3
- package/src/index.test.ts +84 -0
- package/src/index.ts +80 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/configuration-core",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
16
|
-
"@frockbot/kernel-composition": "0.
|
|
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
|
-
|
|
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
|
|