From d4d34d22d1434c95492b271029fae4d67fffe4e2 Mon Sep 17 00:00:00 2001 From: Kirill Smirnov <135383551+ksanrse@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:27:43 +0300 Subject: [PATCH 01/11] Mark legacy catalog entries as compatibility-only --- catalog.json | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/catalog.json b/catalog.json index 8736aa8..80578fa 100644 --- a/catalog.json +++ b/catalog.json @@ -24,7 +24,10 @@ "iconUrl": "https://github.com/makekosmos/extensions/releases/download/arcadia-v0.1.5/arcadia-0.1.5.icon.png", "downloadUrl": "https://github.com/makekosmos/extensions/releases/download/arcadia-v0.1.5/arcadia-0.1.5.kext", "sha256": "c890b4ad91019418ebf1e42524690237f44b68ebe2c56e1bfa9ec4fe802817e9", - "size": 1201728 + "size": 1201728, + "status": "deprecated", + "replacementId": "com.kosmos.arcadia", + "deprecationReason": "Renamed package; retained only for compatibility migration." }, { "id": "arrancador", @@ -36,7 +39,10 @@ "iconUrl": "https://raw.githubusercontent.com/makekosmos/extensions/main/extensions/arrancador/icon.png", "downloadUrl": "https://github.com/makekosmos/extensions/releases/download/arrancador-v0.1.5/arrancador-0.1.5.kext", "sha256": "3443f338952b5c8272b88328fa4639077e5cc078f0a44f05bd5a58cfd97bb20e", - "size": 1439639 + "size": 1439639, + "status": "deprecated", + "replacementId": "com.kosmos.arcadia", + "deprecationReason": "Renamed package; retained only for compatibility migration." }, { "id": "delphi", @@ -48,7 +54,10 @@ "iconUrl": "https://raw.githubusercontent.com/makekosmos/extensions/main/extensions/delphi/icon.png", "downloadUrl": "https://github.com/makekosmos/extensions/releases/download/delphi-v0.1.8/delphi-0.1.8.kext", "sha256": "9f3e24debcdaf770ddbcc3283b977d201813bcfb7968a3ec3ece1dcddd195d6c", - "size": 1601813 + "size": 1601813, + "status": "deprecated", + "replacementId": "com.kosmos.agenda", + "deprecationReason": "Renamed package; retained only for compatibility migration." }, { "id": "eden", @@ -60,7 +69,13 @@ "iconUrl": "https://raw.githubusercontent.com/makekosmos/extensions/main/extensions/eden/icon.png", "downloadUrl": "https://github.com/makekosmos/extensions/releases/download/eden-v0.5.4/eden-0.5.4.kext", "sha256": "6c0476e2263f0aa45ecbdb713cbf940aef72d0aa4ce81417bc9610ddb1fe7f70", - "size": 12013737 + "size": 12013737, + "status": "deprecated", + "replacementId": "com.kosmos.memoria", + "deprecationReason": "Renamed package; retained only for compatibility migration." } - ] + ], + "status": "compatibility-only", + "supportedClientMax": "legacy", + "replacementPolicy": "Existing clients may resolve replacementId; new releases belong in Store and Package Index." } From 3af90ef891f364a490096640cc979adbd2cfe260 Mon Sep 17 00:00:00 2001 From: Kirill Smirnov <135383551+ksanrse@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:28:01 +0300 Subject: [PATCH 02/11] Add compatibility catalog validator --- scripts/validate-catalog.mjs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 scripts/validate-catalog.mjs diff --git a/scripts/validate-catalog.mjs b/scripts/validate-catalog.mjs new file mode 100644 index 0000000..68f81ac --- /dev/null +++ b/scripts/validate-catalog.mjs @@ -0,0 +1,28 @@ +#!/usr/bin/env node +import { readFile } from "node:fs/promises"; + +const catalog = JSON.parse(await readFile(new URL("../catalog.json", import.meta.url), "utf8")); +const entries = catalog.extensions; +if (catalog.schemaVersion !== 1 || catalog.status !== "compatibility-only" || catalog.supportedClientMax !== "legacy") { + throw new Error("catalog must declare schemaVersion 1 and compatibility-only status"); +} +if (!Array.isArray(entries) || entries.length === 0) throw new Error("extensions must be a non-empty array"); + +const ids = new Set(); +const edges = new Map(); +for (const entry of entries) { + if (!entry || typeof entry.id !== "string" || ids.has(entry.id)) throw new Error("duplicate or invalid extension id"); + ids.add(entry.id); + if (typeof entry.name !== "string" || typeof entry.description !== "string") throw new Error(`${entry.id}: name/description required`); + if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(entry.version)) throw new Error(`${entry.id}: invalid semver`); + if (!/^https:\/\//.test(entry.iconUrl) || !/^https:\/\//.test(entry.downloadUrl)) throw new Error(`${entry.id}: HTTPS URLs required`); + if (!/^[a-f0-9]{64}$/.test(entry.sha256) || !Number.isSafeInteger(entry.size) || entry.size <= 0) throw new Error(`${entry.id}: invalid artifact integrity`); + if (entry.status !== "deprecated" || typeof entry.replacementId !== "string" || !entry.replacementId) { + throw new Error(`${entry.id}: compatibility entries must declare deprecation and replacement`); + } + edges.set(entry.id, entry.replacementId); +} +for (const [id, replacement] of edges) { + if (replacement === id || edges.has(replacement)) throw new Error(`${id}: replacement chain must terminate outside the legacy catalog`); +} +console.log(`Validated ${entries.length} compatibility entries.`); From 4f5d49510055d29ff5cfa8195a713801daf050b2 Mon Sep 17 00:00:00 2001 From: Kirill Smirnov <135383551+ksanrse@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:28:02 +0300 Subject: [PATCH 03/11] Add catalog validation CI --- .github/workflows/quality.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/quality.yml diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..47feab8 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,25 @@ +name: Catalog quality + +on: + pull_request: + paths: + - "catalog.json" + - "scripts/**" + - ".github/workflows/**" + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: extensions-quality-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Validate compatibility catalog + run: node scripts/validate-catalog.mjs From 5adec5640f349a950b54a005152640040a1a3418 Mon Sep 17 00:00:00 2001 From: Kirill Smirnov <135383551+ksanrse@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:28:10 +0300 Subject: [PATCH 04/11] Document compatibility-only catalog policy --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d675993..c586365 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ # Kosmos Extensions -Binary marketplace for Kosmos .kext extension packages. +This repository is a frozen, compatibility-only catalog for legacy `.kext` +clients. It is not the source of truth for current product discovery or +installation. + +Legacy entries in `catalog.json` carry an explicit `replacementId`: + +- Arrancador and the legacy Arcadia entry migrate to `com.kosmos.arcadia`; +- Eden migrates to `com.kosmos.memoria`; +- Delphi migrates to `com.kosmos.agenda`. + +New first-party discovery belongs in the signed Store catalog, and installation +authority belongs in the signed Package Index. Do not publish new first-party +artifacts here. The compatibility catalog is validated with: + +```powershell +node scripts/validate-catalog.mjs +``` From 8c2d7d9f7a8e2a5a26d5b0e62f8a3559785907f7 Mon Sep 17 00:00:00 2001 From: Kirill Smirnov <135383551+ksanrse@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:32:47 +0300 Subject: [PATCH 05/11] Classify retained legacy catalog entries --- catalog.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/catalog.json b/catalog.json index 80578fa..2225d84 100644 --- a/catalog.json +++ b/catalog.json @@ -12,7 +12,8 @@ "iconUrl": "https://raw.githubusercontent.com/makekosmos/extensions/main/extensions/akasha/icon.svg", "downloadUrl": "https://github.com/makekosmos/extensions/releases/download/akasha-v0.1.2/akasha-0.1.2.kext", "sha256": "a180b15143ab1778fde7ee004482649521277dbbf2085ad2a0fdc77a2ad7d246", - "size": 1244170 + "size": 1244170, + "status": "legacy" }, { "id": "arcadia", From 91766e0331d9e91328376ac3771143a98429020a Mon Sep 17 00:00:00 2001 From: Kirill Smirnov <135383551+ksanrse@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:32:56 +0300 Subject: [PATCH 06/11] Allow retained legacy entries without replacement From 845f1c71b39fc036a84b10acbb36efb470b609ee Mon Sep 17 00:00:00 2001 From: Kirill Smirnov <135383551+ksanrse@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:33:09 +0300 Subject: [PATCH 07/11] Allow retained legacy entries without replacement --- scripts/validate-catalog.mjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/validate-catalog.mjs b/scripts/validate-catalog.mjs index 68f81ac..b108915 100644 --- a/scripts/validate-catalog.mjs +++ b/scripts/validate-catalog.mjs @@ -17,10 +17,12 @@ for (const entry of entries) { if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(entry.version)) throw new Error(`${entry.id}: invalid semver`); if (!/^https:\/\//.test(entry.iconUrl) || !/^https:\/\//.test(entry.downloadUrl)) throw new Error(`${entry.id}: HTTPS URLs required`); if (!/^[a-f0-9]{64}$/.test(entry.sha256) || !Number.isSafeInteger(entry.size) || entry.size <= 0) throw new Error(`${entry.id}: invalid artifact integrity`); - if (entry.status !== "deprecated" || typeof entry.replacementId !== "string" || !entry.replacementId) { - throw new Error(`${entry.id}: compatibility entries must declare deprecation and replacement`); + if (entry.status === "deprecated") { + if (typeof entry.replacementId !== "string" || !entry.replacementId) throw new Error(`${entry.id}: deprecated entries require replacementId`); + edges.set(entry.id, entry.replacementId); + } else if (entry.status !== "legacy") { + throw new Error(`${entry.id}: compatibility entries must be legacy or deprecated`); } - edges.set(entry.id, entry.replacementId); } for (const [id, replacement] of edges) { if (replacement === id || edges.has(replacement)) throw new Error(`${id}: replacement chain must terminate outside the legacy catalog`); From 20f97ff609b427f0c65bf7135ea0a408b0569f35 Mon Sep 17 00:00:00 2001 From: Kirill Smirnov <135383551+ksanrse@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:46:54 +0300 Subject: [PATCH 08/11] Export compatibility validator and enforce deprecation metadata --- scripts/validate-catalog.mjs | 61 ++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/scripts/validate-catalog.mjs b/scripts/validate-catalog.mjs index b108915..a22f3a2 100644 --- a/scripts/validate-catalog.mjs +++ b/scripts/validate-catalog.mjs @@ -1,30 +1,45 @@ #!/usr/bin/env node import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; -const catalog = JSON.parse(await readFile(new URL("../catalog.json", import.meta.url), "utf8")); -const entries = catalog.extensions; -if (catalog.schemaVersion !== 1 || catalog.status !== "compatibility-only" || catalog.supportedClientMax !== "legacy") { - throw new Error("catalog must declare schemaVersion 1 and compatibility-only status"); -} -if (!Array.isArray(entries) || entries.length === 0) throw new Error("extensions must be a non-empty array"); +export function validateCatalog(catalog) { + const entries = catalog?.extensions; + if (catalog?.schemaVersion !== 1 || catalog.status !== "compatibility-only" || catalog.supportedClientMax !== "legacy") { + throw new Error("catalog must declare schemaVersion 1 and compatibility-only status"); + } + if (!Array.isArray(entries) || entries.length === 0) throw new Error("extensions must be a non-empty array"); -const ids = new Set(); -const edges = new Map(); -for (const entry of entries) { - if (!entry || typeof entry.id !== "string" || ids.has(entry.id)) throw new Error("duplicate or invalid extension id"); - ids.add(entry.id); - if (typeof entry.name !== "string" || typeof entry.description !== "string") throw new Error(`${entry.id}: name/description required`); - if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(entry.version)) throw new Error(`${entry.id}: invalid semver`); - if (!/^https:\/\//.test(entry.iconUrl) || !/^https:\/\//.test(entry.downloadUrl)) throw new Error(`${entry.id}: HTTPS URLs required`); - if (!/^[a-f0-9]{64}$/.test(entry.sha256) || !Number.isSafeInteger(entry.size) || entry.size <= 0) throw new Error(`${entry.id}: invalid artifact integrity`); - if (entry.status === "deprecated") { - if (typeof entry.replacementId !== "string" || !entry.replacementId) throw new Error(`${entry.id}: deprecated entries require replacementId`); - edges.set(entry.id, entry.replacementId); - } else if (entry.status !== "legacy") { - throw new Error(`${entry.id}: compatibility entries must be legacy or deprecated`); + const ids = new Set(); + const edges = new Map(); + for (const entry of entries) { + if (!entry || typeof entry.id !== "string" || ids.has(entry.id)) throw new Error("duplicate or invalid extension id"); + ids.add(entry.id); + if (typeof entry.name !== "string" || typeof entry.description !== "string") throw new Error(`${entry.id}: name/description required`); + if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(entry.version)) throw new Error(`${entry.id}: invalid semver`); + if (!/^https:\/\//.test(entry.iconUrl) || !/^https:\/\//.test(entry.downloadUrl)) throw new Error(`${entry.id}: HTTPS URLs required`); + if (!/^[a-f0-9]{64}$/.test(entry.sha256) || !Number.isSafeInteger(entry.size) || entry.size <= 0) throw new Error(`${entry.id}: invalid artifact integrity`); + if (entry.status === "deprecated") { + if (typeof entry.replacementId !== "string" || !entry.replacementId || entry.replacementId === entry.id) throw new Error(`${entry.id}: deprecated entries require a distinct replacementId`); + if (typeof entry.deprecationReason !== "string" || !entry.deprecationReason.trim()) throw new Error(`${entry.id}: deprecationReason is required`); + edges.set(entry.id, entry.replacementId); + } else if (entry.status !== "legacy") { + throw new Error(`${entry.id}: compatibility entries must be legacy or deprecated`); + } + } + for (const [id, replacement] of edges) { + if (edges.has(replacement)) throw new Error(`${id}: replacement chain must terminate outside the legacy catalog`); } + return true; } -for (const [id, replacement] of edges) { - if (replacement === id || edges.has(replacement)) throw new Error(`${id}: replacement chain must terminate outside the legacy catalog`); + +async function main() { + const catalog = JSON.parse(await readFile(new URL("../catalog.json", import.meta.url), "utf8")); + validateCatalog(catalog); + console.log(`Validated ${catalog.extensions.length} compatibility entries.`); } -console.log(`Validated ${entries.length} compatibility entries.`); + +if (path.resolve(process.argv[1] || "") === fileURLToPath(import.meta.url)) main().catch((error) => { + console.error(error.message); + process.exitCode = 1; +}); From 36d3e503bd5dcc89f75342de6f16f3e4a01a089c Mon Sep 17 00:00:00 2001 From: Kirill Smirnov <135383551+ksanrse@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:47:04 +0300 Subject: [PATCH 09/11] Add compatibility catalog negative tests --- scripts/validate-catalog.test.mjs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 scripts/validate-catalog.test.mjs diff --git a/scripts/validate-catalog.test.mjs b/scripts/validate-catalog.test.mjs new file mode 100644 index 0000000..e7e9db0 --- /dev/null +++ b/scripts/validate-catalog.test.mjs @@ -0,0 +1,28 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { validateCatalog } from "./validate-catalog.mjs"; + +const source = JSON.parse(await readFile(new URL("../catalog.json", import.meta.url), "utf8")); +const copy = () => structuredClone(source); + +test("accepts frozen compatibility catalog", () => assert.equal(validateCatalog(source), true)); + +for (const [name, mutate, pattern] of [ + ["duplicate identities", (c) => c.extensions.push(structuredClone(c.extensions[0])), /duplicate/], + ["bad artifact URL", (c) => { c.extensions[0].downloadUrl = "http://example.invalid/a.kext"; }, /HTTPS/], + ["bad artifact hash", (c) => { c.extensions[0].sha256 = "bad"; }, /integrity/], + ["missing replacement metadata", (c) => { c.extensions[1].replacementId = ""; }, /replacementId/], + ["missing deprecation reason", (c) => { c.extensions[1].deprecationReason = ""; }, /deprecationReason/], + ["replacement cycle", (c) => { + c.extensions[1].replacementId = "arrancador"; + c.extensions[2].replacementId = "arcadia"; + }, /replacement chain/], + ["active entry in compatibility feed", (c) => { c.extensions[0].status = "active"; }, /legacy or deprecated/], +]) { + test(name, () => assert.throws(() => { + const c = copy(); + mutate(c); + validateCatalog(c); + }, pattern)); +} From fc2115f290d04877939e73a55d59292e36c262a1 Mon Sep 17 00:00:00 2001 From: Kirill Smirnov <135383551+ksanrse@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:47:12 +0300 Subject: [PATCH 10/11] Run compatibility negative tests and actionlint --- .github/workflows/quality.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 47feab8..7d582a5 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -23,3 +23,7 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Validate compatibility catalog run: node scripts/validate-catalog.mjs + - name: Run replacement and integrity tests + run: node --test scripts/validate-catalog.test.mjs + - name: Actionlint + uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2 From edf8ef071fba338c51f10186bc4f37fc1428038a Mon Sep 17 00:00:00 2001 From: Kirill Smirnov <135383551+ksanrse@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:47:21 +0300 Subject: [PATCH 11/11] Document compatibility-only policy and validation gates --- README.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c586365..6455419 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,29 @@ This repository is a frozen, compatibility-only catalog for legacy `.kext` clients. It is not the source of truth for current product discovery or installation. -Legacy entries in `catalog.json` carry an explicit `replacementId`: +Legacy entries in `catalog.json` carry explicit deprecation and +`replacementId` metadata: - Arrancador and the legacy Arcadia entry migrate to `com.kosmos.arcadia`; - Eden migrates to `com.kosmos.memoria`; - Delphi migrates to `com.kosmos.agenda`. -New first-party discovery belongs in the signed Store catalog, and installation -authority belongs in the signed Package Index. Do not publish new first-party -artifacts here. The compatibility catalog is validated with: +A compatibility client may resolve these aliases for upgrade, but no entry in +this feed is an active current product. New first-party discovery belongs in +the signed Store catalog, and installation authority belongs in the signed +Package Index. Do not publish new first-party artifacts here. + +## Validation + +Catalog CI rejects duplicate identities, malformed metadata, non-HTTPS or +invalid artifact integrity values, missing deprecation reasons/replacements, and +replacement cycles: ```powershell node scripts/validate-catalog.mjs +node --test scripts/validate-catalog.test.mjs ``` + +The final migration of existing `.kext` data, settings, and grants remains a +consumer/client release gate; this repository cannot prove that external +upgrade path by itself.