ouroboros/web/modules/hub_sync.js
Ouroboros bd7ad09589 perf(skills): the installed list never waits for the hub catalog
GET /api/extensions computed the official_hub_verified display hint through
the authoritative verifier, so every installed OuroborosHub skill cost one
fresh catalog.json download, serially, inside the request the Skills page
blocks on (17 hub skills: 9 s cold on a good link, minutes on a bad one).

The listing is now a local read. The byte-exact match is a pure function,
hub_payload_matches(skill, catalog_files_for): the review profile and owner
attestation still feed it a fresh catalog read, while the listing feeds it
the display-plane memo (§7.1a) it only peeks at and never fetches. Without a
fresh view the hub facts are null (unknown), not false; the page re-reads the
listing once after its own catalog read lands and patches the Published
badge and the Skip review menu item in place. The per-skill 300 s hint cache
is gone: with a catalog view in hand the match costs milliseconds.
2026-09-20 02:25:53 +03:00

174 lines
9 KiB
JavaScript

/**
* OuroborosHub card verdict — the ONE client-side authority joining a hub
* catalog row with the global /api/extensions listing row for the same
* canonical name (plan §7.5, frozen contract).
*
* Pure data-in/data-out: no fetches, no DOM, no version parsing. The only
* comparisons are string inequality on versions and strict equality between
* the local content hash and the publish-receipt hash. The local content hash
* is NEVER compared with the catalog (the payload sidecar is part of the
* hash, so listing-vs-catalog byte equality is structurally false; that
* equality lives server-side in the official_hub review profile and reaches
* this function only as the `official_hub_verified` fact).
*
* @typedef {Object} HubListingRow One /api/extensions skill row projection.
* @property {string} name canonical skill name
* @property {string} source classification tag (self_authored, …)
* @property {string} location physical bucket: external|clawhub|ouroboroshub|native|user_repo|''
* @property {string} version local manifest version
* @property {string} content_hash loader content hash of the local tree
* @property {boolean} official_hub_verified byte-exact match with the hub catalog (server fact; the raw
* listing field is null while the server holds no fresh catalog view — see hubFactsPending)
* @property {Object|null} published publish receipt section (slug, version, content_hash, pr_number, pr_url, …)
* @property {boolean} published_malformed receipt exists on disk but is unreadable (server projects published=null)
* @property {boolean} review_stale
*
* @typedef {Object} HubCatalogRow One /api/marketplace/ouroboroshub/catalog row projection.
* @property {string} slug
* @property {string} sanitized_name server-computed canonical name (JS never sanitizes)
* @property {string} latest_version
* @property {boolean} identity_conflict catalog holds >1 slug with this canonical name
*
* @typedef {Object} HubSyncVerdict
* @property {'install'|'installed'|'update'|'adopt'|'wait_pr'|'none'} action
* @property {Array<'submitted_pr'|'published'|'update_available'|'catalog_unavailable'|'listing_unavailable'|'conflict'>} badges
* @property {{local_version: string, catalog_version: string, receipt_pr: number|null,
* edited_since_submission: boolean, occupying_bucket: string|null,
* no_receipt: boolean, receipt_unreadable: boolean}} copy_facts
* `receipt_unreadable` is the §7.5 "publish record unreadable" copy fact —
* additive beside the frozen keys so malformed-receipt copy stays distinct
* from the no-receipt warning (`no_receipt` is false when the receipt is
* merely unreadable).
*/
/**
* Project one /api/extensions skill row into the §7.5 listing-row shape.
* Prefers a server-provided `location`; otherwise derives the physical bucket
* from `payload_root` (skills/<bucket>/…) and falls back to the source tag
* only for the repo-plane buckets that have no data-plane payload_root.
* @returns {HubListingRow|null}
*/
export function hubListingRowFor(skill) {
if (!skill || typeof skill !== 'object') return null;
return {
name: String(skill.name || ''),
source: String(skill.source || ''),
location: listingLocation(skill),
version: String(skill.version || ''),
content_hash: String(skill.content_hash || ''),
official_hub_verified: skill.official_hub_verified === true,
published: skill.published && typeof skill.published === 'object' ? skill.published : null,
published_malformed: skill.published_malformed === true,
review_stale: skill.review_stale === true,
identity_collision: skill.identity_collision === true,
};
}
/**
* True while a listing carries a hub fact the server could not know yet:
* /api/extensions is a local read that never waits for the hub catalog, so
* `official_hub_verified` is null until a catalog read has landed. The caller
* re-reads the listing once after its own catalog read settles.
*/
export function hubFactsPending(skills) {
return Array.isArray(skills) && skills.some((skill) => skill?.official_hub_verified === null);
}
function listingLocation(skill) {
const explicit = String(skill.location || '');
if (explicit) return explicit;
const bucket = /^skills\/(external|clawhub|ouroboroshub|native)\//.exec(String(skill.payload_root || ''));
if (bucket) return bucket[1];
const source = String(skill.source || '').toLowerCase();
if (source === 'native' || source === 'user_repo') return source;
return '';
}
/**
* Compute the card verdict for one (listing row, catalog row) pair.
* Rules verbatim from plan §7.5; §7 wins over every earlier draft.
*
* @param {HubListingRow|null} listingRow local occupant of the canonical name, or null
* @param {HubCatalogRow|null} catalogRow catalog entry for the slug, or null (slug absent)
* @param {{catalogUnavailable?: boolean, listingUnavailable?: boolean}} [flags]
* @returns {HubSyncVerdict}
*/
export function hubSyncVerdict(listingRow, catalogRow, flags = {}) {
const catalogUnavailable = flags.catalogUnavailable === true;
const listingUnavailable = flags.listingUnavailable === true;
// A failed listing fetch means no local fact may be claimed at all.
const listing = listingUnavailable ? null : (listingRow || null);
const catalog = catalogUnavailable ? null : (catalogRow || null);
// Conflict is fail-closed from EITHER plane: a catalog whose slugs collide
// on one canonical name, or a local listing row the loader marked as an
// identity collision (several same-name occupants — no affordance may act
// on an ambiguous identity).
const conflict = Boolean(
(catalog && catalog.identity_conflict === true)
|| (listing && listing.identity_collision === true),
);
const published = listing && listing.published && typeof listing.published === 'object'
? listing.published
: null;
const location = listing ? String(listing.location || '') : '';
const localVersion = listing ? String(listing.version || '') : '';
const catalogVersion = catalog ? String(catalog.latest_version || '') : '';
const publishedVersion = published ? String(published.version || '') : '';
const receiptHashMatches = Boolean(published
&& String(listing.content_hash || '') === String(published.content_hash || ''));
const copy_facts = {
local_version: localVersion,
catalog_version: catalogVersion,
receipt_pr: published && typeof published.pr_number === 'number' ? published.pr_number : null,
edited_since_submission: Boolean(published && !receiptHashMatches),
occupying_bucket: listing && location && location !== 'ouroboroshub' ? location : null,
no_receipt: Boolean(listing && !published && listing.published_malformed !== true),
receipt_unreadable: Boolean(listing && listing.published_malformed === true),
};
let action = 'none';
if (!listingUnavailable && !conflict) {
if (!listing) {
// No local occupant → Install (only from a live catalog row).
if (catalog) action = 'install';
} else if (location === 'ouroboroshub') {
// Hub bucket: Installed, or Update when the live catalog version
// differs (string inequality only — no ordering semantics).
action = catalog && catalogVersion !== localVersion ? 'update' : 'installed';
} else if (location === 'external') {
// wait_pr preempts Adopt: the local bytes ARE the submitted bytes
// and the catalog does not serve that submitted version yet —
// never offer adopting the older catalog back over the submission.
if (!catalogUnavailable && published && receiptHashMatches
&& catalogVersion !== publishedVersion) {
action = 'wait_pr';
} else if (catalog) {
action = 'adopt';
}
}
// clawhub (v1 unsupported), native, user_repo, unknown → 'none'.
}
const badges = [];
if (!listingUnavailable && !conflict && !catalogUnavailable && published
&& (!catalog || catalogVersion !== publishedVersion)) {
// Receipt exists and the catalog does not confirm the published version
// (slug absent, or a different served version) → "Submitted PR #N".
badges.push('submitted_pr');
}
if (listing && location === 'ouroboroshub' && listing.official_hub_verified === true) {
// "Published vX" rides ONLY on the server's byte-exact verification.
badges.push('published');
}
if (!conflict && listing && location === 'ouroboroshub' && catalog
&& catalogVersion !== localVersion) {
badges.push('update_available');
}
if (catalogUnavailable) badges.push('catalog_unavailable');
if (listingUnavailable) badges.push('listing_unavailable');
if (conflict) badges.push('conflict');
return { action, badges, copy_facts };
}