Files
clawhub/server/skillsShMirrorClassification.ts
Patrick Erichsen 0f84533e9c feat: add permanent skills.sh mirror storage (#3227)
* feat: add staged skills.sh mirror storage

* ci: allow guarded CLAW-563 Test deploy

* ci: expose guarded Test deploy diagnostics

* ci: defer branch guard to deploy step

* ci: deploy CLAW-563 PR head to Test

* ci: admit CLAW-563 PR Test job

* fix: make mirror source recovery durable

* ci: trigger labeled mirror load

* feat: activate mirror search queries

* fix: tighten mirror source typing

* fix: bypass protected Test mirror proof

* feat: attribute skill metrics by source

* feat: present stars as bookmarks

* style: format mirror proof changes

* fix: bypass protected mirror readback

* fix: resume mirror past missing scanner pages

* fix: fetch skills.sh mirror audits from api

* fix: validate structural skills.sh identities

* fix: resolve ambiguous skills.sh mirror identities

* feat: stabilize skills.sh mirror ingestion

* fix: account mirror identity conflicts in proof

* fix: quarantine invalid skills.sh detail ids

* fix: resume skills.sh mirror proof

* fix: preserve skills.sh mirror provenance

* fix: recover exact skills.sh mirror runs

* fix: recover stale skills.sh mirror runs

* fix: normalize skills.sh mirror topic facets

* feat: prove complete skills.sh leaderboard mirror

* fix: canonicalize skills.sh source page hashes

* test: enable skills.sh rollout in mirror tests

* ci: skip unrelated Test deploy pull requests

* fix: preserve Vercel preview marker in Test deploy

* fix: tighten Test deploy and metric reconciliation

* fix: bound mirror detail proof pages

* fix: delegate controlled mirror rate limits

* fix: preserve mirror reconciliation progress

* fix: release mirror retry responses

* fix: preserve stale mirror replay state

* fix: authenticate mirror source starts

* fix: delegate mirror identity rate limits

* ci: trigger mirror proof when labeled

* ci: couple mirror deploy and proof opt-in

* fix: admit permanent Vercel Test runtime

* fix: pass Test target to Vercel runtime

* test: align bookmark sync browser labels

* fix: preserve skills.sh source accounting

* fix: preflight active mirror runs

* fix: bind mirror snapshot accounting

* fix: reject truncated replay hashes

* fix: preserve live mirror overlay metadata
2026-07-24 14:32:00 -05:00

265 lines
8.8 KiB
TypeScript

import { createHash } from "node:crypto";
import {
CLASSIFIER_VERSION,
TOPIC_CLASSIFIER_VERSION,
classifySkill,
} from "../convex/lib/catalogClassifier.mjs";
const MAX_CLASSIFICATION_TEXT_LENGTH = 40_000;
type ClassificationConfidence = "high" | "medium" | "low";
type MirrorClassification = {
inferredCategories: string[];
inferredTopics: string[];
inferredCategoryConfidence: ClassificationConfidence;
inferredTopicConfidence: ClassificationConfidence;
inferredClassifierVersion: string;
inferredTopicClassifierVersion: string;
inferredInputHash: string;
inferredTopicInputHash: string;
inferredAt: number;
};
export type SkillsShMirrorClassificationState = MirrorClassification & {
externalId: string;
slug: string;
displayName: string;
sourceContentHash?: string;
};
type ClassifiableMirrorRow = {
quarantined?: never;
externalId: string;
slug: string;
displayName: string;
sourceContentHash?: string;
detail?: { content: string };
};
type QuarantinedMirrorRow = {
quarantined: true;
externalId: string;
};
type MirrorReplayPair = {
digest: {
externalId: string;
sourceType: "github" | "well-known";
upstreamSourceType?: string;
owner?: string;
repo?: string;
sourceHost?: string;
slug: string;
displayName: string;
sourceUrl: string;
canonicalRepoUrl?: string;
githubPath?: string;
githubCommit?: string;
sourceContentHash?: string;
upstreamInstalls: number;
upstreamScanners: {
genAgentTrustHub: { status: string; sourceCheckedAt?: string; sourceUrl?: string };
socket: { status: string; sourceCheckedAt?: string; sourceUrl?: string };
snyk: { status: string; sourceCheckedAt?: string; sourceUrl?: string };
};
inferredCategories?: string[];
inferredTopics?: string[];
inferredCategoryConfidence?: ClassificationConfidence;
inferredTopicConfidence?: ClassificationConfidence;
inferredClassifierVersion?: string;
inferredTopicClassifierVersion?: string;
inferredInputHash?: string;
inferredTopicInputHash?: string;
inferredAt?: number;
};
detail: {
contentKind: "skill-md" | "readme";
path: string;
content: string;
contentBytes: number;
sourceBytes: number;
sourceFileCount: number;
truncated: boolean;
} | null;
};
type MirrorReplayQuarantine = {
quarantined: true;
externalId: string;
upstreamSourceType: string;
reason: string;
};
type ClassifierOutput = {
categories: string[];
topics: string[];
confidence: ClassificationConfidence;
topicConfidence: ClassificationConfidence;
classifierVersion: string;
topicClassifierVersion: string;
inputHash: string;
topicInputHash: string;
};
type EnrichedMirrorRow<T> = T extends QuarantinedMirrorRow ? T : T & MirrorClassification;
function hasReusableClassification(
row: ClassifiableMirrorRow,
state: SkillsShMirrorClassificationState | undefined,
): state is SkillsShMirrorClassificationState {
return (
state !== undefined &&
state.slug === row.slug &&
state.displayName === row.displayName &&
(row.detail === undefined
? state.inferredInputHash === classificationInputHash(row)
: row.sourceContentHash !== undefined && state.sourceContentHash === row.sourceContentHash) &&
state.inferredClassifierVersion === CLASSIFIER_VERSION &&
state.inferredTopicClassifierVersion === TOPIC_CLASSIFIER_VERSION
);
}
function boundedContentHash(content: string) {
return createHash("sha256").update(content).digest("hex");
}
function classificationText(row: ClassifiableMirrorRow) {
const displayName = row.displayName.replace(/[\r\n]+/g, " ").trim() || row.slug;
const content = row.detail?.content ?? `# ${displayName}`;
return `---\nname: ${displayName}\n---\n${content}`.slice(0, MAX_CLASSIFICATION_TEXT_LENGTH);
}
function classificationInputHash(row: ClassifiableMirrorRow) {
return createHash("sha256")
.update(`${row.slug}\0${classificationText(row)}\0${JSON.stringify([])}`)
.digest("hex");
}
export function enrichSkillsShMirrorClassifications<
T extends ClassifiableMirrorRow | QuarantinedMirrorRow,
>(
rows: T[],
states: SkillsShMirrorClassificationState[],
inferredAt = Date.now(),
classify: (input: { slug?: string; text?: string }) => ClassifierOutput = classifySkill,
): Array<EnrichedMirrorRow<T>> {
const statesByExternalId = new Map(states.map((state) => [state.externalId, state]));
return rows.map((row) => {
if ("quarantined" in row) return row;
const state = statesByExternalId.get(row.externalId);
if (hasReusableClassification(row, state)) {
return {
...row,
inferredCategories: state.inferredCategories,
inferredTopics: state.inferredTopics,
inferredCategoryConfidence: state.inferredCategoryConfidence,
inferredTopicConfidence: state.inferredTopicConfidence,
inferredClassifierVersion: state.inferredClassifierVersion,
inferredTopicClassifierVersion: state.inferredTopicClassifierVersion,
inferredInputHash: state.inferredInputHash,
inferredTopicInputHash: state.inferredTopicInputHash,
inferredAt: state.inferredAt,
};
}
const result = classify({
slug: row.slug,
text: classificationText(row),
});
return {
...row,
inferredCategories: result.categories.length > 0 ? result.categories : ["other"],
inferredTopics: result.topics,
inferredCategoryConfidence: result.confidence,
inferredTopicConfidence: result.topicConfidence,
inferredClassifierVersion: result.classifierVersion,
inferredTopicClassifierVersion: result.topicClassifierVersion,
inferredInputHash: result.inputHash,
inferredTopicInputHash: result.topicInputHash,
inferredAt,
};
}) as Array<EnrichedMirrorRow<T>>;
}
function replayClassificationState(
digest: MirrorReplayPair["digest"],
): SkillsShMirrorClassificationState | null {
if (
!digest.inferredCategories ||
!digest.inferredTopics ||
!digest.inferredCategoryConfidence ||
!digest.inferredTopicConfidence ||
!digest.inferredClassifierVersion ||
!digest.inferredTopicClassifierVersion ||
!digest.inferredInputHash ||
!digest.inferredTopicInputHash ||
digest.inferredAt === undefined
) {
return null;
}
return {
externalId: digest.externalId,
slug: digest.slug,
displayName: digest.displayName,
...(digest.sourceContentHash ? { sourceContentHash: digest.sourceContentHash } : {}),
inferredCategories: digest.inferredCategories,
inferredTopics: digest.inferredTopics,
inferredCategoryConfidence: digest.inferredCategoryConfidence,
inferredTopicConfidence: digest.inferredTopicConfidence,
inferredClassifierVersion: digest.inferredClassifierVersion,
inferredTopicClassifierVersion: digest.inferredTopicClassifierVersion,
inferredInputHash: digest.inferredInputHash,
inferredTopicInputHash: digest.inferredTopicInputHash,
inferredAt: digest.inferredAt,
};
}
export function buildSkillsShMirrorReplayRows(
inputs: Array<MirrorReplayPair | MirrorReplayQuarantine>,
inferredAt = Date.now(),
) {
const pairs = inputs.filter((input): input is MirrorReplayPair => !("quarantined" in input));
const states = pairs.flatMap((pair) => {
const state = replayClassificationState(pair.digest);
return state ? [state] : [];
});
const rows = inputs.map((input) => {
if ("quarantined" in input) return input;
const { digest, detail } = input;
const sourceContentHash =
digest.sourceContentHash ??
(detail && !detail.truncated ? boundedContentHash(detail.content) : undefined);
return {
externalId: digest.externalId,
sourceType: digest.sourceType,
upstreamSourceType: digest.upstreamSourceType ?? digest.sourceType,
...(digest.owner ? { owner: digest.owner } : {}),
...(digest.repo ? { repo: digest.repo } : {}),
...(digest.sourceHost ? { sourceHost: digest.sourceHost } : {}),
slug: digest.slug,
displayName: digest.displayName,
sourceUrl: digest.sourceUrl,
...(digest.canonicalRepoUrl ? { canonicalRepoUrl: digest.canonicalRepoUrl } : {}),
...(digest.githubPath ? { githubPath: digest.githubPath } : {}),
...(digest.githubCommit ? { githubCommit: digest.githubCommit } : {}),
...(sourceContentHash ? { sourceContentHash } : {}),
upstreamInstalls: digest.upstreamInstalls,
upstreamScanners: digest.upstreamScanners,
...(detail
? {
detail: {
contentKind: detail.contentKind,
path: detail.path,
content: detail.content,
contentBytes: detail.contentBytes,
sourceBytes: detail.sourceBytes,
sourceFileCount: detail.sourceFileCount,
truncated: detail.truncated,
},
}
: {}),
};
});
return enrichSkillsShMirrorClassifications(rows, states, inferredAt);
}