mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
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
This commit is contained in:
@@ -5,7 +5,18 @@ on:
|
||||
workflows: ["CI"]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [synchronize, labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
expected_sha:
|
||||
description: Exact branch SHA permitted for a temporary protected Test deploy
|
||||
required: false
|
||||
type: string
|
||||
branch_test_confirm:
|
||||
description: Confirmation phrase for the CLAW-563 branch-only Test deploy
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: deploy-test
|
||||
@@ -18,7 +29,16 @@ jobs:
|
||||
deploy-test:
|
||||
if: >-
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
github.ref == 'refs/heads/main') ||
|
||||
(github.ref == 'refs/heads/main' ||
|
||||
(github.ref == 'refs/heads/pe/claw-563-skills-sh-mirror-10k' &&
|
||||
github.actor == 'Patrick-Erichsen' &&
|
||||
inputs.branch_test_confirm == 'deploy-claw-563-to-permanent-test' &&
|
||||
inputs.expected_sha != ''))) ||
|
||||
(github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.head.ref == 'pe/claw-563-skills-sh-mirror-10k' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.actor == 'Patrick-Erichsen' &&
|
||||
contains(github.event.pull_request.labels.*.name, 'test-mirror-load')) ||
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push')
|
||||
runs-on: ubuntu-latest
|
||||
@@ -30,11 +50,17 @@ jobs:
|
||||
deployment_url: ${{ steps.vercel.outputs.deployment_url }}
|
||||
deploy_sha: ${{ steps.revision.outputs.deploy_sha }}
|
||||
fixture_version: ${{ steps.revision.outputs.fixture_version }}
|
||||
branch_test: ${{ steps.revision.outputs.branch_test }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.ref }}
|
||||
ref: >-
|
||||
${{
|
||||
github.event_name == 'workflow_run' && github.event.workflow_run.head_sha ||
|
||||
github.event_name == 'pull_request' && github.event.pull_request.head.sha ||
|
||||
github.ref
|
||||
}}
|
||||
|
||||
- name: Resolve deployment revision
|
||||
id: revision
|
||||
@@ -44,8 +70,30 @@ jobs:
|
||||
git fetch --no-tags origin main
|
||||
main_sha="$(git rev-parse origin/main)"
|
||||
if [[ "$deploy_sha" != "$main_sha" ]]; then
|
||||
echo "::error::Refusing stale Test deploy: $deploy_sha is not current main $main_sha"
|
||||
exit 1
|
||||
branch_test_allowed=false
|
||||
if [[ "$GITHUB_EVENT_NAME" == workflow_dispatch ]] &&
|
||||
[[ "$GITHUB_REF" == refs/heads/pe/claw-563-skills-sh-mirror-10k ]] &&
|
||||
[[ "$GITHUB_ACTOR" == Patrick-Erichsen ]] &&
|
||||
[[ "${{ inputs.branch_test_confirm }}" == deploy-claw-563-to-permanent-test ]] &&
|
||||
[[ "${{ inputs.expected_sha }}" == "$deploy_sha" ]]
|
||||
then
|
||||
branch_test_allowed=true
|
||||
fi
|
||||
if [[ "$GITHUB_EVENT_NAME" == pull_request ]] &&
|
||||
[[ "$GITHUB_HEAD_REF" == pe/claw-563-skills-sh-mirror-10k ]] &&
|
||||
[[ "$GITHUB_ACTOR" == Patrick-Erichsen ]] &&
|
||||
[[ "${{ github.event.pull_request.head.repo.full_name }}" == "$GITHUB_REPOSITORY" ]] &&
|
||||
[[ "${{ github.event.pull_request.head.sha }}" == "$deploy_sha" ]]
|
||||
then
|
||||
branch_test_allowed=true
|
||||
fi
|
||||
if [[ "$branch_test_allowed" != true ]]; then
|
||||
echo "::error::Refusing non-main Test deploy without the exact CLAW-563 branch guard"
|
||||
exit 1
|
||||
fi
|
||||
echo "branch_test=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "branch_test=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
fixture_version="$(
|
||||
git hash-object convex/devSeed.ts convex/lib/testSeed.ts |
|
||||
@@ -138,6 +186,7 @@ jobs:
|
||||
' <<< "$capabilities"
|
||||
|
||||
- name: Apply additive Test fixtures
|
||||
if: steps.revision.outputs.branch_test != 'true'
|
||||
env:
|
||||
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}
|
||||
CONVEX_DEPLOYMENT: ${{ vars.CONVEX_DEPLOYMENT }}
|
||||
@@ -164,20 +213,22 @@ jobs:
|
||||
--meta githubSha="${{ steps.revision.outputs.deploy_sha }}" \
|
||||
--meta clawhubEnvironment=test \
|
||||
--build-env CONVEX_DEPLOY_KEY= \
|
||||
--build-env VERCEL_ENV=test \
|
||||
--build-env VERCEL_TARGET_ENV=test \
|
||||
--build-env CLAWHUB_ENV=test \
|
||||
--build-env CLAWHUB_SKILLS_SH_ROLLOUT_MODE=test \
|
||||
--build-env CLAWHUB_GITHUB_SKILL_SYNC_ROLLOUT_MODE=test \
|
||||
--build-env CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED=1 \
|
||||
--build-env SITE_URL="$TEST_SITE_URL" \
|
||||
--build-env VITE_CLAWHUB_DEPLOY_ENV=test \
|
||||
--build-env VITE_CONVEX_URL="$VITE_CONVEX_URL" \
|
||||
--build-env VITE_CONVEX_SITE_URL="$VITE_CONVEX_SITE_URL" \
|
||||
--build-env VITE_SITE_URL="$TEST_SITE_URL" \
|
||||
--env CONVEX_DEPLOY_KEY= \
|
||||
--env VERCEL_TARGET_ENV=test \
|
||||
--env CLAWHUB_ENV=test \
|
||||
--env CLAWHUB_SKILLS_SH_ROLLOUT_MODE=test \
|
||||
--env CLAWHUB_GITHUB_SKILL_SYNC_ROLLOUT_MODE=test \
|
||||
--env CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED=1 \
|
||||
--env SITE_URL="$TEST_SITE_URL" \
|
||||
--env VITE_CLAWHUB_DEPLOY_ENV=test \
|
||||
--env VITE_CONVEX_URL="$VITE_CONVEX_URL" \
|
||||
@@ -256,3 +307,173 @@ jobs:
|
||||
echo "- Fixture version: \`${FIXTURE_VERSION:-unresolved}\`"
|
||||
echo "- Result: \`${{ job.status }}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
claw563-mirror-load:
|
||||
needs: deploy-test
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.head.ref == 'pe/claw-563-skills-sh-mirror-10k' &&
|
||||
contains(github.event.pull_request.labels.*.name, 'test-mirror-load')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
environment:
|
||||
name: Test
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Load and prove the authenticated leaderboard mirror foundation
|
||||
env:
|
||||
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}
|
||||
DEPLOY_SHA: ${{ needs.deploy-test.outputs.deploy_sha }}
|
||||
TEST_SITE_URL: ${{ vars.SITE_URL }}
|
||||
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
operator_token=""
|
||||
cleanup() {
|
||||
set +e
|
||||
bunx convex run --no-push skillsShMirror:configureInternal \
|
||||
'{
|
||||
"actor":"CLAW-563 Test workflow",
|
||||
"reason":"CLAW-563 proof cleanup",
|
||||
"confirm":"enable-skills-sh-mirror-test",
|
||||
"enabled":false,
|
||||
"maxRowsPerRun":50000,
|
||||
"maxRowsPerBatch":50,
|
||||
"maxDetailBytes":65536
|
||||
}' > claw563-control-disabled.json 2>&1
|
||||
disable_exit=$?
|
||||
bunx convex run --no-push devSeed:seedCliRoleHelpFixtures '{}' >/dev/null
|
||||
token_rotation_exit=$?
|
||||
if [[ -n "$operator_token" ]]; then
|
||||
revoked_status="$(
|
||||
curl \
|
||||
--silent \
|
||||
--output /dev/null \
|
||||
--write-out '%{http_code}' \
|
||||
--header "Authorization: Bearer $operator_token" \
|
||||
https://academic-chihuahua-392.convex.site/api/v1/operator/skills-sh/catalog-test
|
||||
)"
|
||||
else
|
||||
revoked_status="not-created"
|
||||
fi
|
||||
jq -n \
|
||||
--argjson disableExit "$disable_exit" \
|
||||
--argjson tokenRotationExit "$token_rotation_exit" \
|
||||
--arg revokedStatus "$revoked_status" \
|
||||
'{
|
||||
disableExit:$disableExit,
|
||||
tokenRotationExit:$tokenRotationExit,
|
||||
revokedStatus:$revokedStatus
|
||||
}' > claw563-cleanup.json
|
||||
set -e
|
||||
[[
|
||||
"$disable_exit" -eq 0 &&
|
||||
"$token_rotation_exit" -eq 0 &&
|
||||
"$revoked_status" == "401"
|
||||
]]
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
deployment_json="$(bunx convex run --no-push appMeta:getDeploymentInfo)"
|
||||
build_sha="$(jq -r '.appBuildSha // empty' <<<"$deployment_json")"
|
||||
if [[ "$build_sha" != "$DEPLOY_SHA" ]]; then
|
||||
echo "::error::Unexpected Convex Test build SHA: $build_sha"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
token_seed="$(bunx convex run --no-push devSeed:seedCliRoleHelpFixtures '{}')"
|
||||
operator_token="$(jq -r '.admin.token // empty' <<<"$token_seed")"
|
||||
token_seed=""
|
||||
if [[ -z "$operator_token" ]]; then
|
||||
echo "::error::Test admin fixture token is missing"
|
||||
exit 1
|
||||
fi
|
||||
echo "::add-mask::$operator_token"
|
||||
|
||||
export CLAWHUB_TEST_MIRROR_GATE_URL="$TEST_SITE_URL/ops/skills-sh/mirror-test"
|
||||
export CLAWHUB_TEST_OPERATOR_TOKEN="$operator_token"
|
||||
bun run skills-sh:prove-mirror > claw563-proof-output.json
|
||||
|
||||
curl \
|
||||
--fail-with-body \
|
||||
--silent \
|
||||
--show-error \
|
||||
--request POST \
|
||||
--header "Authorization: Bearer $operator_token" \
|
||||
--header "Content-Type: application/json" \
|
||||
--header "x-vercel-protection-bypass: $VERCEL_AUTOMATION_BYPASS_SECRET" \
|
||||
--data '{"operation":"read","externalId":"patrick-erichsen/skills/html"}' \
|
||||
"$CLAWHUB_TEST_MIRROR_GATE_URL" > claw563-controlled-entry.json
|
||||
jq -e '
|
||||
.digest.externalId == "patrick-erichsen/skills/html" and
|
||||
.digest.sourceType == "github" and
|
||||
.digest.owner == "patrick-erichsen" and
|
||||
.digest.repo == "skills" and
|
||||
.digest.githubPath == "skills/html" and
|
||||
.digest.githubCommit == "050daba89f6b6636470add5cb300aac46a412cf8" and
|
||||
.digest.sourceContentHash == "42d2e89358ea927441dfede45c3b0cf89a21603bc7c32246f098d24a9cbea1ff" and
|
||||
.digest.active == true and
|
||||
.digest.publicVisible == false and
|
||||
.digest.installable == false
|
||||
' claw563-controlled-entry.json >/dev/null
|
||||
|
||||
curl \
|
||||
--fail-with-body \
|
||||
--silent \
|
||||
--show-error \
|
||||
--request POST \
|
||||
--header "Authorization: Bearer $operator_token" \
|
||||
--header "Content-Type: application/json" \
|
||||
--header "x-vercel-protection-bypass: $VERCEL_AUTOMATION_BYPASS_SECRET" \
|
||||
--data '{"operation":"read","externalId":"steipete/clawdis/discrawl"}' \
|
||||
"$CLAWHUB_TEST_MIRROR_GATE_URL" > claw563-discrawl-entry.json
|
||||
jq -e '
|
||||
.digest.externalId == "steipete/clawdis/discrawl" and
|
||||
.digest.sourceType == "github" and
|
||||
.digest.owner == "steipete" and
|
||||
.digest.repo == "clawdis" and
|
||||
.digest.githubPath == ".agents/skills/discrawl" and
|
||||
.digest.githubCommit == "690ed564419291ca6e832dc69b53061300075b62" and
|
||||
.digest.sourceContentHash == "889dc43180b210dbca12f8291e007feb231250ecfdba90c4d3938a18125efb6d" and
|
||||
.digest.active == true and
|
||||
.digest.publicVisible == false and
|
||||
.digest.installable == false
|
||||
' claw563-discrawl-entry.json >/dev/null
|
||||
|
||||
jq -n \
|
||||
--arg sourceSha "$DEPLOY_SHA" \
|
||||
--arg deploymentUrl "$TEST_SITE_URL" \
|
||||
'{
|
||||
sourceSha:$sourceSha,
|
||||
deploymentUrl:$deploymentUrl,
|
||||
convexDeployment:"academic-chihuahua-392"
|
||||
}' > claw563-deployment.json
|
||||
|
||||
cleanup
|
||||
trap - EXIT
|
||||
jq -e '
|
||||
.disableExit == 0 and
|
||||
.tokenRotationExit == 0 and
|
||||
.revokedStatus == "401"
|
||||
' claw563-cleanup.json >/dev/null
|
||||
|
||||
- name: Upload permanent Test mirror proof
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: claw563-mirror-proof
|
||||
if-no-files-found: warn
|
||||
path: |
|
||||
proof/claw-563/skills-sh-mirror-test-proof.json
|
||||
claw563-proof-output.json
|
||||
claw563-controlled-entry.json
|
||||
claw563-discrawl-entry.json
|
||||
claw563-control-disabled.json
|
||||
claw563-cleanup.json
|
||||
claw563-deployment.json
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"mermaid": "^11.16.0",
|
||||
"mime": "4.1.0",
|
||||
"monaco-editor": "0.56.0",
|
||||
"parse5": "8.0.1",
|
||||
"pino": "10.3.1",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7",
|
||||
|
||||
Vendored
+2
@@ -173,6 +173,7 @@ import type * as skillStatEvents from "../skillStatEvents.js";
|
||||
import type * as skillTransfers from "../skillTransfers.js";
|
||||
import type * as skills from "../skills.js";
|
||||
import type * as skillsShCatalog from "../skillsShCatalog.js";
|
||||
import type * as skillsShMirror from "../skillsShMirror.js";
|
||||
import type * as stars from "../stars.js";
|
||||
import type * as statsMaintenance from "../statsMaintenance.js";
|
||||
import type * as telemetry from "../telemetry.js";
|
||||
@@ -354,6 +355,7 @@ declare const fullApi: ApiFromModules<{
|
||||
skillTransfers: typeof skillTransfers;
|
||||
skills: typeof skills;
|
||||
skillsShCatalog: typeof skillsShCatalog;
|
||||
skillsShMirror: typeof skillsShMirror;
|
||||
stars: typeof stars;
|
||||
statsMaintenance: typeof statsMaintenance;
|
||||
telemetry: typeof telemetry;
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import {
|
||||
} from "./lib/downloadTrend";
|
||||
import { normalizePackageName } from "./lib/packageRegistry";
|
||||
import { canAccessPublisherOwnerScope } from "./lib/publishers";
|
||||
import { readCanonicalStat } from "./lib/skillStats";
|
||||
import { readPublicDownloads } from "./lib/skillStats";
|
||||
|
||||
const dashboardMetricSelectionValidator = v.union(
|
||||
v.object({ kind: v.literal("skill"), slug: v.string() }),
|
||||
@@ -46,7 +46,7 @@ async function aggregateSkillDownloads(ctx: QueryCtx, skills: Doc<"skills">[], e
|
||||
);
|
||||
for (const trend of trends) addPoints(points, trend);
|
||||
return {
|
||||
allTimeDownloads: skills.reduce((sum, skill) => sum + readCanonicalStat(skill, "downloads"), 0),
|
||||
allTimeDownloads: skills.reduce((sum, skill) => sum + readPublicDownloads(skill), 0),
|
||||
points,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2400,6 +2400,8 @@ description: Build HTML artifacts.
|
||||
statsStars: 3,
|
||||
statsInstallsCurrent: 2,
|
||||
statsInstallsAllTime: 5,
|
||||
statsSkillsShInstalls: 17,
|
||||
statsGithubStars: 321,
|
||||
stats: { downloads: 7, stars: 3, installsCurrent: 2, installsAllTime: 5, versions: 0 },
|
||||
createdAt: 1,
|
||||
updatedAt: 60,
|
||||
@@ -2436,6 +2438,16 @@ description: Build HTML artifacts.
|
||||
moderationReason: "pending.scan",
|
||||
statsDownloads: 7,
|
||||
statsStars: 3,
|
||||
statsInstallsCurrent: 2,
|
||||
statsInstallsAllTime: 5,
|
||||
statsSkillsShInstalls: 17,
|
||||
statsGithubStars: 321,
|
||||
stats: {
|
||||
downloads: 7,
|
||||
stars: 3,
|
||||
installsCurrent: 2,
|
||||
installsAllTime: 5,
|
||||
},
|
||||
});
|
||||
expect(tables.skills[0]).not.toHaveProperty("githubRemovedAt");
|
||||
expect(tables.skills[0]).not.toHaveProperty("softDeletedAt");
|
||||
|
||||
@@ -1922,8 +1922,7 @@ describe("httpApiV1 handlers", () => {
|
||||
displayName: "Demo",
|
||||
summary: "Summary",
|
||||
updatedAt: 1,
|
||||
statsDownloads: 42,
|
||||
stats: { downloads: 1 },
|
||||
stats: { downloads: 50 },
|
||||
},
|
||||
version: { version: "1.0.0" },
|
||||
ownerHandle: "openclaw",
|
||||
@@ -1951,7 +1950,7 @@ describe("httpApiV1 handlers", () => {
|
||||
displayName: "Demo",
|
||||
summary: "Summary",
|
||||
version: "1.0.0",
|
||||
downloads: 42,
|
||||
downloads: 50,
|
||||
updatedAt: 1,
|
||||
ownerHandle: "openclaw",
|
||||
owner: {
|
||||
|
||||
@@ -235,6 +235,405 @@ describe("skills.sh catalog Test HTTP API", () => {
|
||||
expect(buildGitHubApiHeaders).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes mirror batches through the scanless mirror mutation", async () => {
|
||||
const runMutation = vi.fn(async (_ref, args: Record<string, unknown>) => ({
|
||||
status: "running",
|
||||
page: args.page,
|
||||
offset: 1,
|
||||
}));
|
||||
const ctx = {
|
||||
runQuery: vi.fn(async () => ({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})),
|
||||
runMutation,
|
||||
} as never;
|
||||
const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "mirror-batch",
|
||||
runId: "skillsShMirrorRuns:test",
|
||||
leaseToken: "lease:test",
|
||||
page: 0,
|
||||
offset: 0,
|
||||
pageLength: 500,
|
||||
hasMore: true,
|
||||
sourceTotal: 9_571,
|
||||
sourceRequests: 2,
|
||||
sourceBytes: 1_024,
|
||||
rows: [{ externalId: "vercel-labs/skills/find-skills" }],
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await skillsShCatalogTestV1Handler(ctx, request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(runMutation).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
runId: "skillsShMirrorRuns:test",
|
||||
leaseToken: "lease:test",
|
||||
page: 0,
|
||||
offset: 0,
|
||||
sourceTotal: 9_571,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("stores and summarizes immutable mirror source pages", async () => {
|
||||
const staging = {
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
};
|
||||
const rows = [
|
||||
{
|
||||
id: "vercel-labs/skills/find-skills",
|
||||
installUrl: "https://github.com/vercel-labs/skills",
|
||||
installs: 42,
|
||||
name: "Find Skills",
|
||||
slug: "find-skills",
|
||||
source: "vercel-labs/skills",
|
||||
sourceType: "github",
|
||||
url: "https://skills.sh/vercel-labs/skills/find-skills",
|
||||
},
|
||||
];
|
||||
const runMutation = vi.fn(async () => ({ stored: true, page: 0, rows: 1 }));
|
||||
const storeCtx = { runQuery: vi.fn(async () => staging), runMutation } as never;
|
||||
const storeResponse = await skillsShCatalogTestV1Handler(
|
||||
storeCtx,
|
||||
new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "mirror-source-page-store",
|
||||
snapshotHash: "a".repeat(64),
|
||||
page: 0,
|
||||
sourceTotal: 1,
|
||||
pageLength: 1,
|
||||
hasMore: false,
|
||||
identityHash: "b".repeat(64),
|
||||
contentHash: "c".repeat(64),
|
||||
sourceBytes: 512,
|
||||
serializedBytes: 768,
|
||||
rows,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(storeResponse.status).toBe(200);
|
||||
expect(runMutation).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
snapshotHash: "a".repeat(64),
|
||||
page: 0,
|
||||
rows,
|
||||
}),
|
||||
);
|
||||
|
||||
const summary = {
|
||||
snapshotHash: "a".repeat(64),
|
||||
pageDocuments: 1,
|
||||
rows: 1,
|
||||
sourceBytes: 512,
|
||||
serializedBytes: 768,
|
||||
};
|
||||
const runQuery = vi.fn().mockResolvedValueOnce(staging).mockResolvedValueOnce(summary);
|
||||
const summaryResponse = await skillsShCatalogTestV1Handler(
|
||||
{ runQuery } as never,
|
||||
new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "mirror-source-summary",
|
||||
snapshotHash: "a".repeat(64),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(summaryResponse.status).toBe(200);
|
||||
expect(await summaryResponse.json()).toEqual(summary);
|
||||
expect(runQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("routes guarded mirror batch lease claims and releases", async () => {
|
||||
const runMutation = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
runId: "skillsShMirrorRuns:test",
|
||||
page: 3,
|
||||
offset: 50,
|
||||
leaseToken: "lease:test",
|
||||
leaseExpiresAt: Date.now() + 300_000,
|
||||
})
|
||||
.mockResolvedValueOnce({ released: true });
|
||||
const ctx = {
|
||||
runQuery: vi.fn(async () => ({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})),
|
||||
runMutation,
|
||||
} as never;
|
||||
|
||||
for (const operation of ["mirror-batch-claim", "mirror-batch-release"] as const) {
|
||||
const response = await skillsShCatalogTestV1Handler(
|
||||
ctx,
|
||||
new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation,
|
||||
runId: "skillsShMirrorRuns:test",
|
||||
page: 3,
|
||||
offset: 50,
|
||||
leaseToken: "lease:test",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(response.status).toBe(200);
|
||||
}
|
||||
|
||||
expect(runMutation).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
runId: "skillsShMirrorRuns:test",
|
||||
page: 3,
|
||||
offset: 50,
|
||||
leaseToken: "lease:test",
|
||||
}),
|
||||
);
|
||||
expect(runMutation).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
runId: "skillsShMirrorRuns:test",
|
||||
page: 3,
|
||||
offset: 50,
|
||||
leaseToken: "lease:test",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reads one mirror run for source-fetch preflight", async () => {
|
||||
const runQuery = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
runId: "skillsShMirrorRuns:test",
|
||||
status: "paused",
|
||||
page: 3,
|
||||
offset: 50,
|
||||
});
|
||||
const ctx = { runQuery } as never;
|
||||
const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "mirror-run",
|
||||
runId: "skillsShMirrorRuns:test",
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await skillsShCatalogTestV1Handler(ctx, request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toMatchObject({
|
||||
status: "paused",
|
||||
page: 3,
|
||||
offset: 50,
|
||||
});
|
||||
expect(runQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("cancels one stale active mirror run with explicit confirmation", async () => {
|
||||
const runQuery = vi.fn().mockResolvedValue({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
});
|
||||
const runMutation = vi.fn().mockResolvedValue({
|
||||
runId: "skillsShMirrorRuns:stale",
|
||||
status: "canceled",
|
||||
});
|
||||
const ctx = { runQuery, runMutation } as never;
|
||||
const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "mirror-cancel",
|
||||
runId: "skillsShMirrorRuns:stale",
|
||||
reason: "discard stale captured recovery",
|
||||
confirm: "cancel-skills-sh-mirror-test-run",
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await skillsShCatalogTestV1Handler(ctx, request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual({
|
||||
runId: "skillsShMirrorRuns:stale",
|
||||
status: "canceled",
|
||||
});
|
||||
expect(runMutation).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
runId: "skillsShMirrorRuns:stale",
|
||||
reason: "discard stale captured recovery",
|
||||
confirm: "cancel-skills-sh-mirror-test-run",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reads bounded mirror conflicts for an exact completed run", async () => {
|
||||
const runQuery = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
runId: "skillsShMirrorRuns:live",
|
||||
externalId: "larksuite/cli/lark-doc",
|
||||
kind: "source-quarantine",
|
||||
},
|
||||
]);
|
||||
const ctx = { runQuery } as never;
|
||||
const response = await skillsShCatalogTestV1Handler(
|
||||
ctx,
|
||||
new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "mirror-conflicts",
|
||||
runId: "skillsShMirrorRuns:live",
|
||||
limit: 50,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
runId: "skillsShMirrorRuns:live",
|
||||
externalId: "larksuite/cli/lark-doc",
|
||||
kind: "source-quarantine",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(runQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("reads bounded mirror classification reuse state", async () => {
|
||||
const runQuery = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
externalId: "patrick-erichsen/skills/html",
|
||||
inferredClassifierVersion: "taxonomy-prototype-v9",
|
||||
},
|
||||
]);
|
||||
const ctx = { runQuery } as never;
|
||||
const response = await skillsShCatalogTestV1Handler(
|
||||
ctx,
|
||||
new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "mirror-classification-states",
|
||||
externalIds: ["patrick-erichsen/skills/html"],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toMatchObject({
|
||||
states: [{ externalId: "patrick-erichsen/skills/html" }],
|
||||
});
|
||||
expect(runQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("reads a bounded mirror facet proof page", async () => {
|
||||
const runQuery = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
page: [{ kind: "category", term: "development" }],
|
||||
isDone: true,
|
||||
continueCursor: "",
|
||||
});
|
||||
const ctx = { runQuery } as never;
|
||||
const response = await skillsShCatalogTestV1Handler(
|
||||
ctx,
|
||||
new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "mirror-facet-page",
|
||||
cursor: null,
|
||||
limit: 500,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toMatchObject({
|
||||
page: [{ kind: "category", term: "development" }],
|
||||
isDone: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("reads bounded captured mirror rows for replay", async () => {
|
||||
const runQuery = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
digest: { externalId: "patrick-erichsen/skills/html", active: true },
|
||||
detail: null,
|
||||
},
|
||||
]);
|
||||
const ctx = { runQuery } as never;
|
||||
const response = await skillsShCatalogTestV1Handler(
|
||||
ctx,
|
||||
new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "mirror-replay-rows",
|
||||
externalIds: ["patrick-erichsen/skills/html"],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toMatchObject({
|
||||
rows: [{ digest: { externalId: "patrick-erichsen/skills/html" }, detail: null }],
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches only owners missing from authenticated staging-live state", async () => {
|
||||
const githubFetch = vi.fn(async (url: string, init?: RequestInit) => {
|
||||
expect(init?.headers).toMatchObject({ Authorization: "Bearer placeholder" });
|
||||
|
||||
@@ -28,6 +28,29 @@ const internalRefs = internal as unknown as {
|
||||
startFixtureRunInternal: unknown;
|
||||
startStagingLiveRunInternal: unknown;
|
||||
};
|
||||
skillsShMirror: {
|
||||
cancelRunInternal: unknown;
|
||||
claimBatchLeaseInternal: unknown;
|
||||
configureInternal: unknown;
|
||||
getByExternalIdInternal: unknown;
|
||||
getClassificationStatesInternal: unknown;
|
||||
getDetailByExternalIdInternal: unknown;
|
||||
getIsolationInternal: unknown;
|
||||
getReplayRowsInternal: unknown;
|
||||
getRunInternal: unknown;
|
||||
getSourceCaptureSummaryInternal: unknown;
|
||||
getStatusInternal: unknown;
|
||||
listDetailsPageInternal: unknown;
|
||||
listDigestsPageInternal: unknown;
|
||||
listConflictsByRunInternal: unknown;
|
||||
listFacetsPageInternal: unknown;
|
||||
processBatchInternal: unknown;
|
||||
reconcileBatchInternal: unknown;
|
||||
releaseBatchLeaseInternal: unknown;
|
||||
setPausedInternal: unknown;
|
||||
startRunInternal: unknown;
|
||||
storeSourcePageInternal: unknown;
|
||||
};
|
||||
};
|
||||
const MAX_GITHUB_OWNER_RESOLUTIONS = 500;
|
||||
const GITHUB_OWNER_RESOLUTION_CONCURRENCY = 8;
|
||||
@@ -91,6 +114,19 @@ function requireBoolean(record: Record<string, unknown>, key: string) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function requireStringArray(record: Record<string, unknown>, key: string, maxItems: number) {
|
||||
const value = record[key];
|
||||
if (
|
||||
!Array.isArray(value) ||
|
||||
value.length < 1 ||
|
||||
value.length > maxItems ||
|
||||
value.some((item) => typeof item !== "string" || !item.trim())
|
||||
) {
|
||||
throw new Error(`${key} must contain between 1 and ${maxItems} strings`);
|
||||
}
|
||||
return value as string[];
|
||||
}
|
||||
|
||||
async function sha256Hex(bytes: Uint8Array) {
|
||||
const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes).buffer);
|
||||
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
@@ -426,6 +462,250 @@ export async function skillsShCatalogTestV1Handler(ctx: ActionCtx, request: Requ
|
||||
const body = asRecord(await request.json());
|
||||
if (!body) return text("Invalid JSON", 400, rate.headers);
|
||||
const operation = requireString(body, "operation");
|
||||
if (operation === "mirror-status") {
|
||||
return json(
|
||||
await runQueryRef(ctx, internalRefs.skillsShMirror.getStatusInternal, {}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-isolation") {
|
||||
return json(
|
||||
await runQueryRef(ctx, internalRefs.skillsShMirror.getIsolationInternal, {}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-run") {
|
||||
return json(
|
||||
await runQueryRef(ctx, internalRefs.skillsShMirror.getRunInternal, {
|
||||
runId: requireString(body, "runId"),
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-conflicts") {
|
||||
const conflicts = await runQueryRef(
|
||||
ctx,
|
||||
internalRefs.skillsShMirror.listConflictsByRunInternal,
|
||||
{
|
||||
runId: requireString(body, "runId"),
|
||||
limit: requireNumber(body, "limit"),
|
||||
},
|
||||
);
|
||||
return json({ conflicts }, 200, rate.headers);
|
||||
}
|
||||
if (operation === "mirror-read") {
|
||||
const externalId = requireString(body, "externalId");
|
||||
const [digest, detail] = await Promise.all([
|
||||
runQueryRef(ctx, internalRefs.skillsShMirror.getByExternalIdInternal, { externalId }),
|
||||
runQueryRef(ctx, internalRefs.skillsShMirror.getDetailByExternalIdInternal, {
|
||||
externalId,
|
||||
}),
|
||||
]);
|
||||
return json({ digest, detail }, 200, rate.headers);
|
||||
}
|
||||
if (operation === "mirror-classification-states") {
|
||||
const states = await runQueryRef(
|
||||
ctx,
|
||||
internalRefs.skillsShMirror.getClassificationStatesInternal,
|
||||
{
|
||||
externalIds: requireStringArray(body, "externalIds", 50),
|
||||
},
|
||||
);
|
||||
return json({ states }, 200, rate.headers);
|
||||
}
|
||||
if (operation === "mirror-replay-rows") {
|
||||
const rows = await runQueryRef(ctx, internalRefs.skillsShMirror.getReplayRowsInternal, {
|
||||
externalIds: requireStringArray(body, "externalIds", 50),
|
||||
});
|
||||
return json({ rows }, 200, rate.headers);
|
||||
}
|
||||
if (operation === "mirror-source-summary") {
|
||||
return json(
|
||||
await runQueryRef(ctx, internalRefs.skillsShMirror.getSourceCaptureSummaryInternal, {
|
||||
snapshotHash: requireString(body, "snapshotHash"),
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-page") {
|
||||
const cursor =
|
||||
body.cursor === null || typeof body.cursor === "string"
|
||||
? body.cursor
|
||||
: (() => {
|
||||
throw new Error("cursor is required");
|
||||
})();
|
||||
return json(
|
||||
await runQueryRef(ctx, internalRefs.skillsShMirror.listDigestsPageInternal, {
|
||||
cursor,
|
||||
limit: requireNumber(body, "limit"),
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-detail-page") {
|
||||
const cursor =
|
||||
body.cursor === null || typeof body.cursor === "string"
|
||||
? body.cursor
|
||||
: (() => {
|
||||
throw new Error("cursor is required");
|
||||
})();
|
||||
return json(
|
||||
await runQueryRef(ctx, internalRefs.skillsShMirror.listDetailsPageInternal, {
|
||||
cursor,
|
||||
limit: requireNumber(body, "limit"),
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-facet-page") {
|
||||
const cursor =
|
||||
body.cursor === null || typeof body.cursor === "string"
|
||||
? body.cursor
|
||||
: (() => {
|
||||
throw new Error("cursor is required");
|
||||
})();
|
||||
return json(
|
||||
await runQueryRef(ctx, internalRefs.skillsShMirror.listFacetsPageInternal, {
|
||||
cursor,
|
||||
limit: requireNumber(body, "limit"),
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-configure") {
|
||||
return json(
|
||||
await runMutationRef(ctx, internalRefs.skillsShMirror.configureInternal, {
|
||||
actor: auth.user.handle,
|
||||
reason: requireString(body, "reason"),
|
||||
confirm: requireString(body, "confirm"),
|
||||
enabled: requireBoolean(body, "enabled"),
|
||||
maxRowsPerRun: requireNumber(body, "maxRowsPerRun"),
|
||||
maxRowsPerBatch: requireNumber(body, "maxRowsPerBatch"),
|
||||
maxDetailBytes: requireNumber(body, "maxDetailBytes"),
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-start") {
|
||||
return json(
|
||||
await runMutationRef(ctx, internalRefs.skillsShMirror.startRunInternal, {
|
||||
actor: auth.user.handle,
|
||||
reason: requireString(body, "reason"),
|
||||
snapshotId: requireString(body, "snapshotId"),
|
||||
...(typeof body.sourceSnapshotHash === "string"
|
||||
? { sourceSnapshotHash: requireString(body, "sourceSnapshotHash") }
|
||||
: {}),
|
||||
...(typeof body.sourceCaptureWrites === "number"
|
||||
? { sourceCaptureWrites: requireNumber(body, "sourceCaptureWrites") }
|
||||
: {}),
|
||||
sourceTotal: requireNumber(body, "sourceTotal"),
|
||||
sourcePageSize: requireNumber(body, "sourcePageSize"),
|
||||
sourceMeasuredAt: requireString(body, "sourceMeasuredAt"),
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-source-page-store") {
|
||||
if (!Array.isArray(body.rows)) throw new Error("rows is required");
|
||||
return json(
|
||||
await runMutationRef(ctx, internalRefs.skillsShMirror.storeSourcePageInternal, {
|
||||
snapshotHash: requireString(body, "snapshotHash"),
|
||||
page: requireNumber(body, "page"),
|
||||
sourceTotal: requireNumber(body, "sourceTotal"),
|
||||
pageLength: requireNumber(body, "pageLength"),
|
||||
hasMore: requireBoolean(body, "hasMore"),
|
||||
identityHash: requireString(body, "identityHash"),
|
||||
contentHash: requireString(body, "contentHash"),
|
||||
sourceBytes: requireNumber(body, "sourceBytes"),
|
||||
serializedBytes: requireNumber(body, "serializedBytes"),
|
||||
rows: body.rows,
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-batch") {
|
||||
if (!Array.isArray(body.rows)) throw new Error("rows is required");
|
||||
return json(
|
||||
await runMutationRef(ctx, internalRefs.skillsShMirror.processBatchInternal, {
|
||||
runId: requireString(body, "runId"),
|
||||
leaseToken: requireString(body, "leaseToken"),
|
||||
page: requireNumber(body, "page"),
|
||||
offset: requireNumber(body, "offset"),
|
||||
pageLength: requireNumber(body, "pageLength"),
|
||||
hasMore: requireBoolean(body, "hasMore"),
|
||||
sourceTotal: requireNumber(body, "sourceTotal"),
|
||||
sourceRequests: requireNumber(body, "sourceRequests"),
|
||||
sourceBytes: requireNumber(body, "sourceBytes"),
|
||||
rows: body.rows,
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-batch-claim" || operation === "mirror-batch-release") {
|
||||
const args = {
|
||||
runId: requireString(body, "runId"),
|
||||
page: requireNumber(body, "page"),
|
||||
offset: requireNumber(body, "offset"),
|
||||
leaseToken: requireString(body, "leaseToken"),
|
||||
};
|
||||
return json(
|
||||
await runMutationRef(
|
||||
ctx,
|
||||
operation === "mirror-batch-claim"
|
||||
? internalRefs.skillsShMirror.claimBatchLeaseInternal
|
||||
: internalRefs.skillsShMirror.releaseBatchLeaseInternal,
|
||||
args,
|
||||
),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-pause") {
|
||||
return json(
|
||||
await runMutationRef(ctx, internalRefs.skillsShMirror.setPausedInternal, {
|
||||
runId: requireString(body, "runId"),
|
||||
paused: requireBoolean(body, "paused"),
|
||||
actor: auth.user.handle,
|
||||
reason: requireString(body, "reason"),
|
||||
confirm: requireString(body, "confirm"),
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-cancel") {
|
||||
return json(
|
||||
await runMutationRef(ctx, internalRefs.skillsShMirror.cancelRunInternal, {
|
||||
runId: requireString(body, "runId"),
|
||||
actor: auth.user.handle,
|
||||
reason: requireString(body, "reason"),
|
||||
confirm: requireString(body, "confirm"),
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-reconcile") {
|
||||
return json(
|
||||
await runMutationRef(ctx, internalRefs.skillsShMirror.reconcileBatchInternal, {
|
||||
runId: requireString(body, "runId"),
|
||||
limit: requireNumber(body, "limit"),
|
||||
}),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "verify-canary") {
|
||||
return json(await verifyControlledCanaryGitHubSource({}), 200, rate.headers);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ import {
|
||||
getSkillFileModerationInfoFromSkill,
|
||||
isSkillVersionForSkill,
|
||||
} from "../lib/skillFileAccess";
|
||||
import { readCanonicalStat } from "../lib/skillStats";
|
||||
import {
|
||||
buildDeterministicZip,
|
||||
buildMergedExportZip,
|
||||
@@ -101,7 +100,6 @@ type SearchSkillEntry = {
|
||||
stars?: number;
|
||||
installs?: number;
|
||||
};
|
||||
statsDownloads?: number;
|
||||
} | null;
|
||||
version: { version?: string; createdAt?: number } | null;
|
||||
ownerHandle?: string | null;
|
||||
@@ -1394,7 +1392,9 @@ export async function searchSkillsV1Handler(ctx: ActionCtx, request: Request) {
|
||||
displayName: result.skill?.displayName,
|
||||
summary: result.skill?.summary ?? null,
|
||||
version: result.version?.version ?? null,
|
||||
downloads: result.skill ? readCanonicalStat(result.skill, "downloads") : 0,
|
||||
// searchSkills already returns the ordinary public skill shape, including
|
||||
// the combined presentation value and no source-attribution fields.
|
||||
downloads: result.skill?.stats.downloads ?? 0,
|
||||
updatedAt: result.skill?.updatedAt,
|
||||
ownerHandle: result.ownerHandle ?? owner?.handle ?? null,
|
||||
owner,
|
||||
|
||||
@@ -51,13 +51,15 @@ describe("public skill mapping", () => {
|
||||
statsStars: 3,
|
||||
statsInstallsCurrent: 5,
|
||||
statsInstallsAllTime: 7,
|
||||
statsSkillsShInstalls: 8,
|
||||
statsGithubStars: 99,
|
||||
});
|
||||
|
||||
const mapped = toPublicSkill(legacySkill);
|
||||
|
||||
expect(mapped).not.toBeNull();
|
||||
expect(mapped?.stats).toEqual({
|
||||
downloads: 12,
|
||||
downloads: 20,
|
||||
stars: 3,
|
||||
installs: 7,
|
||||
versions: 0,
|
||||
@@ -65,6 +67,20 @@ describe("public skill mapping", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not expose source breakdowns on the ordinary public skill shape", () => {
|
||||
const mapped = toPublicSkill(
|
||||
makeSkill({
|
||||
statsDownloads: 12,
|
||||
statsSkillsShInstalls: 8,
|
||||
statsGithubStars: 99,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mapped?.stats.downloads).toBe(20);
|
||||
expect(mapped).not.toHaveProperty("statsSkillsShInstalls");
|
||||
expect(mapped).not.toHaveProperty("statsGithubStars");
|
||||
});
|
||||
|
||||
it("exposes GitHub-backed skill source fields", () => {
|
||||
const mapped = toPublicSkill(
|
||||
makeSkill({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Doc } from "../_generated/dataModel";
|
||||
import { isPublicSkillDoc } from "./globalStats";
|
||||
import { readCanonicalStat } from "./skillStats";
|
||||
import { readCanonicalStat, readPublicDownloads } from "./skillStats";
|
||||
|
||||
export type PublicUser = Pick<
|
||||
Doc<"users">,
|
||||
@@ -101,6 +101,8 @@ export type HydratableSkill = Pick<
|
||||
| "statsStars"
|
||||
| "statsInstallsCurrent"
|
||||
| "statsInstallsAllTime"
|
||||
| "statsSkillsShInstalls"
|
||||
| "statsGithubStars"
|
||||
| "softDeletedAt"
|
||||
| "moderationStatus"
|
||||
| "moderationFlags"
|
||||
@@ -149,7 +151,7 @@ export function toPublicSkill(skill: HydratableSkill | null | undefined): Public
|
||||
if (!skill) return null;
|
||||
if (!isPublicSkillDoc(skill)) return null;
|
||||
const stats = {
|
||||
downloads: readCanonicalStat(skill, "downloads"),
|
||||
downloads: readPublicDownloads(skill),
|
||||
stars: readCanonicalStat(skill, "stars"),
|
||||
installs: readCanonicalStat(skill, "installsAllTime"),
|
||||
versions: skill.stats?.versions ?? 0,
|
||||
|
||||
@@ -222,6 +222,15 @@ export const RETENTION_POLICIES = {
|
||||
skillsShCatalogScanAttempts: permanent(
|
||||
"Exact-hash skills.sh scan attempts are durable audit and deduplication history.",
|
||||
),
|
||||
skillsShMirrorControls: permanent("Durable skills.sh external mirror operator controls."),
|
||||
skillsShMirrorRuns: permanent("Skills.sh external mirror cursor and reconciliation history."),
|
||||
skillsShMirrorSourcePages: permanent(
|
||||
"Immutable authenticated leaderboard source pages retained for mirror provenance.",
|
||||
),
|
||||
skillsShMirrorDigests: permanent("Normalized skills.sh external search digests."),
|
||||
skillsShMirrorDetails: permanent("Bounded skills.sh external detail content."),
|
||||
skillsShMirrorFacets: permanent("Indexed skills.sh external category and topic metadata."),
|
||||
skillsShMirrorConflicts: permanent("Skills.sh external observation conflict audit history."),
|
||||
publisherAbuseScoreRuns: permanent("Abuse scoring run history."),
|
||||
publisherAbuseTemporalScanSamples: ephemeral(
|
||||
"Exact temporal percentile samples are temporary scan working state.",
|
||||
|
||||
@@ -58,6 +58,8 @@ function makeSkillDoc(overrides: Record<string, unknown> = {}) {
|
||||
statsStars: 5,
|
||||
statsInstallsCurrent: 10,
|
||||
statsInstallsAllTime: 100,
|
||||
statsSkillsShInstalls: 8,
|
||||
statsGithubStars: 250,
|
||||
stats: {
|
||||
downloads: 42,
|
||||
installsCurrent: 10,
|
||||
@@ -88,6 +90,8 @@ describe("extractDigestFields", () => {
|
||||
expect(digest.statsStars).toBe(5);
|
||||
expect(digest.statsInstallsCurrent).toBe(10);
|
||||
expect(digest.statsInstallsAllTime).toBe(100);
|
||||
expect(digest.statsSkillsShInstalls).toBe(8);
|
||||
expect(digest.statsGithubStars).toBe(250);
|
||||
expect(digest.recommendedScore).toBe(
|
||||
computeRecommendationScore({ downloads: 42, installs: 100, stars: 5 }),
|
||||
);
|
||||
|
||||
@@ -47,6 +47,8 @@ const SHARED_KEYS = [
|
||||
"statsStars",
|
||||
"statsInstallsCurrent",
|
||||
"statsInstallsAllTime",
|
||||
"statsSkillsShInstalls",
|
||||
"statsGithubStars",
|
||||
"softDeletedAt",
|
||||
"moderationStatus",
|
||||
"moderationFlags",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildExternalSkillMetricPatch,
|
||||
readPublicDownloads,
|
||||
readSkillMetricSources,
|
||||
} from "./skillStats";
|
||||
|
||||
const skill = {
|
||||
statsDownloads: 40,
|
||||
statsStars: 7,
|
||||
statsInstallsCurrent: 3,
|
||||
statsInstallsAllTime: 12,
|
||||
statsSkillsShInstalls: 9,
|
||||
statsGithubStars: 250,
|
||||
stats: {
|
||||
downloads: 40,
|
||||
stars: 7,
|
||||
installsCurrent: 3,
|
||||
installsAllTime: 12,
|
||||
},
|
||||
};
|
||||
|
||||
describe("source-attributed skill metrics", () => {
|
||||
it("keeps every source independently attributable", () => {
|
||||
expect(readSkillMetricSources(skill)).toEqual({
|
||||
clawHubDownloads: 40,
|
||||
skillsShInstalls: 9,
|
||||
openClawInstallsCurrent: 3,
|
||||
openClawInstallsAllTime: 12,
|
||||
githubStars: 250,
|
||||
bookmarks: 7,
|
||||
});
|
||||
});
|
||||
|
||||
it("adds skills.sh installs to public downloads without double-counting OpenClaw installs", () => {
|
||||
expect(readPublicDownloads(skill)).toBe(49);
|
||||
});
|
||||
|
||||
it("builds a source-only refresh patch and preserves unknown GitHub popularity", () => {
|
||||
expect(
|
||||
buildExternalSkillMetricPatch({
|
||||
skillsShInstalls: 11,
|
||||
githubStars: undefined,
|
||||
}),
|
||||
).toEqual({
|
||||
statsSkillsShInstalls: 11,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,8 +17,20 @@ export type SkillStatReadable = {
|
||||
statsStars?: number;
|
||||
statsInstallsCurrent?: number;
|
||||
statsInstallsAllTime?: number;
|
||||
statsSkillsShInstalls?: number;
|
||||
statsGithubStars?: number;
|
||||
};
|
||||
|
||||
type ExternalSkillMetricSnapshot = {
|
||||
skillsShInstalls?: number;
|
||||
githubStars?: number;
|
||||
};
|
||||
|
||||
function nonNegativeCount(value: number | undefined): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
||||
return Math.max(0, Math.trunc(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the canonical value of a migrated stat field from a skill document.
|
||||
*
|
||||
@@ -42,6 +54,32 @@ export function readCanonicalStat(
|
||||
return typeof skill[topLevelKey] === "number" ? skill[topLevelKey]! : (skill.stats[field] ?? 0);
|
||||
}
|
||||
|
||||
export function readSkillMetricSources(skill: SkillStatReadable) {
|
||||
return {
|
||||
clawHubDownloads: readCanonicalStat(skill, "downloads"),
|
||||
skillsShInstalls: nonNegativeCount(skill.statsSkillsShInstalls),
|
||||
openClawInstallsCurrent: readCanonicalStat(skill, "installsCurrent"),
|
||||
openClawInstallsAllTime: readCanonicalStat(skill, "installsAllTime"),
|
||||
githubStars: nonNegativeCount(skill.statsGithubStars),
|
||||
bookmarks: readCanonicalStat(skill, "stars"),
|
||||
};
|
||||
}
|
||||
|
||||
export function readPublicDownloads(skill: SkillStatReadable): number {
|
||||
return readCanonicalStat(skill, "downloads") + nonNegativeCount(skill.statsSkillsShInstalls);
|
||||
}
|
||||
|
||||
export function buildExternalSkillMetricPatch(snapshot: ExternalSkillMetricSnapshot) {
|
||||
return {
|
||||
...(snapshot.skillsShInstalls === undefined
|
||||
? {}
|
||||
: { statsSkillsShInstalls: nonNegativeCount(snapshot.skillsShInstalls) }),
|
||||
...(snapshot.githubStars === undefined
|
||||
? {}
|
||||
: { statsGithubStars: nonNegativeCount(snapshot.githubStars) }),
|
||||
};
|
||||
}
|
||||
|
||||
export function applySkillStatDeltas(skill: Doc<"skills">, deltas: SkillStatDeltas) {
|
||||
const currentDownloads = readCanonicalStat(skill, "downloads");
|
||||
const currentStars = readCanonicalStat(skill, "stars");
|
||||
|
||||
@@ -15,6 +15,7 @@ export type SkillsShCatalogFixtureRow = {
|
||||
claimPublisherHandle?: string;
|
||||
sourceContentHash: string;
|
||||
installs: number;
|
||||
githubStars?: number;
|
||||
};
|
||||
|
||||
type SkillsShCatalogFixture = {
|
||||
@@ -61,7 +62,8 @@ const PATRICK_HTML_CANARY: SkillsShCatalogFixtureRow = {
|
||||
githubContentHash: "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f",
|
||||
claimPublisherHandle: "patrick-erichsen",
|
||||
sourceContentHash: "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f",
|
||||
installs: 0,
|
||||
installs: 17,
|
||||
githubStars: 321,
|
||||
};
|
||||
|
||||
const FROZEN_ROWS = frozenSnapshot.rows satisfies SkillsShCatalogFixtureRow[];
|
||||
|
||||
@@ -283,7 +283,12 @@ const listPublishedPageHandler = (
|
||||
paginationOpts: { cursor: string | null; numItems: number };
|
||||
},
|
||||
{
|
||||
page: Array<{ displayName: string; href: string; kind: "skill" | "plugin" }>;
|
||||
page: Array<{
|
||||
displayName: string;
|
||||
href: string;
|
||||
kind: "skill" | "plugin";
|
||||
downloads: number;
|
||||
}>;
|
||||
continueCursor: string;
|
||||
isDone: boolean;
|
||||
}
|
||||
@@ -3139,6 +3144,7 @@ describe("publishers membership controls", () => {
|
||||
slug: "demo",
|
||||
displayName: "Demo Skill",
|
||||
moderationStatus: "active",
|
||||
statsSkillsShInstalls: 6,
|
||||
stats: { downloads: 10, stars: 1, installsCurrent: 1, installsAllTime: 2 },
|
||||
updatedAt: 5,
|
||||
};
|
||||
@@ -3188,6 +3194,7 @@ describe("publishers membership controls", () => {
|
||||
});
|
||||
|
||||
expect(firstPage.page.map((item) => item.kind)).toEqual(["skill"]);
|
||||
expect(firstPage.page[0]?.downloads).toBe(16);
|
||||
expect(firstPage.continueCursor).toBe("1");
|
||||
expect(firstPage.isDone).toBe(false);
|
||||
expect(secondPage.page.map((item) => item.kind)).toEqual(["plugin"]);
|
||||
@@ -3231,6 +3238,7 @@ describe("publishers membership controls", () => {
|
||||
displayName: "Modern Low",
|
||||
moderationStatus: "active",
|
||||
statsDownloads: 7,
|
||||
statsSkillsShInstalls: 100,
|
||||
stats: { downloads: 7, stars: 1, installsCurrent: 1, installsAllTime: 2 },
|
||||
updatedAt: 6,
|
||||
};
|
||||
@@ -3283,6 +3291,7 @@ describe("publishers membership controls", () => {
|
||||
paginationOpts: { cursor: null, numItems: 1 },
|
||||
});
|
||||
legacyLookup.first.mockResolvedValue(null);
|
||||
expect(firstPage.page[0]?.displayName).toBe("Legacy High");
|
||||
const secondPage = await listPublishedPageHandler(ctx as never, {
|
||||
handle: "legacy",
|
||||
kind: "skill",
|
||||
@@ -3463,6 +3472,7 @@ describe("publishers membership controls", () => {
|
||||
ownerPublisherId: "publishers:openclaw",
|
||||
softDeletedAt: undefined,
|
||||
moderationStatus: "active",
|
||||
statsSkillsShInstalls: 8,
|
||||
stats: { downloads: 42, stars: 2, installsCurrent: 4, installsAllTime: 7 },
|
||||
};
|
||||
const pkg = {
|
||||
@@ -3509,7 +3519,7 @@ describe("publishers membership controls", () => {
|
||||
skills: 1,
|
||||
packages: 1,
|
||||
installs: 12,
|
||||
downloads: 50,
|
||||
downloads: 58,
|
||||
stars: 3,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
isHandleReservedForAnotherUser,
|
||||
} from "./lib/reservedHandles";
|
||||
import { syncSkillSearchDigestForSkill } from "./lib/skillSearchDigest";
|
||||
import { readCanonicalStat } from "./lib/skillStats";
|
||||
import { readCanonicalStat, readPublicDownloads } from "./lib/skillStats";
|
||||
import { adjustUserSkillStatsForSkillChange } from "./lib/userSkillStats";
|
||||
|
||||
const MAX_PUBLIC_PUBLISHER_LIST_LIMIT = 500;
|
||||
@@ -388,7 +388,7 @@ function getIndexedPublisherStatsFromRows(rows: PublisherPublishedRows): Publish
|
||||
for (const skill of rows.skills) {
|
||||
stats.skills += 1;
|
||||
stats.installs += readCanonicalStat(skill, "installsAllTime");
|
||||
stats.downloads += readCanonicalStat(skill, "downloads");
|
||||
stats.downloads += readPublicDownloads(skill);
|
||||
stats.stars += readCanonicalStat(skill, "stars");
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@ function getPublisherPublishedItems(
|
||||
inferredCategories: skill.inferredCategories,
|
||||
latestVersionId: skill.latestVersionId,
|
||||
inferredFromVersionId: skill.inferredFromVersionId,
|
||||
downloads: readCanonicalStat(skill, "downloads"),
|
||||
downloads: readPublicDownloads(skill),
|
||||
installs: readCanonicalStat(skill, "installsAllTime"),
|
||||
})),
|
||||
...rows.packages.map((pkg) => ({
|
||||
@@ -551,7 +551,7 @@ function toPublisherSkillCatalogItem(
|
||||
icon: skill.icon ?? null,
|
||||
href: `/${encodeURIComponent(publisher.handle)}/${encodeURIComponent(skill.slug)}`,
|
||||
installs: readCanonicalStat(skill, "installsAllTime"),
|
||||
downloads: readCanonicalStat(skill, "downloads"),
|
||||
downloads: readPublicDownloads(skill),
|
||||
stars: readCanonicalStat(skill, "stars"),
|
||||
isOfficial: publisherOfficial || Boolean(skill.badges?.official),
|
||||
updatedAt: skill.updatedAt,
|
||||
@@ -2578,7 +2578,7 @@ export const listStarredPage = query({
|
||||
icon: skill.icon ?? null,
|
||||
href: `/${encodeURIComponent(ownerHandle)}/${encodeURIComponent(skill.slug)}`,
|
||||
installs: readCanonicalStat(skill, "installsAllTime"),
|
||||
downloads: readCanonicalStat(skill, "downloads"),
|
||||
downloads: readPublicDownloads(skill),
|
||||
stars: readCanonicalStat(skill, "stars"),
|
||||
isOfficial: official || Boolean(skill.badges?.official),
|
||||
updatedAt: skill.updatedAt,
|
||||
|
||||
@@ -921,6 +921,8 @@ const skills = defineTable({
|
||||
statsStars: v.optional(v.number()),
|
||||
statsInstallsCurrent: v.optional(v.number()),
|
||||
statsInstallsAllTime: v.optional(v.number()),
|
||||
statsSkillsShInstalls: v.optional(v.number()),
|
||||
statsGithubStars: v.optional(v.number()),
|
||||
installBackfill: v.optional(
|
||||
v.object({
|
||||
modelVersion: v.string(),
|
||||
@@ -1347,6 +1349,8 @@ const skillSearchDigest = defineTable({
|
||||
statsStars: v.optional(v.number()),
|
||||
statsInstallsCurrent: v.optional(v.number()),
|
||||
statsInstallsAllTime: v.optional(v.number()),
|
||||
statsSkillsShInstalls: v.optional(v.number()),
|
||||
statsGithubStars: v.optional(v.number()),
|
||||
recommendedScore: v.optional(v.number()),
|
||||
recommendedScoreVersion: v.optional(v.number()),
|
||||
softDeletedAt: v.optional(v.number()),
|
||||
@@ -2916,6 +2920,7 @@ const skillsShCatalogEntries = defineTable({
|
||||
githubContentHash: v.optional(v.string()),
|
||||
sourceContentHash: v.string(),
|
||||
installs: v.number(),
|
||||
githubStars: v.optional(v.number()),
|
||||
sourceSnapshotId: v.string(),
|
||||
reconciliation: v.optional(
|
||||
v.object({
|
||||
@@ -2996,6 +3001,254 @@ const skillsShCatalogScanAttempts = defineTable({
|
||||
.index("by_status_and_created_at", ["status", "createdAt"])
|
||||
.index("by_dispatch_kind_and_status_and_created_at", ["dispatchKind", "status", "createdAt"]);
|
||||
|
||||
const skillsShMirrorControls = defineTable({
|
||||
key: v.literal("global"),
|
||||
enabled: v.boolean(),
|
||||
paused: v.boolean(),
|
||||
maxRowsPerRun: v.number(),
|
||||
maxRowsPerBatch: v.number(),
|
||||
maxDetailBytes: v.number(),
|
||||
updatedBy: v.string(),
|
||||
reason: v.string(),
|
||||
updatedAt: v.number(),
|
||||
}).index("by_key", ["key"]);
|
||||
|
||||
const skillsShMirrorRunCountsValidator = v.object({
|
||||
observed: v.number(),
|
||||
inserted: v.number(),
|
||||
updated: v.number(),
|
||||
unchanged: v.number(),
|
||||
rejected: v.number(),
|
||||
quarantined: v.optional(v.number()),
|
||||
quarantinedPreserved: v.optional(v.number()),
|
||||
conflicts: v.number(),
|
||||
detailsInserted: v.number(),
|
||||
detailsUpdated: v.number(),
|
||||
detailsUnchanged: v.number(),
|
||||
detailsMissing: v.number(),
|
||||
detailsTruncated: v.number(),
|
||||
tombstoned: v.number(),
|
||||
reactivated: v.number(),
|
||||
scansPlanned: v.literal(0),
|
||||
scansAdmitted: v.literal(0),
|
||||
});
|
||||
|
||||
const skillsShMirrorRuns = defineTable({
|
||||
snapshotId: v.string(),
|
||||
sourceSnapshotHash: v.optional(v.string()),
|
||||
sourceCaptureWrites: v.optional(v.number()),
|
||||
status: v.union(
|
||||
v.literal("running"),
|
||||
v.literal("paused"),
|
||||
v.literal("reconciling"),
|
||||
v.literal("completed"),
|
||||
v.literal("failed"),
|
||||
v.literal("canceled"),
|
||||
),
|
||||
sourceTotal: v.number(),
|
||||
sourcePageSize: v.number(),
|
||||
sourceMeasuredAt: v.string(),
|
||||
page: v.number(),
|
||||
offset: v.number(),
|
||||
batchLeaseToken: v.optional(v.string()),
|
||||
batchLeaseExpiresAt: v.optional(v.number()),
|
||||
reconcileCursor: v.optional(v.string()),
|
||||
counts: skillsShMirrorRunCountsValidator,
|
||||
operations: v.object({
|
||||
functionCalls: v.number(),
|
||||
dbReads: v.number(),
|
||||
dbWrites: v.number(),
|
||||
sourceRequests: v.number(),
|
||||
sourceBytes: v.number(),
|
||||
}),
|
||||
actor: v.string(),
|
||||
reason: v.string(),
|
||||
startedAt: v.number(),
|
||||
completedAt: v.optional(v.number()),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_started_at", ["startedAt"])
|
||||
.index("by_status_and_updated_at", {
|
||||
fields: ["status", "updatedAt"],
|
||||
});
|
||||
|
||||
const skillsShMirrorSourcePages = defineTable({
|
||||
snapshotHash: v.string(),
|
||||
page: v.number(),
|
||||
sourceTotal: v.number(),
|
||||
pageLength: v.number(),
|
||||
hasMore: v.boolean(),
|
||||
identityHash: v.string(),
|
||||
contentHash: v.string(),
|
||||
sourceBytes: v.number(),
|
||||
serializedBytes: v.number(),
|
||||
rows: v.array(
|
||||
v.object({
|
||||
id: v.string(),
|
||||
installUrl: v.union(v.string(), v.null()),
|
||||
installs: v.number(),
|
||||
name: v.string(),
|
||||
slug: v.string(),
|
||||
source: v.string(),
|
||||
sourceType: v.string(),
|
||||
url: v.string(),
|
||||
}),
|
||||
),
|
||||
createdAt: v.number(),
|
||||
}).index("by_snapshot_hash_and_page", ["snapshotHash", "page"]);
|
||||
|
||||
const skillsShMirrorUpstreamScannerValidator = v.object({
|
||||
status: v.string(),
|
||||
sourceCheckedAt: v.optional(v.string()),
|
||||
sourceUrl: v.optional(v.string()),
|
||||
});
|
||||
|
||||
const skillsShMirrorClassificationConfidenceValidator = v.union(
|
||||
v.literal("high"),
|
||||
v.literal("medium"),
|
||||
v.literal("low"),
|
||||
);
|
||||
|
||||
const skillsShMirrorDigests = defineTable({
|
||||
externalId: v.string(),
|
||||
sourceType: v.union(v.literal("github"), v.literal("well-known")),
|
||||
upstreamSourceType: v.optional(v.string()),
|
||||
owner: v.optional(v.string()),
|
||||
repo: v.optional(v.string()),
|
||||
sourceHost: v.optional(v.string()),
|
||||
slug: v.string(),
|
||||
normalizedSlug: v.string(),
|
||||
normalizedSlugFirstToken: v.string(),
|
||||
displayName: v.string(),
|
||||
normalizedDisplayName: v.string(),
|
||||
normalizedDisplayNameFirstToken: v.string(),
|
||||
searchText: v.string(),
|
||||
sourceUrl: v.string(),
|
||||
canonicalRepoUrl: v.optional(v.string()),
|
||||
githubPath: v.optional(v.string()),
|
||||
githubCommit: v.optional(v.string()),
|
||||
sourceContentHash: v.optional(v.string()),
|
||||
upstreamInstalls: v.number(),
|
||||
upstreamScanners: v.object({
|
||||
genAgentTrustHub: skillsShMirrorUpstreamScannerValidator,
|
||||
socket: skillsShMirrorUpstreamScannerValidator,
|
||||
snyk: skillsShMirrorUpstreamScannerValidator,
|
||||
}),
|
||||
inferredCategories: v.optional(v.array(v.string())),
|
||||
inferredTopics: v.optional(v.array(v.string())),
|
||||
inferredCategoryConfidence: v.optional(skillsShMirrorClassificationConfidenceValidator),
|
||||
inferredTopicConfidence: v.optional(skillsShMirrorClassificationConfidenceValidator),
|
||||
inferredClassifierVersion: v.optional(v.string()),
|
||||
inferredTopicClassifierVersion: v.optional(v.string()),
|
||||
inferredInputHash: v.optional(v.string()),
|
||||
inferredTopicInputHash: v.optional(v.string()),
|
||||
inferredAt: v.optional(v.number()),
|
||||
sourceFreshnessStatus: v.union(v.literal("observed-only"), v.literal("stale")),
|
||||
staleQuarantineReason: v.optional(v.string()),
|
||||
detailStatus: v.union(v.literal("available"), v.literal("missing")),
|
||||
observationFingerprint: v.string(),
|
||||
sourceSnapshotId: v.string(),
|
||||
lastObservedRunId: v.id("skillsShMirrorRuns"),
|
||||
active: v.boolean(),
|
||||
publicVisible: v.literal(false),
|
||||
installable: v.literal(false),
|
||||
tombstonedAt: v.optional(v.number()),
|
||||
firstObservedAt: v.number(),
|
||||
lastObservedAt: v.number(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_external_id", ["externalId"])
|
||||
.index("by_active_and_normalized_slug", {
|
||||
fields: ["active", "normalizedSlug"],
|
||||
})
|
||||
.index("by_active_and_normalized_display_name", {
|
||||
fields: ["active", "normalizedDisplayName"],
|
||||
})
|
||||
.index("by_active_and_normalized_slug_first_token", {
|
||||
fields: ["active", "normalizedSlugFirstToken"],
|
||||
})
|
||||
.index("by_active_and_normalized_display_name_first_token", {
|
||||
fields: ["active", "normalizedDisplayNameFirstToken"],
|
||||
})
|
||||
.index("by_active_and_source_type_and_owner_and_repo_and_external_id", {
|
||||
fields: ["active", "sourceType", "owner", "repo", "externalId"],
|
||||
})
|
||||
.index("by_active_and_upstream_installs", {
|
||||
fields: ["active", "upstreamInstalls"],
|
||||
})
|
||||
.index("by_source_type_and_external_id", {
|
||||
fields: ["sourceType", "externalId"],
|
||||
})
|
||||
.index("by_last_observed_at", {
|
||||
fields: ["lastObservedAt"],
|
||||
})
|
||||
.searchIndex("search_by_search_text", {
|
||||
searchField: "searchText",
|
||||
filterFields: ["active"],
|
||||
});
|
||||
|
||||
const skillsShMirrorDetails = defineTable({
|
||||
externalId: v.string(),
|
||||
digestId: v.id("skillsShMirrorDigests"),
|
||||
contentKind: v.union(v.literal("skill-md"), v.literal("readme")),
|
||||
path: v.string(),
|
||||
content: v.string(),
|
||||
contentBytes: v.number(),
|
||||
sourceBytes: v.number(),
|
||||
sourceFileCount: v.number(),
|
||||
truncated: v.boolean(),
|
||||
sourceContentHash: v.optional(v.string()),
|
||||
sourceSnapshotId: v.string(),
|
||||
lastObservedRunId: v.id("skillsShMirrorRuns"),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_external_id", ["externalId"])
|
||||
.index("by_digest_id", {
|
||||
fields: ["digestId"],
|
||||
});
|
||||
|
||||
const skillsShMirrorFacets = defineTable({
|
||||
digestId: v.id("skillsShMirrorDigests"),
|
||||
externalId: v.string(),
|
||||
kind: v.union(v.literal("category"), v.literal("topic")),
|
||||
term: v.string(),
|
||||
active: v.boolean(),
|
||||
installs: v.number(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_digest_id_and_kind_and_term", ["digestId", "kind", "term"])
|
||||
.index("by_active_and_kind_and_term_and_installs_and_external_id", [
|
||||
"active",
|
||||
"kind",
|
||||
"term",
|
||||
"installs",
|
||||
"externalId",
|
||||
]);
|
||||
|
||||
const skillsShMirrorConflicts = defineTable({
|
||||
runId: v.id("skillsShMirrorRuns"),
|
||||
externalId: v.string(),
|
||||
kind: v.union(
|
||||
v.literal("same-run-drift"),
|
||||
v.literal("identity-mismatch"),
|
||||
v.literal("source-quarantine"),
|
||||
),
|
||||
reason: v.optional(v.string()),
|
||||
upstreamSourceType: v.optional(v.string()),
|
||||
previousFingerprint: v.optional(v.string()),
|
||||
observedFingerprint: v.string(),
|
||||
page: v.number(),
|
||||
offset: v.number(),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_run_id", ["runId"])
|
||||
.index("by_external_id_and_created_at", {
|
||||
fields: ["externalId", "createdAt"],
|
||||
});
|
||||
|
||||
const publisherAbuseScoreRuns = defineTable({
|
||||
modelVersion: v.string(),
|
||||
modelConfig: publisherAbuseModelConfigValidator,
|
||||
@@ -3692,6 +3945,13 @@ export default defineSchema({
|
||||
skillsShCatalogRuns,
|
||||
skillsShCatalogEntries,
|
||||
skillsShCatalogScanAttempts,
|
||||
skillsShMirrorControls,
|
||||
skillsShMirrorRuns,
|
||||
skillsShMirrorSourcePages,
|
||||
skillsShMirrorDigests,
|
||||
skillsShMirrorDetails,
|
||||
skillsShMirrorFacets,
|
||||
skillsShMirrorConflicts,
|
||||
publisherAbuseScoreRuns,
|
||||
publisherAbuseTemporalScanSamples,
|
||||
publisherAbuseTemporalScanCandidates,
|
||||
|
||||
@@ -2014,6 +2014,54 @@ describe("search helpers", () => {
|
||||
expect(result.map((entry) => entry.skill.slug)).toEqual(["tool-installed", "tool-downloaded"]);
|
||||
});
|
||||
|
||||
it("keeps skills.sh installs out of the native download ranking tie-breaker", async () => {
|
||||
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
||||
const indexed = {
|
||||
nativeDownloads: 10,
|
||||
skill: makePublicSkill({
|
||||
id: "skills:indexed",
|
||||
slug: "tool-indexed",
|
||||
displayName: "Tool",
|
||||
downloads: 10_010,
|
||||
installs: 0,
|
||||
stars: 0,
|
||||
}),
|
||||
version: null,
|
||||
ownerHandle: "owner",
|
||||
owner: null,
|
||||
};
|
||||
const native = {
|
||||
nativeDownloads: 20,
|
||||
skill: makePublicSkill({
|
||||
id: "skills:native",
|
||||
slug: "tool-native",
|
||||
displayName: "Tool",
|
||||
downloads: 20,
|
||||
installs: 0,
|
||||
stars: 0,
|
||||
}),
|
||||
version: null,
|
||||
ownerHandle: "owner",
|
||||
owner: null,
|
||||
};
|
||||
const runQuery = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(null) // getExactSkillSlugMatch
|
||||
.mockResolvedValueOnce([]) // directPrefixSkillMatches
|
||||
.mockResolvedValueOnce([indexed, native]); // lexicalFallbackSkills
|
||||
|
||||
const result = await searchSkillsHandler(
|
||||
{
|
||||
vectorSearch: vi.fn().mockResolvedValue([]),
|
||||
runQuery,
|
||||
},
|
||||
{ query: "tool", limit: 2 },
|
||||
);
|
||||
|
||||
expect(result.map((entry) => entry.skill.slug)).toEqual(["tool-native", "tool-indexed"]);
|
||||
expect(result[0]).not.toHaveProperty("nativeDownloads");
|
||||
});
|
||||
|
||||
it("uses digest doc instead of full skill doc in hydrateResults but revalidates the owner", async () => {
|
||||
// Derive digest from makeSkillDoc so it stays in sync with schema changes.
|
||||
const skillDoc = makeSkillDoc({
|
||||
|
||||
+11
-3
@@ -35,6 +35,7 @@ import {
|
||||
normalizeSkillSearchText,
|
||||
} from "./lib/skillSearchDigest";
|
||||
import { isSearchableSkillSlugShape, normalizeSkillSlug } from "./lib/skillSlugValidator";
|
||||
import { readCanonicalStat } from "./lib/skillStats";
|
||||
|
||||
type OwnerInfo = { ownerHandle: string | null; owner: PublicPublisher | null };
|
||||
|
||||
@@ -75,6 +76,7 @@ async function withOfficialOwnerInfo(ctx: Pick<QueryCtx, "db">, ownerInfo: Owner
|
||||
|
||||
type SkillSearchEntry = {
|
||||
embeddingId?: Id<"skillEmbeddings">;
|
||||
nativeDownloads: number;
|
||||
skill: NonNullable<ReturnType<typeof toPublicSkill>>;
|
||||
version: Doc<"skillVersions"> | null;
|
||||
ownerHandle: string | null;
|
||||
@@ -89,7 +91,7 @@ type SearchResult = SkillSearchEntry &
|
||||
SearchMatch & {
|
||||
score: number;
|
||||
};
|
||||
type PublicSearchResult = SkillSearchEntry & {
|
||||
type PublicSearchResult = Omit<SkillSearchEntry, "nativeDownloads"> & {
|
||||
score: number;
|
||||
};
|
||||
|
||||
@@ -239,7 +241,7 @@ function compareSkillTrustAndUsage(a: SkillSearchEntry, b: SkillSearchEntry) {
|
||||
{ stars: a.skill.stats.stars, installsAllTime: a.skill.stats.installs },
|
||||
{ stars: b.skill.stats.stars, installsAllTime: b.skill.stats.installs },
|
||||
) ||
|
||||
(b.skill.stats.downloads ?? 0) - (a.skill.stats.downloads ?? 0)
|
||||
b.nativeDownloads - a.nativeDownloads
|
||||
);
|
||||
}
|
||||
|
||||
@@ -508,7 +510,9 @@ export const searchSkills: ReturnType<typeof action> = action({
|
||||
b.skill.updatedAt - a.skill.updatedAt,
|
||||
)
|
||||
.slice(0, limit);
|
||||
return rankedMatches.map(({ rankTier: _rankTier, ...entry }) => entry);
|
||||
return rankedMatches.map(
|
||||
({ nativeDownloads: _nativeDownloads, rankTier: _rankTier, ...entry }) => entry,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -541,6 +545,7 @@ export const getExactSkillSlugMatch = internalQuery({
|
||||
if (!publicSkill || !resolved.owner) return null;
|
||||
|
||||
const entry: SkillSearchEntry = {
|
||||
nativeDownloads: readCanonicalStat(skill, "downloads"),
|
||||
skill: publicSkill,
|
||||
version: null as Doc<"skillVersions"> | null,
|
||||
ownerHandle: resolved.ownerHandle,
|
||||
@@ -851,6 +856,7 @@ export const directPrefixSkillMatches = internalQuery({
|
||||
const publicSkill = toPublicSearchSkill(skill);
|
||||
if (!publicSkill || !resolved.owner) return null;
|
||||
return {
|
||||
nativeDownloads: readCanonicalStat(skill, "downloads"),
|
||||
skill: publicSkill,
|
||||
version: null as Doc<"skillVersions"> | null,
|
||||
ownerHandle: resolved.ownerHandle,
|
||||
@@ -920,6 +926,7 @@ export const hydrateResults = internalQuery({
|
||||
if (!publicSkill) return null;
|
||||
return {
|
||||
embeddingId,
|
||||
nativeDownloads: readCanonicalStat(skill, "downloads"),
|
||||
skill: publicSkill,
|
||||
version: null as Doc<"skillVersions"> | null,
|
||||
ownerHandle: resolved.ownerHandle,
|
||||
@@ -1093,6 +1100,7 @@ export const lexicalFallbackSkills = internalQuery({
|
||||
const publicSkill = toPublicSearchSkill(skill);
|
||||
if (!publicSkill) return null;
|
||||
return {
|
||||
nativeDownloads: readCanonicalStat(skill, "downloads"),
|
||||
skill: publicSkill,
|
||||
version: null as Doc<"skillVersions"> | null,
|
||||
ownerHandle: resolved.ownerHandle,
|
||||
|
||||
@@ -208,7 +208,24 @@ describe("skills.listDashboardPaginated", () => {
|
||||
it("paginates user dashboard skills through an active owner index", async () => {
|
||||
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
||||
const { ctx, indexCalls } = makeCtx({
|
||||
by_owner_active_updated: [makeSkill("slack")],
|
||||
by_owner_active_updated: [
|
||||
makeSkill("slack", {
|
||||
statsDownloads: 12,
|
||||
statsSkillsShInstalls: 8,
|
||||
statsGithubStars: 99,
|
||||
statsInstallsCurrent: 3,
|
||||
statsInstallsAllTime: 7,
|
||||
statsStars: 4,
|
||||
stats: {
|
||||
downloads: 12,
|
||||
installsCurrent: 3,
|
||||
installsAllTime: 7,
|
||||
stars: 4,
|
||||
versions: 1,
|
||||
comments: 0,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const result = await handler(
|
||||
@@ -220,7 +237,20 @@ describe("skills.listDashboardPaginated", () => {
|
||||
);
|
||||
|
||||
expect(indexCalls).toContain("by_owner_active_updated");
|
||||
expect(result.page).toEqual([expect.objectContaining({ slug: "slack" })]);
|
||||
expect(result.page).toEqual([
|
||||
expect.objectContaining({
|
||||
slug: "slack",
|
||||
stats: expect.objectContaining({ downloads: 20 }),
|
||||
metricSources: {
|
||||
clawHubDownloads: 12,
|
||||
skillsShInstalls: 8,
|
||||
openClawInstallsCurrent: 3,
|
||||
openClawInstallsAllTime: 7,
|
||||
githubStars: 99,
|
||||
bookmarks: 4,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("includes linked-user legacy skills when paginating a personal publisher", async () => {
|
||||
|
||||
@@ -33,6 +33,7 @@ const listPackageCatalogPageHandler = (
|
||||
family: "skill";
|
||||
channel: "official" | "community";
|
||||
isOfficial: boolean;
|
||||
stats: { downloads: number };
|
||||
}>;
|
||||
isDone: boolean;
|
||||
continueCursor: string;
|
||||
@@ -344,6 +345,34 @@ describe("skills package catalog queries", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("presents combined downloads without changing the native download index", async () => {
|
||||
const indexNames: string[] = [];
|
||||
const result = await listPackageCatalogPageHandler(
|
||||
makeCtx(
|
||||
[
|
||||
{
|
||||
page: [
|
||||
makeDigest("indexed-skill", {
|
||||
statsDownloads: 12,
|
||||
statsSkillsShInstalls: 8,
|
||||
}),
|
||||
],
|
||||
isDone: true,
|
||||
continueCursor: "",
|
||||
},
|
||||
],
|
||||
indexNames,
|
||||
),
|
||||
{
|
||||
sort: "downloads",
|
||||
paginationOpts: { cursor: null, numItems: 10 },
|
||||
},
|
||||
);
|
||||
|
||||
expect(indexNames).toContain("by_active_stats_downloads");
|
||||
expect(result.page[0]?.stats.downloads).toBe(20);
|
||||
});
|
||||
|
||||
it("normalizes and filters skill package catalog topics", async () => {
|
||||
const indexNames: string[] = [];
|
||||
const calendarSkill = makeDigest("calendar-skill", { topics: ["calendar"] });
|
||||
|
||||
+7
-3
@@ -167,7 +167,7 @@ import {
|
||||
upsertSkillSearchDigest,
|
||||
} from "./lib/skillSearchDigest";
|
||||
import { assertValidSkillSlug, normalizeSkillSlug } from "./lib/skillSlugValidator";
|
||||
import { readCanonicalStat } from "./lib/skillStats";
|
||||
import { readCanonicalStat, readPublicDownloads, readSkillMetricSources } from "./lib/skillStats";
|
||||
import { normalizeSkillTags } from "./lib/skillTags";
|
||||
import { runStaticPublishScan } from "./lib/staticPublishScan";
|
||||
import { adjustUserSkillStatsForSkillChange } from "./lib/userSkillStats";
|
||||
@@ -1984,6 +1984,7 @@ type DashboardSkillListItem = {
|
||||
tags: Doc<"skills">["tags"];
|
||||
badges: Doc<"skills">["badges"];
|
||||
stats: Doc<"skills">["stats"];
|
||||
metricSources: ReturnType<typeof readSkillMetricSources>;
|
||||
moderationStatus?: Doc<"skills">["moderationStatus"];
|
||||
moderationReason?: string;
|
||||
moderationSummary?: string;
|
||||
@@ -2377,7 +2378,7 @@ async function toDashboardSkillListItem(
|
||||
const latestVersion = skill.latestVersionId ? await ctx.db.get(skill.latestVersionId) : null;
|
||||
const stats = {
|
||||
...skill.stats,
|
||||
downloads: readCanonicalStat(skill, "downloads"),
|
||||
downloads: readPublicDownloads(skill),
|
||||
stars: readCanonicalStat(skill, "stars"),
|
||||
installsCurrent: readCanonicalStat(skill, "installsCurrent"),
|
||||
installsAllTime: readCanonicalStat(skill, "installsAllTime"),
|
||||
@@ -2397,6 +2398,7 @@ async function toDashboardSkillListItem(
|
||||
tags: skill.tags,
|
||||
badges: skill.badges,
|
||||
stats,
|
||||
metricSources: readSkillMetricSources(skill),
|
||||
moderationStatus: skill.moderationStatus,
|
||||
moderationReason: skill.moderationReason,
|
||||
moderationSummary: skill.moderationSummary,
|
||||
@@ -6432,7 +6434,9 @@ async function toPublicSkillCatalogItem(
|
||||
latestVersion: latestVersion?.version ?? null,
|
||||
verificationTier: null,
|
||||
stats: {
|
||||
downloads: readDigestRankStat(digest, "downloads"),
|
||||
// CLAW-561 changes presentation only. Download indexes and ranking remain
|
||||
// native-only until a separately accepted indexed combined metric exists.
|
||||
downloads: readPublicDownloads(digest),
|
||||
installs: readDigestRankStat(digest, "installsAllTime"),
|
||||
stars: readDigestRankStat(digest, "stars"),
|
||||
versions: digest.stats.versions,
|
||||
|
||||
+132
-4
@@ -19,6 +19,7 @@ import {
|
||||
isExactSkillsShCatalogAttempt,
|
||||
shouldPublishSkillsShCatalogEntry,
|
||||
} from "./lib/skillsShCatalogPublication";
|
||||
import { buildExternalSkillMetricPatch } from "./lib/skillStats";
|
||||
import { validateFilePath } from "./lib/skillZip";
|
||||
import { enqueueSkillsShCatalogScanRequest } from "./securityScan";
|
||||
|
||||
@@ -37,6 +38,10 @@ const MAX_WRITES_PER_BATCH = 100;
|
||||
const MAX_SCAN_ADMISSIONS_PER_BATCH = 100;
|
||||
const MAX_SCAN_ADMISSIONS_PER_RUN = 500;
|
||||
const MAX_REAL_TEST_ADMISSIONS = 10;
|
||||
const CLEARED_EXTERNAL_SKILL_METRICS = {
|
||||
statsSkillsShInstalls: undefined,
|
||||
statsGithubStars: undefined,
|
||||
};
|
||||
const MAX_DETERMINISTIC_COMPLETIONS_PER_BATCH = 50;
|
||||
|
||||
const fixtureIdValidator = v.union(
|
||||
@@ -69,6 +74,7 @@ const stagingLiveRowValidator = v.object({
|
||||
claimPublisherHandle: v.optional(v.string()),
|
||||
sourceContentHash: v.string(),
|
||||
installs: v.number(),
|
||||
githubStars: v.optional(v.number()),
|
||||
});
|
||||
const sourceVerificationValidator = v.object({
|
||||
githubOwnerId: v.number(),
|
||||
@@ -168,8 +174,21 @@ async function reconcileNativeSkill(
|
||||
const source = await ctx.db.get(skill.githubSourceId);
|
||||
reads += 1;
|
||||
if (source?.repo.trim().toLowerCase() !== `${row.owner}/${row.repo}`) continue;
|
||||
const metricPatch = buildExternalSkillMetricPatch({
|
||||
skillsShInstalls: row.installs,
|
||||
githubStars: row.githubStars,
|
||||
});
|
||||
const metricPatchChanged = Object.entries(metricPatch).some(
|
||||
([field, value]) => skill[field as keyof typeof skill] !== value,
|
||||
);
|
||||
return {
|
||||
reads,
|
||||
nativeMetricUpdate: metricPatchChanged
|
||||
? {
|
||||
skillId: skill._id,
|
||||
patch: metricPatch,
|
||||
}
|
||||
: undefined,
|
||||
reconciliation: {
|
||||
kind: "exact-native" as const,
|
||||
nativeSkillId: skill._id,
|
||||
@@ -200,6 +219,54 @@ async function reconcileNativeSkill(
|
||||
};
|
||||
}
|
||||
|
||||
function nativeMetricSyncSkillIds(
|
||||
existing: Doc<"skillsShCatalogEntries"> | null,
|
||||
native: Awaited<ReturnType<typeof reconcileNativeSkill>>,
|
||||
) {
|
||||
const skillIds = new Set<Id<"skills">>();
|
||||
if ("nativeMetricUpdate" in native && native.nativeMetricUpdate) {
|
||||
skillIds.add(native.nativeMetricUpdate.skillId);
|
||||
}
|
||||
const previous = existing?.reconciliation;
|
||||
if (
|
||||
previous?.kind === "exact-native" &&
|
||||
previous.nativeSkillId &&
|
||||
(native.reconciliation.kind !== "exact-native" ||
|
||||
native.reconciliation.nativeSkillId !== previous.nativeSkillId)
|
||||
) {
|
||||
skillIds.add(previous.nativeSkillId);
|
||||
}
|
||||
return [...skillIds];
|
||||
}
|
||||
|
||||
export const syncNativeSkillMetricsFromCatalogEntryInternal = internalMutation({
|
||||
args: {
|
||||
entryId: v.id("skillsShCatalogEntries"),
|
||||
skillId: v.id("skills"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const [entry, skill] = await Promise.all([ctx.db.get(args.entryId), ctx.db.get(args.skillId)]);
|
||||
if (!entry || !skill) return { applied: false };
|
||||
const patch =
|
||||
entry.reconciliation?.kind === "exact-native" &&
|
||||
entry.reconciliation.nativeSkillId === args.skillId
|
||||
? buildExternalSkillMetricPatch({
|
||||
skillsShInstalls: entry.installs,
|
||||
githubStars: entry.githubStars,
|
||||
})
|
||||
: CLEARED_EXTERNAL_SKILL_METRICS;
|
||||
const changed = Object.entries(patch).some(
|
||||
([field, value]) => skill[field as keyof typeof skill] !== value,
|
||||
);
|
||||
if (!changed) return { applied: false };
|
||||
|
||||
// The skills-table trigger in functions.ts refreshes the search digest from
|
||||
// this patch, keeping the batch write budget independent of digest fan-out.
|
||||
await ctx.db.patch(skill._id, patch);
|
||||
return { applied: true };
|
||||
},
|
||||
});
|
||||
|
||||
function incrementReconciliationCounts(
|
||||
counts: ReturnType<typeof normalizedCounts>,
|
||||
reconciliation: Doc<"skillsShCatalogEntries">["reconciliation"],
|
||||
@@ -950,6 +1017,7 @@ export const processStagingLiveBatchInternal = internalMutation({
|
||||
}
|
||||
const native = await reconcileNativeSkill(ctx, row, now);
|
||||
readsUsed += native.reads;
|
||||
const metricSkillIds = nativeMetricSyncSkillIds(existing, native);
|
||||
const observationUnchanged = existing
|
||||
? sameFixtureObservation(existing, row, native.reconciliation)
|
||||
: false;
|
||||
@@ -981,6 +1049,7 @@ export const processStagingLiveBatchInternal = internalMutation({
|
||||
counts.observed += 1;
|
||||
cursor += 1;
|
||||
if (shouldPlanScan) counts.scansPlanned += 1;
|
||||
let entryId = existing?._id;
|
||||
if (existing) {
|
||||
if (observationUnchanged) counts.unchanged += 1;
|
||||
else {
|
||||
@@ -1001,6 +1070,7 @@ export const processStagingLiveBatchInternal = internalMutation({
|
||||
githubContentHash: row.githubContentHash,
|
||||
sourceContentHash: row.sourceContentHash,
|
||||
installs: row.installs,
|
||||
githubStars: row.githubStars,
|
||||
sourceSnapshotId: run.snapshotId,
|
||||
reconciliation: native.reconciliation,
|
||||
publicVisible: false,
|
||||
@@ -1017,7 +1087,7 @@ export const processStagingLiveBatchInternal = internalMutation({
|
||||
} else {
|
||||
counts.wouldInsert += 1;
|
||||
counts.inserted += 1;
|
||||
await ctx.db.insert("skillsShCatalogEntries", {
|
||||
entryId = await ctx.db.insert("skillsShCatalogEntries", {
|
||||
externalId: row.externalId,
|
||||
sourceKind: "staging-live",
|
||||
githubOwnerId: row.githubOwnerId,
|
||||
@@ -1032,6 +1102,7 @@ export const processStagingLiveBatchInternal = internalMutation({
|
||||
githubContentHash: row.githubContentHash,
|
||||
sourceContentHash: row.sourceContentHash,
|
||||
installs: row.installs,
|
||||
githubStars: row.githubStars,
|
||||
sourceSnapshotId: run.snapshotId,
|
||||
reconciliation: native.reconciliation,
|
||||
publicVisible: false,
|
||||
@@ -1043,6 +1114,18 @@ export const processStagingLiveBatchInternal = internalMutation({
|
||||
});
|
||||
}
|
||||
writesUsed += 1;
|
||||
if (entryId) {
|
||||
for (const skillId of metricSkillIds) {
|
||||
await ctx.scheduler.runAfter(
|
||||
0,
|
||||
internal.skillsShCatalog.syncNativeSkillMetricsFromCatalogEntryInternal,
|
||||
{
|
||||
entryId,
|
||||
skillId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor !== args.cursor + args.rows.length) {
|
||||
@@ -1122,6 +1205,7 @@ export const processFixtureBatchInternal = internalMutation({
|
||||
}
|
||||
const native = await reconcileNativeSkill(ctx, row, now);
|
||||
readsUsed += native.reads;
|
||||
const metricSkillIds = nativeMetricSyncSkillIds(existing, native);
|
||||
|
||||
const observationUnchanged = existing
|
||||
? sameFixtureObservation(existing, row, native.reconciliation)
|
||||
@@ -1149,13 +1233,16 @@ export const processFixtureBatchInternal = internalMutation({
|
||||
!existingAttempt &&
|
||||
(!existing || contentChanged || existing.scanStatus !== "planned");
|
||||
const entryWriteRequired = !run.dryRun;
|
||||
if (entryWriteRequired && writesUsed + 2 > run.budgets.maxWritesPerBatch) break;
|
||||
if (entryWriteRequired && writesUsed + 2 > run.budgets.maxWritesPerBatch) {
|
||||
break;
|
||||
}
|
||||
|
||||
incrementReconciliationCounts(counts, native.reconciliation);
|
||||
counts.observed += 1;
|
||||
cursor += 1;
|
||||
entriesProcessed += 1;
|
||||
if (shouldPlanScan) counts.scansPlanned += 1;
|
||||
let entryId = existing?._id;
|
||||
if (existing) {
|
||||
if (observationUnchanged) counts.unchanged += 1;
|
||||
else {
|
||||
@@ -1177,6 +1264,7 @@ export const processFixtureBatchInternal = internalMutation({
|
||||
githubContentHash: row.githubContentHash,
|
||||
sourceContentHash: row.sourceContentHash,
|
||||
installs: row.installs,
|
||||
githubStars: row.githubStars,
|
||||
sourceSnapshotId: fixture.snapshotId,
|
||||
reconciliation: native.reconciliation,
|
||||
// This gate has no publication seam; every catalog write reasserts dark visibility.
|
||||
@@ -1193,12 +1281,24 @@ export const processFixtureBatchInternal = internalMutation({
|
||||
});
|
||||
writesUsed += 1;
|
||||
}
|
||||
if (entryId && entryWriteRequired) {
|
||||
for (const skillId of metricSkillIds) {
|
||||
await ctx.scheduler.runAfter(
|
||||
0,
|
||||
internal.skillsShCatalog.syncNativeSkillMetricsFromCatalogEntryInternal,
|
||||
{
|
||||
entryId,
|
||||
skillId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
counts.wouldInsert += 1;
|
||||
if (!run.dryRun) {
|
||||
await ctx.db.insert("skillsShCatalogEntries", {
|
||||
entryId = await ctx.db.insert("skillsShCatalogEntries", {
|
||||
externalId: row.externalId,
|
||||
sourceKind: fixture.sourceKind,
|
||||
githubOwnerId: row.githubOwnerId,
|
||||
@@ -1213,6 +1313,7 @@ export const processFixtureBatchInternal = internalMutation({
|
||||
githubContentHash: row.githubContentHash,
|
||||
sourceContentHash: row.sourceContentHash,
|
||||
installs: row.installs,
|
||||
githubStars: row.githubStars,
|
||||
sourceSnapshotId: fixture.snapshotId,
|
||||
reconciliation: native.reconciliation,
|
||||
publicVisible: false,
|
||||
@@ -1224,6 +1325,16 @@ export const processFixtureBatchInternal = internalMutation({
|
||||
});
|
||||
writesUsed += 1;
|
||||
counts.inserted += 1;
|
||||
for (const skillId of metricSkillIds) {
|
||||
await ctx.scheduler.runAfter(
|
||||
0,
|
||||
internal.skillsShCatalog.syncNativeSkillMetricsFromCatalogEntryInternal,
|
||||
{
|
||||
entryId,
|
||||
skillId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2362,6 +2473,7 @@ export const rollbackFixtureRunInternal = internalMutation({
|
||||
}
|
||||
const fixture = getSkillsShCatalogFixture(run.fixtureId);
|
||||
let deletedEntries = 0;
|
||||
let nativeSkillsChanged = 0;
|
||||
for (let index = 0; index < fixture.length; index += 1) {
|
||||
const expected = normalizeIdentity(fixture.rowAt(index));
|
||||
const entry = await ctx.db
|
||||
@@ -2384,6 +2496,21 @@ export const rollbackFixtureRunInternal = internalMutation({
|
||||
`Controlled canary has retained scan history: ${expected.externalId}`,
|
||||
);
|
||||
}
|
||||
const nativeSkillId =
|
||||
entry.reconciliation?.kind === "exact-native"
|
||||
? entry.reconciliation.nativeSkillId
|
||||
: undefined;
|
||||
if (nativeSkillId) {
|
||||
const nativeSkill = await ctx.db.get(nativeSkillId);
|
||||
if (
|
||||
nativeSkill &&
|
||||
(nativeSkill.statsSkillsShInstalls !== undefined ||
|
||||
nativeSkill.statsGithubStars !== undefined)
|
||||
) {
|
||||
await ctx.db.patch(nativeSkillId, CLEARED_EXTERNAL_SKILL_METRICS);
|
||||
nativeSkillsChanged += 1;
|
||||
}
|
||||
}
|
||||
await ctx.db.delete(entry._id);
|
||||
deletedEntries += 1;
|
||||
}
|
||||
@@ -2392,7 +2519,7 @@ export const rollbackFixtureRunInternal = internalMutation({
|
||||
actor: args.actor.trim(),
|
||||
reason: args.reason.trim(),
|
||||
deletedEntries,
|
||||
nativeSkillsChanged: 0,
|
||||
nativeSkillsChanged,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -2760,6 +2887,7 @@ function sameFixtureObservation(
|
||||
existing.githubContentHash === row.githubContentHash &&
|
||||
existing.sourceContentHash === row.sourceContentHash &&
|
||||
existing.installs === row.installs &&
|
||||
existing.githubStars === row.githubStars &&
|
||||
existingReconciliation?.kind === reconciliation.kind &&
|
||||
existingReconciliation.nativeSkillId === reconciliation.nativeSkillId &&
|
||||
existingReconciliation.nativeSlug === reconciliation.nativeSlug &&
|
||||
|
||||
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { api, internal } from "./_generated/api";
|
||||
import type { Doc, Id } from "./_generated/dataModel";
|
||||
import canarySkillMarkdown from "./fixtures/patrick-html-canary-SKILL.txt?raw";
|
||||
import { extractDigestFields } from "./lib/skillSearchDigest";
|
||||
import schema from "./schema";
|
||||
|
||||
const modules = import.meta.glob("./**/*.ts");
|
||||
@@ -214,6 +215,11 @@ async function seedNativeSkill(
|
||||
options: {
|
||||
exactSource: boolean;
|
||||
downloads: number;
|
||||
bookmarks?: number;
|
||||
openClawInstalls?: number;
|
||||
skillsShInstalls?: number;
|
||||
githubStars?: number;
|
||||
seedDigest?: boolean;
|
||||
},
|
||||
) {
|
||||
return await t.run(async (ctx) => {
|
||||
@@ -250,26 +256,40 @@ async function seedNativeSkill(
|
||||
tags: {},
|
||||
moderationStatus: "active",
|
||||
statsDownloads: options.downloads,
|
||||
statsStars: 0,
|
||||
statsInstallsCurrent: 0,
|
||||
statsInstallsAllTime: 0,
|
||||
statsStars: options.bookmarks ?? 0,
|
||||
statsInstallsCurrent: options.openClawInstalls ?? 0,
|
||||
statsInstallsAllTime: options.openClawInstalls ?? 0,
|
||||
statsSkillsShInstalls: options.skillsShInstalls,
|
||||
statsGithubStars: options.githubStars,
|
||||
stats: {
|
||||
downloads: options.downloads,
|
||||
stars: 0,
|
||||
installsCurrent: 0,
|
||||
installsAllTime: 0,
|
||||
stars: options.bookmarks ?? 0,
|
||||
installsCurrent: options.openClawInstalls ?? 0,
|
||||
installsAllTime: options.openClawInstalls ?? 0,
|
||||
versions: 0,
|
||||
comments: 0,
|
||||
},
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
});
|
||||
if (options.seedDigest) {
|
||||
const skill = await ctx.db.get(skillId);
|
||||
if (!skill) throw new Error("seeded native skill missing");
|
||||
await ctx.db.insert("skillSearchDigest", {
|
||||
...extractDigestFields(skill),
|
||||
ownerHandle: "native-owner",
|
||||
ownerKind: "user",
|
||||
ownerName: "Native Owner",
|
||||
ownerDisplayName: "Native Owner",
|
||||
});
|
||||
}
|
||||
return skillId;
|
||||
});
|
||||
}
|
||||
|
||||
describe("skills.sh controlled hidden metadata canary", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
@@ -324,17 +344,33 @@ describe("skills.sh controlled hidden metadata canary", () => {
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("records an exact native match and preserves its downloads", async () => {
|
||||
it("attaches upstream metrics to an exact native match without rewriting native counters", async () => {
|
||||
vi.useFakeTimers();
|
||||
useEnvironment(LOCAL_ENV);
|
||||
const t = convexTest(schema, modules);
|
||||
const nativeSkillId = await seedNativeSkill(t, { exactSource: true, downloads: 143 });
|
||||
const nativeSkillId = await seedNativeSkill(t, {
|
||||
exactSource: true,
|
||||
downloads: 143,
|
||||
bookmarks: 5,
|
||||
openClawInstalls: 11,
|
||||
skillsShInstalls: 2,
|
||||
githubStars: 300,
|
||||
seedDigest: true,
|
||||
});
|
||||
await configureCanary(t);
|
||||
|
||||
const { runId, run } = await runCanary(t);
|
||||
await t.finishAllScheduledFunctions(vi.runAllTimers);
|
||||
const readback = await t.query(internal.skillsShCatalog.getRunReconciliationInternal, {
|
||||
runId,
|
||||
});
|
||||
const native = await t.run(async (ctx) => await ctx.db.get(nativeSkillId));
|
||||
const digest = await t.run(async (ctx) =>
|
||||
ctx.db
|
||||
.query("skillSearchDigest")
|
||||
.withIndex("by_skill", (q) => q.eq("skillId", nativeSkillId))
|
||||
.unique(),
|
||||
);
|
||||
|
||||
expect(run.counts).toMatchObject({
|
||||
newExternal: 0,
|
||||
@@ -352,10 +388,121 @@ describe("skills.sh controlled hidden metadata canary", () => {
|
||||
expect(native).toMatchObject({
|
||||
_id: nativeSkillId,
|
||||
statsDownloads: 143,
|
||||
stats: { downloads: 143 },
|
||||
statsStars: 5,
|
||||
statsInstallsCurrent: 11,
|
||||
statsInstallsAllTime: 11,
|
||||
statsSkillsShInstalls: 17,
|
||||
statsGithubStars: 321,
|
||||
stats: {
|
||||
downloads: 143,
|
||||
stars: 5,
|
||||
installsCurrent: 11,
|
||||
installsAllTime: 11,
|
||||
},
|
||||
githubCurrentCommit: CANARY_COMMIT,
|
||||
githubCurrentContentHash: CANARY_CONTENT_HASH,
|
||||
});
|
||||
expect(digest).toMatchObject({
|
||||
statsDownloads: 143,
|
||||
statsSkillsShInstalls: 17,
|
||||
statsGithubStars: 321,
|
||||
});
|
||||
});
|
||||
|
||||
it("clears exact-native upstream metrics when the controlled entry is removed", async () => {
|
||||
vi.useFakeTimers();
|
||||
useEnvironment(LOCAL_ENV);
|
||||
const t = convexTest(schema, modules);
|
||||
const nativeSkillId = await seedNativeSkill(t, {
|
||||
exactSource: true,
|
||||
downloads: 143,
|
||||
bookmarks: 5,
|
||||
openClawInstalls: 11,
|
||||
seedDigest: true,
|
||||
});
|
||||
await configureCanary(t);
|
||||
|
||||
const { runId } = await runCanary(t);
|
||||
await t.finishAllScheduledFunctions(vi.runAllTimers);
|
||||
const rollback = await t.mutation(internal.skillsShCatalog.rollbackFixtureRunInternal, {
|
||||
runId,
|
||||
actor: "codex-test",
|
||||
reason: "remove the controlled exact-native metadata",
|
||||
confirm: "rollback-skills-sh-controlled-canary",
|
||||
});
|
||||
const native = await t.run(async (ctx) => await ctx.db.get(nativeSkillId));
|
||||
const digest = await t.run(async (ctx) =>
|
||||
ctx.db
|
||||
.query("skillSearchDigest")
|
||||
.withIndex("by_skill", (q) => q.eq("skillId", nativeSkillId))
|
||||
.unique(),
|
||||
);
|
||||
|
||||
expect(rollback).toMatchObject({
|
||||
deletedEntries: 1,
|
||||
nativeSkillsChanged: 1,
|
||||
});
|
||||
expect(native?.statsSkillsShInstalls).toBeUndefined();
|
||||
expect(native?.statsGithubStars).toBeUndefined();
|
||||
expect(digest?.statsSkillsShInstalls).toBeUndefined();
|
||||
expect(digest?.statsGithubStars).toBeUndefined();
|
||||
expect(native).toMatchObject({
|
||||
statsDownloads: 143,
|
||||
statsStars: 5,
|
||||
statsInstallsCurrent: 11,
|
||||
statsInstallsAllTime: 11,
|
||||
stats: {
|
||||
downloads: 143,
|
||||
stars: 5,
|
||||
installsCurrent: 11,
|
||||
installsAllTime: 11,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("clears upstream metrics when an exact native match becomes a route collision", async () => {
|
||||
vi.useFakeTimers();
|
||||
useEnvironment(LOCAL_ENV);
|
||||
const t = convexTest(schema, modules);
|
||||
const nativeSkillId = await seedNativeSkill(t, {
|
||||
exactSource: true,
|
||||
downloads: 143,
|
||||
skillsShInstalls: 2,
|
||||
githubStars: 300,
|
||||
seedDigest: true,
|
||||
});
|
||||
await configureCanary(t);
|
||||
|
||||
await runCanary(t);
|
||||
await t.finishAllScheduledFunctions(vi.runAllTimers);
|
||||
await t.run(async (ctx) => {
|
||||
await ctx.db.patch(nativeSkillId, {
|
||||
githubCurrentCommit: "f".repeat(40),
|
||||
});
|
||||
});
|
||||
|
||||
const { run } = await runCanary(t);
|
||||
await t.finishAllScheduledFunctions(vi.runAllTimers);
|
||||
const native = await t.run(async (ctx) => await ctx.db.get(nativeSkillId));
|
||||
const digest = await t.run(async (ctx) =>
|
||||
ctx.db
|
||||
.query("skillSearchDigest")
|
||||
.withIndex("by_skill", (q) => q.eq("skillId", nativeSkillId))
|
||||
.unique(),
|
||||
);
|
||||
|
||||
expect(run.counts).toMatchObject({
|
||||
exactNativeMatches: 0,
|
||||
routeCollisions: 1,
|
||||
});
|
||||
expect(native?.statsSkillsShInstalls).toBeUndefined();
|
||||
expect(native?.statsGithubStars).toBeUndefined();
|
||||
expect(digest?.statsSkillsShInstalls).toBeUndefined();
|
||||
expect(digest?.statsGithubStars).toBeUndefined();
|
||||
expect(native).toMatchObject({
|
||||
statsDownloads: 143,
|
||||
stats: { downloads: 143 },
|
||||
});
|
||||
});
|
||||
|
||||
it("records a route collision without changing or attaching the native skill", async () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -96,7 +96,8 @@ Stores your API token + cached registry URL.
|
||||
|
||||
### `star <skill>` / `unstar <skill>`
|
||||
|
||||
- Adds/removes a skill from your highlights.
|
||||
- Adds/removes a skill from your Bookmarks. Command names remain `star` and
|
||||
`unstar` for compatibility.
|
||||
- Calls `POST /api/v1/stars/<slug>` and `DELETE /api/v1/stars/<slug>`.
|
||||
- `--yes` skips confirmation.
|
||||
|
||||
|
||||
+2
-1
@@ -1590,7 +1590,8 @@ Response:
|
||||
|
||||
### `POST /api/v1/stars/{slug}` / `DELETE /api/v1/stars/{slug}`
|
||||
|
||||
Add/remove a star (highlights). Both endpoints are idempotent.
|
||||
Add/remove a Bookmark. The legacy `stars` route and response field names remain
|
||||
for compatibility. Both endpoints are idempotent.
|
||||
|
||||
Responses:
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ test.skip(
|
||||
test.setTimeout(180_000);
|
||||
|
||||
async function gotoUntilStarButtonReady(page: Page, detailPath: string): Promise<Locator> {
|
||||
const starButton = page.getByRole("button", { name: "Star skill" });
|
||||
const starButton = page.getByRole("button", { name: "Bookmark skill" });
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 1; attempt <= 4; attempt += 1) {
|
||||
@@ -96,14 +96,14 @@ test("starring a skill survives refresh with the synchronized count", async ({
|
||||
|
||||
await starButton.click();
|
||||
|
||||
const unstarButton = page.getByRole("button", { name: "Unstar skill" });
|
||||
const unstarButton = page.getByRole("button", { name: "Remove bookmark" });
|
||||
await expect(unstarButton).toBeVisible({ timeout: 30_000 });
|
||||
await expect(unstarButton).toContainText("1", { timeout: 30_000 });
|
||||
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await waitForHydration(page);
|
||||
|
||||
const refreshedUnstarButton = page.getByRole("button", { name: "Unstar skill" });
|
||||
const refreshedUnstarButton = page.getByRole("button", { name: "Remove bookmark" });
|
||||
await expect(refreshedUnstarButton).toBeVisible({ timeout: 30_000 });
|
||||
await expect(refreshedUnstarButton).toContainText("1", { timeout: 30_000 });
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"skills-sh:capture-500": "bun scripts/skills-sh-catalog/capture-frozen-snapshot.ts",
|
||||
"skills-sh:prove-500": "bun scripts/skills-sh-catalog/prove-500.ts",
|
||||
"skills-sh:prove-canary": "CLAWHUB_TEST_CATALOG_MODE=controlled-canary bun scripts/skills-sh-catalog/run-test-gate.ts",
|
||||
"skills-sh:prove-mirror": "bun scripts/skills-sh-catalog/prove-mirror-test.ts",
|
||||
"skills:install": "npx --yes skills@1.5.16 add openclaw/design-system --skill openclaw-design openclaw-brand openclaw-design-system openclaw-marketing-pages openclaw-design-audit --agent codex --copy --yes",
|
||||
"test": "vitest run",
|
||||
"test:e2e": "vitest run -c vitest.e2e.config.ts",
|
||||
@@ -128,6 +129,7 @@
|
||||
"mermaid": "^11.16.0",
|
||||
"mime": "4.1.0",
|
||||
"monaco-editor": "0.56.0",
|
||||
"parse5": "8.0.1",
|
||||
"pino": "10.3.1",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7",
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildMirrorStepRequest,
|
||||
buildMirrorProofHeaders,
|
||||
capturedMirrorSourceRunId,
|
||||
findCompletedLiveMirrorRun,
|
||||
findRecoverableMirrorRun,
|
||||
mirrorRateLimitRetryDelayMs,
|
||||
reconcileMirrorRunToCompletion,
|
||||
resolveCompletedLiveMirrorRun,
|
||||
mirrorRunFromPayload,
|
||||
mirrorRunAccounting,
|
||||
} from "./prove-mirror-request";
|
||||
|
||||
describe("skills.sh mirror proof request headers", () => {
|
||||
it("carries the Test deployment protection bypass without changing operator auth", () => {
|
||||
expect(buildMirrorProofHeaders("operator-token", " bypass-secret ")).toEqual({
|
||||
Authorization: "Bearer operator-token",
|
||||
"Content-Type": "application/json",
|
||||
"x-vercel-protection-bypass": "bypass-secret",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits the bypass header outside protected deployments", () => {
|
||||
expect(buildMirrorProofHeaders("operator-token")).toEqual({
|
||||
Authorization: "Bearer operator-token",
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
});
|
||||
|
||||
it("selects the newest durable active run for interruption recovery", () => {
|
||||
expect(
|
||||
findRecoverableMirrorRun({
|
||||
runs: [
|
||||
{
|
||||
runId: "completed-run",
|
||||
status: "completed",
|
||||
page: 20,
|
||||
offset: 0,
|
||||
sourceTotal: 9_571,
|
||||
sourcePageSize: 500,
|
||||
sourceMeasuredAt: "2026-07-22T20:14:00.000Z",
|
||||
startedAt: 1,
|
||||
},
|
||||
{
|
||||
runId: "active-run",
|
||||
snapshotId: "skills-sh:2026-07-22T21:18:13.365Z:9571",
|
||||
status: "running",
|
||||
page: 0,
|
||||
offset: 50,
|
||||
sourceTotal: 9_571,
|
||||
sourcePageSize: 500,
|
||||
sourceMeasuredAt: "2026-07-22T21:18:13.365Z",
|
||||
startedAt: 2,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
runId: "active-run",
|
||||
snapshotId: "skills-sh:2026-07-22T21:18:13.365Z:9571",
|
||||
status: "running",
|
||||
page: 0,
|
||||
offset: 50,
|
||||
sourceTotal: 9_571,
|
||||
sourcePageSize: 500,
|
||||
sourceMeasuredAt: "2026-07-22T21:18:13.365Z",
|
||||
startedAt: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes direct and nested mirror run responses", () => {
|
||||
expect(mirrorRunFromPayload({ runId: "run", status: "completed" }, "reconcile")).toEqual({
|
||||
runId: "run",
|
||||
status: "completed",
|
||||
});
|
||||
expect(
|
||||
mirrorRunFromPayload(
|
||||
{ run: { runId: "run", status: "reconciling" }, cursor: "next" },
|
||||
"reconcile",
|
||||
),
|
||||
).toEqual({
|
||||
runId: "run",
|
||||
status: "reconciling",
|
||||
});
|
||||
expect(() => mirrorRunFromPayload({ runId: "run" }, "start-replay")).toThrow(
|
||||
'start-replay mirror response lacks run status: {"runId":"run"}',
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a resumed captured run on the replay operation and exact cursor", () => {
|
||||
expect(
|
||||
buildMirrorStepRequest({
|
||||
runId: "captured-run",
|
||||
page: 1,
|
||||
offset: 50,
|
||||
capturedSource: {
|
||||
externalIds: Array.from({ length: 175 }, (_, index) => `owner/repo/skill-${index}`),
|
||||
sourcePageSize: 100,
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
operation: "step-replay",
|
||||
runId: "captured-run",
|
||||
page: 1,
|
||||
offset: 50,
|
||||
pageLength: 75,
|
||||
hasMore: false,
|
||||
sourceTotal: 175,
|
||||
externalIds: Array.from({ length: 25 }, (_, index) => `owner/repo/skill-${index + 150}`),
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps live source steps bound to the server-side run", () => {
|
||||
expect(
|
||||
buildMirrorStepRequest({
|
||||
runId: "live-run",
|
||||
page: 3,
|
||||
offset: 50,
|
||||
}),
|
||||
).toEqual({
|
||||
operation: "step",
|
||||
runId: "live-run",
|
||||
page: 3,
|
||||
offset: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes reconciliation responses through completion", async () => {
|
||||
const responses = [
|
||||
{ run: { runId: "run", status: "reconciling", page: 20, offset: 0 } },
|
||||
{ runId: "run", status: "completed", page: 20, offset: 0 },
|
||||
];
|
||||
const reconcile = vi.fn(async () => responses.shift()!);
|
||||
|
||||
await expect(
|
||||
reconcileMirrorRunToCompletion(
|
||||
{ runId: "run", status: "reconciling", page: 20, offset: 0 },
|
||||
reconcile,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
run: { runId: "run", status: "completed", page: 20, offset: 0 },
|
||||
reconciliationBatches: 2,
|
||||
});
|
||||
expect(reconcile).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("ties a captured replay recovery to its completed authenticated source run", () => {
|
||||
const liveRun = {
|
||||
runId: "live-run",
|
||||
snapshotId: "skills-sh:2026-07-22T21:18:13.365Z:9571",
|
||||
status: "completed",
|
||||
page: 20,
|
||||
offset: 0,
|
||||
sourceTotal: 9_571,
|
||||
sourcePageSize: 500,
|
||||
sourceMeasuredAt: "2026-07-22T21:18:13.365Z",
|
||||
startedAt: 1,
|
||||
completedAt: 2,
|
||||
counts: { observed: 9_571 },
|
||||
operations: { sourceRequests: 18_360 },
|
||||
};
|
||||
const payload = { runs: [{ ...liveRun, status: "running" }, liveRun] };
|
||||
|
||||
expect(capturedMirrorSourceRunId("skills-sh-captured:live-run")).toBe("live-run");
|
||||
expect(capturedMirrorSourceRunId("skills-sh:live-run")).toBeNull();
|
||||
expect(findCompletedLiveMirrorRun(payload, "live-run")).toEqual(liveRun);
|
||||
expect(findCompletedLiveMirrorRun(liveRun, "live-run")).toEqual(liveRun);
|
||||
expect(findCompletedLiveMirrorRun(payload, "missing")).toBeNull();
|
||||
});
|
||||
|
||||
it("resolves a completed captured ancestor to its authenticated live source run", async () => {
|
||||
const liveRun = {
|
||||
runId: "live-run",
|
||||
snapshotId: "skills-sh:2026-07-22T21:18:13.365Z:9571",
|
||||
status: "completed" as const,
|
||||
page: 20,
|
||||
offset: 0,
|
||||
sourceTotal: 9_571,
|
||||
sourcePageSize: 500,
|
||||
sourceMeasuredAt: "2026-07-22T21:18:13.365Z",
|
||||
startedAt: 1,
|
||||
completedAt: 2,
|
||||
counts: { observed: 9_571 },
|
||||
operations: { sourceRequests: 18_360 },
|
||||
};
|
||||
const capturedRun = {
|
||||
...liveRun,
|
||||
runId: "captured-run",
|
||||
snapshotId: "skills-sh-captured:live-run",
|
||||
startedAt: 3,
|
||||
completedAt: 4,
|
||||
};
|
||||
const readRun = vi.fn(async (runId: string) => {
|
||||
expect(runId).toBe("live-run");
|
||||
return liveRun;
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveCompletedLiveMirrorRun({
|
||||
payload: { runs: [capturedRun] },
|
||||
runId: "captured-run",
|
||||
readRun,
|
||||
}),
|
||||
).resolves.toEqual(liveRun);
|
||||
expect(readRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("bounds cyclic captured-run lineage", async () => {
|
||||
const completedRun = {
|
||||
status: "completed" as const,
|
||||
page: 20,
|
||||
offset: 0,
|
||||
sourceTotal: 9_571,
|
||||
sourcePageSize: 500,
|
||||
sourceMeasuredAt: "2026-07-22T21:18:13.365Z",
|
||||
startedAt: 1,
|
||||
completedAt: 2,
|
||||
counts: { observed: 9_571 },
|
||||
operations: { sourceRequests: 18_360 },
|
||||
};
|
||||
const first = {
|
||||
...completedRun,
|
||||
runId: "first",
|
||||
snapshotId: "skills-sh-captured:second",
|
||||
};
|
||||
const second = {
|
||||
...completedRun,
|
||||
runId: "second",
|
||||
snapshotId: "skills-sh-captured:first",
|
||||
};
|
||||
const readRun = vi.fn(async (runId: string) => (runId === "first" ? first : second));
|
||||
|
||||
await expect(
|
||||
resolveCompletedLiveMirrorRun({
|
||||
payload: { runs: [first] },
|
||||
runId: "first",
|
||||
readRun,
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
expect(readRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("treats a missing captured ancestor as stale lineage", async () => {
|
||||
const capturedRun = {
|
||||
runId: "captured-run",
|
||||
snapshotId: "skills-sh-captured:missing-live-run",
|
||||
status: "completed" as const,
|
||||
page: 20,
|
||||
offset: 0,
|
||||
sourceTotal: 9_571,
|
||||
sourcePageSize: 500,
|
||||
sourceMeasuredAt: "2026-07-22T21:18:13.365Z",
|
||||
startedAt: 1,
|
||||
completedAt: 2,
|
||||
counts: { observed: 9_571 },
|
||||
operations: { sourceRequests: 18_360 },
|
||||
};
|
||||
const readRun = vi.fn(async () => null);
|
||||
|
||||
await expect(
|
||||
resolveCompletedLiveMirrorRun({
|
||||
payload: { runs: [capturedRun] },
|
||||
runId: "captured-run",
|
||||
readRun,
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("fails closed without canceling when captured lineage exceeds the read bound", async () => {
|
||||
const completedRun = {
|
||||
status: "completed" as const,
|
||||
page: 20,
|
||||
offset: 0,
|
||||
sourceTotal: 9_571,
|
||||
sourcePageSize: 500,
|
||||
sourceMeasuredAt: "2026-07-22T21:18:13.365Z",
|
||||
startedAt: 1,
|
||||
completedAt: 2,
|
||||
counts: { observed: 9_571 },
|
||||
operations: { sourceRequests: 18_360 },
|
||||
};
|
||||
const runs = new Map(
|
||||
Array.from({ length: 9 }, (_, index) => [
|
||||
`captured-${index}`,
|
||||
{
|
||||
...completedRun,
|
||||
runId: `captured-${index}`,
|
||||
snapshotId: `skills-sh-captured:captured-${index + 1}`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolveCompletedLiveMirrorRun({
|
||||
payload: { runs: [runs.get("captured-0")] },
|
||||
runId: "captured-0",
|
||||
readRun: async (runId) => runs.get(runId) ?? null,
|
||||
}),
|
||||
).rejects.toThrow("captured mirror lineage exceeded 8 runs");
|
||||
});
|
||||
|
||||
it("bounds rate-limit recovery delays while preserving Retry-After", () => {
|
||||
expect(mirrorRateLimitRetryDelayMs(429, "17", 0)).toBe(17_000);
|
||||
expect(mirrorRateLimitRetryDelayMs(429, "120", 0)).toBe(120_000);
|
||||
expect(mirrorRateLimitRetryDelayMs(429, null, 3)).toBe(8_000);
|
||||
expect(mirrorRateLimitRetryDelayMs(502, "17", 0)).toBeNull();
|
||||
});
|
||||
|
||||
it("accounts fail-closed identity conflicts separately from source quarantines", () => {
|
||||
expect(
|
||||
mirrorRunAccounting(9_571, {
|
||||
conflicts: 205,
|
||||
rejected: 205,
|
||||
quarantined: 166,
|
||||
}),
|
||||
).toEqual({
|
||||
accepted: 9_366,
|
||||
rejected: 205,
|
||||
quarantined: 166,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects unrecorded failures and impossible quarantine counts", () => {
|
||||
expect(() =>
|
||||
mirrorRunAccounting(100, {
|
||||
rejected: 0,
|
||||
quarantined: 0,
|
||||
}),
|
||||
).toThrow("mirror conflicts must be a nonnegative integer");
|
||||
expect(() =>
|
||||
mirrorRunAccounting(100, {
|
||||
conflicts: 0,
|
||||
rejected: Number.NaN,
|
||||
quarantined: 0,
|
||||
}),
|
||||
).toThrow("mirror rejected must be a nonnegative integer");
|
||||
expect(() =>
|
||||
mirrorRunAccounting(100, {
|
||||
conflicts: 4,
|
||||
rejected: 5,
|
||||
quarantined: 3,
|
||||
}),
|
||||
).toThrow("mirror conflict accounting");
|
||||
expect(() =>
|
||||
mirrorRunAccounting(100, {
|
||||
conflicts: 5,
|
||||
rejected: 5,
|
||||
quarantined: 6,
|
||||
}),
|
||||
).toThrow("mirror quarantine accounting");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,268 @@
|
||||
export function buildMirrorProofHeaders(
|
||||
operatorAuthorization: string,
|
||||
vercelAutomationBypassSecret?: string,
|
||||
) {
|
||||
return {
|
||||
Authorization: `Bearer ${operatorAuthorization}`,
|
||||
"Content-Type": "application/json",
|
||||
...(vercelAutomationBypassSecret?.trim()
|
||||
? { "x-vercel-protection-bypass": vercelAutomationBypassSecret.trim() }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function mirrorRateLimitRetryDelayMs(
|
||||
status: number,
|
||||
retryAfterHeader: string | null,
|
||||
attempt: number,
|
||||
) {
|
||||
if (status !== 429) return null;
|
||||
const retryAfterSeconds = retryAfterHeader === null ? Number.NaN : Number(retryAfterHeader);
|
||||
const requestedMs =
|
||||
Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0
|
||||
? retryAfterSeconds * 1_000
|
||||
: 1_000 * 2 ** attempt;
|
||||
return Math.max(1_000, requestedMs);
|
||||
}
|
||||
|
||||
export function mirrorRunAccounting(total: number, counts: Record<string, number>) {
|
||||
if (!Number.isSafeInteger(total) || total < 0) {
|
||||
throw new Error("mirror source total must be a nonnegative integer");
|
||||
}
|
||||
const requiredCount = (name: "conflicts" | "rejected" | "quarantined") => {
|
||||
const value = counts[name];
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw new Error(`mirror ${name} must be a nonnegative integer`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
const rejected = requiredCount("rejected");
|
||||
const quarantined = requiredCount("quarantined");
|
||||
const conflicts = requiredCount("conflicts");
|
||||
if (conflicts !== rejected) {
|
||||
throw new Error("mirror conflict accounting does not equal rejected rows");
|
||||
}
|
||||
if (quarantined > rejected) {
|
||||
throw new Error("mirror quarantine accounting exceeds rejected rows");
|
||||
}
|
||||
const accepted = total - rejected;
|
||||
if (accepted < 0) {
|
||||
throw new Error("mirror rejected rows exceed the source total");
|
||||
}
|
||||
return { accepted, rejected, quarantined };
|
||||
}
|
||||
|
||||
export function mirrorRunFromPayload(
|
||||
payload: Record<string, unknown>,
|
||||
operation: string,
|
||||
): Record<string, unknown> {
|
||||
const nested = payload.run;
|
||||
const candidates = [
|
||||
payload,
|
||||
nested && typeof nested === "object" && !Array.isArray(nested)
|
||||
? (nested as Record<string, unknown>)
|
||||
: null,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (
|
||||
candidate &&
|
||||
["running", "paused", "reconciling", "completed", "failed"].includes(String(candidate.status))
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
const diagnostic = JSON.stringify(payload).slice(0, 1_000);
|
||||
throw new Error(`${operation} mirror response lacks run status: ${diagnostic}`);
|
||||
}
|
||||
|
||||
type CapturedMirrorStepSource = {
|
||||
externalIds: string[];
|
||||
sourcePageSize: number;
|
||||
};
|
||||
|
||||
export function buildMirrorStepRequest(args: {
|
||||
runId: string;
|
||||
page: number;
|
||||
offset: number;
|
||||
capturedSource?: CapturedMirrorStepSource | null;
|
||||
}) {
|
||||
if (!args.capturedSource) {
|
||||
return {
|
||||
operation: "step" as const,
|
||||
runId: args.runId,
|
||||
page: args.page,
|
||||
offset: args.offset,
|
||||
};
|
||||
}
|
||||
const { externalIds, sourcePageSize } = args.capturedSource;
|
||||
const pageStart = args.page * sourcePageSize;
|
||||
const pageLength = Math.min(sourcePageSize, externalIds.length - pageStart);
|
||||
const rowStart = pageStart + args.offset;
|
||||
const rows = externalIds.slice(rowStart, rowStart + 50);
|
||||
if (pageLength < 1 || rows.length < 1) {
|
||||
throw new Error(`captured mirror replay has no rows for cursor ${args.page}:${args.offset}`);
|
||||
}
|
||||
return {
|
||||
operation: "step-replay" as const,
|
||||
runId: args.runId,
|
||||
page: args.page,
|
||||
offset: args.offset,
|
||||
pageLength,
|
||||
hasMore: pageStart + sourcePageSize < externalIds.length,
|
||||
sourceTotal: externalIds.length,
|
||||
externalIds: rows,
|
||||
};
|
||||
}
|
||||
|
||||
export async function reconcileMirrorRunToCompletion(
|
||||
initialRun: Record<string, unknown>,
|
||||
reconcile: () => Promise<Record<string, unknown>>,
|
||||
) {
|
||||
let run = initialRun;
|
||||
let reconciliationBatches = 0;
|
||||
while (run.status === "reconciling") {
|
||||
if (reconciliationBatches >= 1_000) {
|
||||
throw new Error("mirror reconciliation exceeded 1000 bounded batches");
|
||||
}
|
||||
run = mirrorRunFromPayload(await reconcile(), "reconcile");
|
||||
reconciliationBatches += 1;
|
||||
}
|
||||
return { run, reconciliationBatches };
|
||||
}
|
||||
|
||||
export type RecoverableMirrorRun = {
|
||||
runId: string;
|
||||
snapshotId: string;
|
||||
status: "running" | "paused" | "reconciling";
|
||||
page: number;
|
||||
offset: number;
|
||||
sourceTotal: number;
|
||||
sourcePageSize: number;
|
||||
sourceMeasuredAt: string;
|
||||
startedAt: number;
|
||||
};
|
||||
|
||||
export function findRecoverableMirrorRun(
|
||||
payload: Record<string, unknown>,
|
||||
): RecoverableMirrorRun | null {
|
||||
if (!Array.isArray(payload.runs)) return null;
|
||||
for (const value of payload.runs) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
||||
const run = value as Record<string, unknown>;
|
||||
if (
|
||||
!["running", "paused", "reconciling"].includes(String(run.status)) ||
|
||||
typeof run.runId !== "string" ||
|
||||
typeof run.snapshotId !== "string" ||
|
||||
typeof run.page !== "number" ||
|
||||
typeof run.offset !== "number" ||
|
||||
typeof run.sourceTotal !== "number" ||
|
||||
typeof run.sourcePageSize !== "number" ||
|
||||
typeof run.sourceMeasuredAt !== "string" ||
|
||||
typeof run.startedAt !== "number"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
return run as RecoverableMirrorRun;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export type CompletedLiveMirrorRun = {
|
||||
runId: string;
|
||||
snapshotId: string;
|
||||
status: "completed";
|
||||
page: number;
|
||||
offset: number;
|
||||
sourceTotal: number;
|
||||
sourcePageSize: number;
|
||||
sourceMeasuredAt: string;
|
||||
startedAt: number;
|
||||
completedAt: number;
|
||||
counts: Record<string, number>;
|
||||
operations: Record<string, number>;
|
||||
};
|
||||
|
||||
type CompletedMirrorRun = CompletedLiveMirrorRun;
|
||||
|
||||
export function capturedMirrorSourceRunId(snapshotId: string) {
|
||||
const prefix = "skills-sh-captured:";
|
||||
return snapshotId.startsWith(prefix) ? snapshotId.slice(prefix.length) || null : null;
|
||||
}
|
||||
|
||||
function findCompletedMirrorRun(
|
||||
payload: unknown,
|
||||
runId?: string | null,
|
||||
): CompletedMirrorRun | null {
|
||||
const root =
|
||||
payload && typeof payload === "object" && !Array.isArray(payload)
|
||||
? (payload as Record<string, unknown>)
|
||||
: null;
|
||||
const candidates = root ? [root, ...(Array.isArray(root.runs) ? root.runs : [])] : [];
|
||||
for (const value of candidates) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
||||
const run = value as Record<string, unknown>;
|
||||
if (
|
||||
run.status !== "completed" ||
|
||||
typeof run.runId !== "string" ||
|
||||
(runId && run.runId !== runId) ||
|
||||
typeof run.snapshotId !== "string" ||
|
||||
typeof run.page !== "number" ||
|
||||
typeof run.offset !== "number" ||
|
||||
typeof run.sourceTotal !== "number" ||
|
||||
typeof run.sourcePageSize !== "number" ||
|
||||
typeof run.sourceMeasuredAt !== "string" ||
|
||||
typeof run.startedAt !== "number" ||
|
||||
typeof run.completedAt !== "number" ||
|
||||
!run.counts ||
|
||||
typeof run.counts !== "object" ||
|
||||
Array.isArray(run.counts) ||
|
||||
!run.operations ||
|
||||
typeof run.operations !== "object" ||
|
||||
Array.isArray(run.operations)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
return run as CompletedMirrorRun;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findCompletedLiveMirrorRun(
|
||||
payload: unknown,
|
||||
runId?: string | null,
|
||||
): CompletedLiveMirrorRun | null {
|
||||
const run = findCompletedMirrorRun(payload, runId);
|
||||
if (
|
||||
!run ||
|
||||
!run.snapshotId.startsWith("skills-sh:") ||
|
||||
run.snapshotId.startsWith("skills-sh-captured:")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return run;
|
||||
}
|
||||
|
||||
export async function resolveCompletedLiveMirrorRun(args: {
|
||||
payload: unknown;
|
||||
runId: string;
|
||||
readRun: (runId: string) => Promise<unknown>;
|
||||
}) {
|
||||
const seen = new Set<string>();
|
||||
let runId = args.runId;
|
||||
let payload = args.payload;
|
||||
for (let depth = 0; depth < 8; depth += 1) {
|
||||
if (seen.has(runId)) return null;
|
||||
seen.add(runId);
|
||||
let run = findCompletedMirrorRun(payload, runId);
|
||||
if (!run) {
|
||||
payload = await args.readRun(runId);
|
||||
run = findCompletedMirrorRun(payload, runId);
|
||||
}
|
||||
if (!run) return null;
|
||||
const capturedRunId = capturedMirrorSourceRunId(run.snapshotId);
|
||||
if (!capturedRunId) return findCompletedLiveMirrorRun(run, runId);
|
||||
runId = capturedRunId;
|
||||
payload = {};
|
||||
}
|
||||
throw new Error("captured mirror lineage exceeded 8 runs");
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import {
|
||||
SKILLS_SH_MIRROR_CONTROLLED_EXTERNAL_IDS,
|
||||
parseSkillsShMirrorProofSnapshotId,
|
||||
} from "../../server/skillsShCatalogSource";
|
||||
import {
|
||||
buildMirrorStepRequest,
|
||||
buildMirrorProofHeaders,
|
||||
capturedMirrorSourceRunId,
|
||||
findRecoverableMirrorRun,
|
||||
mirrorRateLimitRetryDelayMs,
|
||||
reconcileMirrorRunToCompletion,
|
||||
resolveCompletedLiveMirrorRun,
|
||||
mirrorRunFromPayload,
|
||||
mirrorRunAccounting,
|
||||
type CompletedLiveMirrorRun,
|
||||
type RecoverableMirrorRun,
|
||||
} from "./prove-mirror-request";
|
||||
|
||||
const OUTPUT_PATH = resolve("proof/claw-563/skills-sh-mirror-test-proof.json");
|
||||
const PROJECTED_SCALE = 700_000;
|
||||
const MAX_RATE_LIMIT_RETRIES_PER_RUN = 30;
|
||||
const MAX_RATE_LIMIT_WAIT_MS_PER_RUN = 30 * 60 * 1_000;
|
||||
const MAX_DETAIL_PAGE_ROWS = 50;
|
||||
|
||||
function requireEnv(name: string) {
|
||||
const value = process.env[name]?.trim();
|
||||
if (!value) throw new Error(`${name} is required`);
|
||||
return value;
|
||||
}
|
||||
|
||||
const targetUrl = requireEnv("CLAWHUB_TEST_MIRROR_GATE_URL");
|
||||
const operatorAuthorization = requireEnv("CLAWHUB_TEST_OPERATOR_TOKEN");
|
||||
const vercelAutomationBypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
||||
|
||||
async function callRaw(body: Record<string, unknown>) {
|
||||
const startedAt = performance.now();
|
||||
const response = await fetch(targetUrl, {
|
||||
method: "POST",
|
||||
headers: buildMirrorProofHeaders(operatorAuthorization, vercelAutomationBypassSecret),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const text = await response.text();
|
||||
let payload: Record<string, unknown>;
|
||||
try {
|
||||
payload = JSON.parse(text) as Record<string, unknown>;
|
||||
} catch {
|
||||
payload = { text };
|
||||
}
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
elapsedMs: performance.now() - startedAt,
|
||||
payload,
|
||||
retryAfter: response.headers.get("retry-after"),
|
||||
};
|
||||
}
|
||||
|
||||
function callFailure(body: Record<string, unknown>, result: Awaited<ReturnType<typeof callRaw>>) {
|
||||
return new Error(
|
||||
`${String(body.operation)} returned HTTP ${result.status}: ${JSON.stringify(result.payload)}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function call(body: Record<string, unknown>) {
|
||||
const result = await callRaw(body);
|
||||
if (!result.ok) throw callFailure(body, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function requireRunId(payload: Record<string, unknown>) {
|
||||
if (typeof payload.runId !== "string") throw new Error("mirror start did not return runId");
|
||||
return payload.runId;
|
||||
}
|
||||
|
||||
async function runMirror(
|
||||
reason: string,
|
||||
provePauseResume: boolean,
|
||||
recoverableRun: RecoverableMirrorRun | null = null,
|
||||
capturedSource: CapturedMirrorSource | null = null,
|
||||
) {
|
||||
const attemptStartedAt = Date.now();
|
||||
const start = recoverableRun
|
||||
? { payload: recoverableRun }
|
||||
: capturedSource
|
||||
? await call({
|
||||
operation: "start-replay",
|
||||
reason,
|
||||
capturedRunId: capturedSource.capturedRunId,
|
||||
sourceTotal: capturedSource.externalIds.length,
|
||||
sourcePageSize: capturedSource.sourcePageSize,
|
||||
sourceMeasuredAt: capturedSource.sourceMeasuredAt,
|
||||
})
|
||||
: await call({ operation: "start", reason });
|
||||
const startRun = mirrorRunFromPayload(
|
||||
start.payload,
|
||||
recoverableRun ? "recover" : capturedSource ? "start-replay" : "start",
|
||||
);
|
||||
const startPayload = start.payload as Record<string, unknown>;
|
||||
const runId = requireRunId(startRun);
|
||||
const proofSource =
|
||||
capturedSource === null
|
||||
? parseSkillsShMirrorProofSnapshotId(String(startRun.snapshotId))
|
||||
: null;
|
||||
let page = recoverableRun?.page ?? 0;
|
||||
let offset = recoverableRun?.offset ?? 0;
|
||||
let steps = 0;
|
||||
let pauseProof: Record<string, unknown> | null = null;
|
||||
let run = startRun;
|
||||
let recovery: Record<string, unknown> | null = null;
|
||||
let rateLimitRetries = 0;
|
||||
let rateLimitWaitMs = 0;
|
||||
if (recoverableRun) {
|
||||
recovery = {
|
||||
runId,
|
||||
status: recoverableRun.status,
|
||||
cursor: { page, offset },
|
||||
interruptedAtLeastOnce: true,
|
||||
};
|
||||
if (recoverableRun.status === "paused") {
|
||||
run = (
|
||||
await call({
|
||||
operation: "resume",
|
||||
runId,
|
||||
reason: "CLAW-563 interrupted run recovery",
|
||||
})
|
||||
).payload;
|
||||
}
|
||||
}
|
||||
while (run.status === "running") {
|
||||
const stepRequest = buildMirrorStepRequest({
|
||||
runId,
|
||||
page,
|
||||
offset,
|
||||
capturedSource,
|
||||
});
|
||||
const step = await callRaw(stepRequest);
|
||||
if (!step.ok) {
|
||||
const delayMs = mirrorRateLimitRetryDelayMs(step.status, step.retryAfter, rateLimitRetries);
|
||||
if (
|
||||
delayMs === null ||
|
||||
rateLimitRetries >= MAX_RATE_LIMIT_RETRIES_PER_RUN ||
|
||||
rateLimitWaitMs + delayMs > MAX_RATE_LIMIT_WAIT_MS_PER_RUN
|
||||
) {
|
||||
throw callFailure(stepRequest, step);
|
||||
}
|
||||
rateLimitRetries += 1;
|
||||
rateLimitWaitMs += delayMs;
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
continue;
|
||||
}
|
||||
run = mirrorRunFromPayload(step.payload, String(stepRequest.operation));
|
||||
steps += 1;
|
||||
if (typeof run.page !== "number" || typeof run.offset !== "number") {
|
||||
throw new Error("mirror step did not return a durable cursor");
|
||||
}
|
||||
page = run.page;
|
||||
offset = run.offset;
|
||||
if (provePauseResume && steps === 1) {
|
||||
const pause = await call({
|
||||
operation: "pause",
|
||||
runId,
|
||||
reason: "CLAW-563 deliberate pause proof",
|
||||
});
|
||||
const blocked = await callRaw(
|
||||
buildMirrorStepRequest({
|
||||
runId,
|
||||
page,
|
||||
offset,
|
||||
capturedSource,
|
||||
}),
|
||||
);
|
||||
if (blocked.ok || !JSON.stringify(blocked.payload).includes("paused")) {
|
||||
throw new Error("paused mirror step did not fail closed");
|
||||
}
|
||||
const resume = await call({
|
||||
operation: "resume",
|
||||
runId,
|
||||
reason: "CLAW-563 exact cursor resume proof",
|
||||
});
|
||||
pauseProof = {
|
||||
pause: pause.payload,
|
||||
blockedStatus: blocked.status,
|
||||
blockedPayload: blocked.payload,
|
||||
resume: resume.payload,
|
||||
resumeCursor: { page, offset },
|
||||
};
|
||||
}
|
||||
}
|
||||
const reconciliation = await reconcileMirrorRunToCompletion(run, async () => {
|
||||
const result = await call({ operation: "reconcile", runId, limit: 250 });
|
||||
return result.payload;
|
||||
});
|
||||
run = reconciliation.run;
|
||||
const reconciliationBatches = reconciliation.reconciliationBatches;
|
||||
if (run.status !== "completed") {
|
||||
throw new Error(`mirror run ended in unexpected status ${String(run.status)}`);
|
||||
}
|
||||
return {
|
||||
runId,
|
||||
run,
|
||||
source: {
|
||||
total: startPayload.sourceTotal,
|
||||
catalogTotal: proofSource?.catalogTotal ?? startPayload.sourceTotal,
|
||||
controlledExternalIds: proofSource?.controlledExternalIds ?? [],
|
||||
controlledOverlayExternalIds: proofSource?.controlledOverlayExternalIds ?? [],
|
||||
controlledSupplementExternalIds: proofSource?.controlledSupplementExternalIds ?? [],
|
||||
evidence: proofSource?.evidence ?? null,
|
||||
capture:
|
||||
startPayload.sourceCapture &&
|
||||
typeof startPayload.sourceCapture === "object" &&
|
||||
!Array.isArray(startPayload.sourceCapture)
|
||||
? startPayload.sourceCapture
|
||||
: null,
|
||||
measuredAt: startPayload.sourceMeasuredAt,
|
||||
pageSize: startPayload.sourcePageSize,
|
||||
},
|
||||
recovery,
|
||||
steps,
|
||||
reconciliationBatches,
|
||||
pauseProof,
|
||||
rateLimitRecovery: {
|
||||
retries: rateLimitRetries,
|
||||
waitedMs: rateLimitWaitMs,
|
||||
retryBudget: MAX_RATE_LIMIT_RETRIES_PER_RUN,
|
||||
waitBudgetMs: MAX_RATE_LIMIT_WAIT_MS_PER_RUN,
|
||||
},
|
||||
elapsedMs: Date.now() - (recoverableRun?.startedAt ?? attemptStartedAt),
|
||||
attemptElapsedMs: Date.now() - attemptStartedAt,
|
||||
};
|
||||
}
|
||||
|
||||
async function collectPages(
|
||||
operation: "page" | "detail-page" | "facet-page",
|
||||
validate: (document: Record<string, unknown>) => void,
|
||||
) {
|
||||
const documents: Record<string, unknown>[] = [];
|
||||
let cursor: string | null = null;
|
||||
let calls = 0;
|
||||
let count = 0;
|
||||
let serializedBytes = 0;
|
||||
do {
|
||||
const result = await call({
|
||||
operation,
|
||||
cursor,
|
||||
limit: operation === "detail-page" ? MAX_DETAIL_PAGE_ROWS : 500,
|
||||
});
|
||||
const page = result.payload.page;
|
||||
if (!Array.isArray(page)) throw new Error(`${operation} did not return a page`);
|
||||
for (const document of page as Record<string, unknown>[]) {
|
||||
validate(document);
|
||||
count += 1;
|
||||
serializedBytes += Buffer.byteLength(JSON.stringify(document), "utf8");
|
||||
if (operation === "page") documents.push(document);
|
||||
}
|
||||
calls += 1;
|
||||
cursor =
|
||||
result.payload.isDone === true
|
||||
? null
|
||||
: typeof result.payload.continueCursor === "string"
|
||||
? result.payload.continueCursor
|
||||
: null;
|
||||
if (result.payload.isDone !== true && cursor === null) {
|
||||
throw new Error(`${operation} did not return a continuation cursor`);
|
||||
}
|
||||
} while (cursor);
|
||||
return {
|
||||
count,
|
||||
calls,
|
||||
serializedBytes,
|
||||
documents,
|
||||
};
|
||||
}
|
||||
|
||||
type MirrorRunProof = Awaited<ReturnType<typeof runMirror>>;
|
||||
type CapturedMirrorSource = {
|
||||
capturedRunId: string;
|
||||
externalIds: string[];
|
||||
sourceMeasuredAt: string;
|
||||
sourcePageSize: number;
|
||||
};
|
||||
|
||||
function completedLiveRunProof(run: CompletedLiveMirrorRun): MirrorRunProof {
|
||||
const proofSource = parseSkillsShMirrorProofSnapshotId(run.snapshotId);
|
||||
return {
|
||||
runId: run.runId,
|
||||
run,
|
||||
source: {
|
||||
total: run.sourceTotal,
|
||||
catalogTotal: proofSource.catalogTotal,
|
||||
controlledExternalIds: proofSource.controlledExternalIds,
|
||||
controlledOverlayExternalIds: proofSource.controlledOverlayExternalIds,
|
||||
controlledSupplementExternalIds: proofSource.controlledSupplementExternalIds,
|
||||
evidence: proofSource.evidence ?? null,
|
||||
capture: null,
|
||||
measuredAt: run.sourceMeasuredAt,
|
||||
pageSize: run.sourcePageSize,
|
||||
},
|
||||
recovery: {
|
||||
runId: run.runId,
|
||||
status: run.status,
|
||||
cursor: { page: run.page, offset: run.offset },
|
||||
completedBeforeProofContinuation: true,
|
||||
},
|
||||
steps: 0,
|
||||
reconciliationBatches: 0,
|
||||
pauseProof: null,
|
||||
rateLimitRecovery: {
|
||||
retries: 0,
|
||||
waitedMs: 0,
|
||||
retryBudget: MAX_RATE_LIMIT_RETRIES_PER_RUN,
|
||||
waitBudgetMs: MAX_RATE_LIMIT_WAIT_MS_PER_RUN,
|
||||
},
|
||||
elapsedMs: run.completedAt - run.startedAt,
|
||||
attemptElapsedMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function runCounts(runResult: MirrorRunProof) {
|
||||
const counts = runResult.run.counts;
|
||||
if (!counts || typeof counts !== "object") throw new Error("mirror run lacks counts");
|
||||
return counts as Record<string, number>;
|
||||
}
|
||||
|
||||
function assertZeroCounts(counts: Record<string, number>, names: string[]) {
|
||||
for (const name of names) {
|
||||
if (counts[name] !== 0) throw new Error(`expected ${name}=0, received ${String(counts[name])}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertRunProof(runResult: MirrorRunProof, mode: "first" | "identical") {
|
||||
const counts = runCounts(runResult);
|
||||
const total = Number(runResult.source.total);
|
||||
const { accepted } = mirrorRunAccounting(total, counts);
|
||||
if (counts.observed !== total) {
|
||||
throw new Error(`mirror observed ${String(counts.observed)} of ${total} source rows`);
|
||||
}
|
||||
assertZeroCounts(counts, ["scansPlanned", "scansAdmitted"]);
|
||||
if ((counts.inserted ?? 0) + (counts.updated ?? 0) + (counts.unchanged ?? 0) !== accepted) {
|
||||
throw new Error(`${mode} run digest accounting does not equal the source total`);
|
||||
}
|
||||
if (
|
||||
(counts.detailsInserted ?? 0) +
|
||||
(counts.detailsUpdated ?? 0) +
|
||||
(counts.detailsUnchanged ?? 0) +
|
||||
(counts.detailsMissing ?? 0) !==
|
||||
accepted
|
||||
) {
|
||||
throw new Error(`${mode} run detail accounting does not equal the source total`);
|
||||
}
|
||||
if (mode === "identical") {
|
||||
assertZeroCounts(counts, [
|
||||
"inserted",
|
||||
"updated",
|
||||
"detailsInserted",
|
||||
"detailsUpdated",
|
||||
"tombstoned",
|
||||
"reactivated",
|
||||
]);
|
||||
if (counts.unchanged !== accepted) throw new Error("identical rerun changed a digest");
|
||||
}
|
||||
}
|
||||
|
||||
function validateDigestIsolation(document: Record<string, unknown>) {
|
||||
if (
|
||||
document.active !== true ||
|
||||
document.publicVisible !== false ||
|
||||
document.installable !== false
|
||||
) {
|
||||
throw new Error(`mirror digest isolation failed: ${String(document.externalId)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateDigest(document: Record<string, unknown>) {
|
||||
validateDigestIsolation(document);
|
||||
for (const field of [
|
||||
"normalizedSlug",
|
||||
"normalizedSlugFirstToken",
|
||||
"normalizedDisplayName",
|
||||
"normalizedDisplayNameFirstToken",
|
||||
"searchText",
|
||||
"upstreamSourceType",
|
||||
"inferredClassifierVersion",
|
||||
"inferredTopicClassifierVersion",
|
||||
"inferredInputHash",
|
||||
"inferredTopicInputHash",
|
||||
]) {
|
||||
if (typeof document[field] !== "string" || !document[field]) {
|
||||
throw new Error(`mirror digest lacks ${field}: ${String(document.externalId)}`);
|
||||
}
|
||||
}
|
||||
if (
|
||||
!Array.isArray(document.inferredCategories) ||
|
||||
document.inferredCategories.length < 1 ||
|
||||
document.inferredCategories.length > 3 ||
|
||||
!Array.isArray(document.inferredTopics) ||
|
||||
document.inferredTopics.length > 5 ||
|
||||
!["high", "medium", "low"].includes(String(document.inferredCategoryConfidence)) ||
|
||||
!["high", "medium", "low"].includes(String(document.inferredTopicConfidence)) ||
|
||||
typeof document.inferredAt !== "number"
|
||||
) {
|
||||
throw new Error(`mirror digest lacks bounded inference: ${String(document.externalId)}`);
|
||||
}
|
||||
const scanners = document.upstreamScanners;
|
||||
if (!scanners || typeof scanners !== "object" || Array.isArray(scanners)) {
|
||||
throw new Error(`mirror digest lacks upstream scanners: ${String(document.externalId)}`);
|
||||
}
|
||||
for (const provider of ["genAgentTrustHub", "socket", "snyk"]) {
|
||||
const scanner = (scanners as Record<string, unknown>)[provider];
|
||||
if (
|
||||
!scanner ||
|
||||
typeof scanner !== "object" ||
|
||||
Array.isArray(scanner) ||
|
||||
typeof (scanner as Record<string, unknown>).status !== "string"
|
||||
) {
|
||||
throw new Error(`mirror digest lacks ${provider} status: ${String(document.externalId)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateFacet(document: Record<string, unknown>) {
|
||||
if (
|
||||
document.active !== true ||
|
||||
!["category", "topic"].includes(String(document.kind)) ||
|
||||
typeof document.term !== "string" ||
|
||||
!document.term ||
|
||||
typeof document.externalId !== "string" ||
|
||||
typeof document.installs !== "number"
|
||||
) {
|
||||
throw new Error(`mirror facet is invalid: ${String(document.externalId)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateDetail(document: Record<string, unknown>) {
|
||||
const content = document.content;
|
||||
const contentBytes = document.contentBytes;
|
||||
if (
|
||||
typeof content !== "string" ||
|
||||
typeof contentBytes !== "number" ||
|
||||
contentBytes > 64 * 1024 ||
|
||||
Buffer.byteLength(content, "utf8") !== contentBytes
|
||||
) {
|
||||
throw new Error(`mirror detail exceeds its boundary: ${String(document.externalId)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function percentile(values: number[], percentileValue: number) {
|
||||
const sorted = [...values].sort((left, right) => left - right);
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * percentileValue))] ?? 0;
|
||||
}
|
||||
|
||||
async function measureReads(externalIds: string[]) {
|
||||
const results = [];
|
||||
for (const externalId of externalIds) {
|
||||
const samples = [];
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
const read = await call({ operation: "read", externalId });
|
||||
if (!read.payload.digest) throw new Error(`indexed read missed ${externalId}`);
|
||||
samples.push(read.elapsedMs);
|
||||
}
|
||||
results.push({
|
||||
externalId,
|
||||
samplesMs: samples,
|
||||
medianMs: percentile(samples, 0.5),
|
||||
p95Ms: percentile(samples, 0.95),
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const cronSource = await readFile(resolve("convex/crons.ts"), "utf8");
|
||||
if (cronSource.includes("skillsShMirror") || cronSource.includes("skills-sh/mirror")) {
|
||||
throw new Error("skills.sh mirror scheduler reference exists");
|
||||
}
|
||||
|
||||
const proofStartedAt = Date.now();
|
||||
let proof: Record<string, unknown>;
|
||||
await call({
|
||||
operation: "configure",
|
||||
enabled: true,
|
||||
reason: "CLAW-563 authenticated leaderboard foundation proof",
|
||||
});
|
||||
try {
|
||||
const isolationBefore = (await call({ operation: "isolation" })).payload;
|
||||
const statusBefore = (await call({ operation: "status" })).payload;
|
||||
let recoverableRun = findRecoverableMirrorRun(statusBefore);
|
||||
const recoveredLiveRunId = recoverableRun
|
||||
? capturedMirrorSourceRunId(recoverableRun.snapshotId)
|
||||
: null;
|
||||
let recoveredNormalizationRun = recoveredLiveRunId ? recoverableRun : null;
|
||||
const completedLiveRun = recoveredLiveRunId
|
||||
? await resolveCompletedLiveMirrorRun({
|
||||
payload: statusBefore,
|
||||
runId: recoveredLiveRunId,
|
||||
readRun: async (runId) => (await call({ operation: "run", runId })).payload,
|
||||
})
|
||||
: null;
|
||||
let discardedStaleRecovery: Record<string, unknown> | null = null;
|
||||
if (recoveredNormalizationRun && !completedLiveRun) {
|
||||
const discarded = (
|
||||
await call({
|
||||
operation: "discard",
|
||||
runId: recoveredNormalizationRun.runId,
|
||||
reason: `discard stale captured recovery without live source ${recoveredLiveRunId}`,
|
||||
})
|
||||
).payload;
|
||||
if (discarded.status !== "canceled" || discarded.runId !== recoveredNormalizationRun.runId) {
|
||||
throw new Error(
|
||||
`discard stale captured recovery returned unexpected payload: ${JSON.stringify(discarded)}`,
|
||||
);
|
||||
}
|
||||
discardedStaleRecovery = {
|
||||
runId: recoveredNormalizationRun.runId,
|
||||
missingLiveRunId: recoveredLiveRunId,
|
||||
status: discarded.status,
|
||||
};
|
||||
recoverableRun = null;
|
||||
recoveredNormalizationRun = null;
|
||||
}
|
||||
const firstRun = completedLiveRun
|
||||
? completedLiveRunProof(completedLiveRun)
|
||||
: await runMirror(
|
||||
"CLAW-563 complete authenticated leaderboard enumeration",
|
||||
true,
|
||||
recoveredNormalizationRun ? null : recoverableRun,
|
||||
);
|
||||
const conflictsAfterFirstRun = (
|
||||
await call({ operation: "conflicts", runId: firstRun.runId, limit: 50 })
|
||||
).payload;
|
||||
const liveRunConflicts = Array.isArray(conflictsAfterFirstRun.conflicts)
|
||||
? (conflictsAfterFirstRun.conflicts as Record<string, unknown>[])
|
||||
: [];
|
||||
const capturedAfterLive = await collectPages("page", validateDigestIsolation);
|
||||
const liveCapturedSource: CapturedMirrorSource = {
|
||||
capturedRunId: firstRun.runId,
|
||||
externalIds: capturedAfterLive.documents.map((document) => {
|
||||
if (typeof document.externalId !== "string") {
|
||||
throw new Error("captured mirror digest lacks externalId");
|
||||
}
|
||||
return document.externalId;
|
||||
}),
|
||||
sourceMeasuredAt: String(firstRun.source.measuredAt),
|
||||
sourcePageSize: Number(firstRun.source.pageSize),
|
||||
};
|
||||
const normalizationRun = await runMirror(
|
||||
"CLAW-563 authenticated captured snapshot normalization",
|
||||
false,
|
||||
recoveredNormalizationRun,
|
||||
liveCapturedSource,
|
||||
);
|
||||
const capturedAfterNormalization = await collectPages("page", validateDigest);
|
||||
const normalizedCapturedSource: CapturedMirrorSource = {
|
||||
...liveCapturedSource,
|
||||
capturedRunId: normalizationRun.runId,
|
||||
externalIds: capturedAfterNormalization.documents.map((document) => {
|
||||
if (typeof document.externalId !== "string") {
|
||||
throw new Error("normalized mirror digest lacks externalId");
|
||||
}
|
||||
return document.externalId;
|
||||
}),
|
||||
};
|
||||
const identicalRerun = await runMirror(
|
||||
"CLAW-563 authenticated captured snapshot identical rerun",
|
||||
false,
|
||||
null,
|
||||
normalizedCapturedSource,
|
||||
);
|
||||
assertRunProof(firstRun, "first");
|
||||
assertRunProof(normalizationRun, "first");
|
||||
assertRunProof(identicalRerun, "identical");
|
||||
const [digests, details, facets] = await Promise.all([
|
||||
collectPages("page", validateDigest),
|
||||
collectPages("detail-page", validateDetail),
|
||||
collectPages("facet-page", validateFacet),
|
||||
]);
|
||||
const firstCounts = runCounts(firstRun);
|
||||
const sourceEvidence = firstRun.source.evidence;
|
||||
if (!sourceEvidence) {
|
||||
throw new Error("live mirror run lacks durable source pagination and field evidence");
|
||||
}
|
||||
if (sourceEvidence.fields.normalizedUpstreamTaxonomyFields.length > 0) {
|
||||
throw new Error(
|
||||
"skills.sh now exposes normalized taxonomy fields; mirror ingestion must prefer them",
|
||||
);
|
||||
}
|
||||
const sourceSnapshotHash = parseSkillsShMirrorProofSnapshotId(
|
||||
String(firstRun.run.snapshotId),
|
||||
).sourceSnapshotHash;
|
||||
if (!sourceSnapshotHash) {
|
||||
throw new Error("live mirror run lacks a durable source snapshot hash");
|
||||
}
|
||||
const sourceCaptureSummary = (firstRun.source.capture ??
|
||||
(await call({ operation: "source-summary", snapshotHash: sourceSnapshotHash }))
|
||||
.payload) as Record<string, unknown>;
|
||||
const sourceCapture = {
|
||||
...sourceCaptureSummary,
|
||||
actualDbWrites: Number(
|
||||
sourceCaptureSummary.requestDbWrites ?? firstRun.run.sourceCaptureWrites ?? 0,
|
||||
),
|
||||
};
|
||||
const firstAccounting = mirrorRunAccounting(Number(firstRun.source.total), firstCounts);
|
||||
const quarantinedCount = firstAccounting.quarantined;
|
||||
const quarantinedPreservedCount = firstCounts.quarantinedPreserved ?? 0;
|
||||
const acceptedCount = firstAccounting.accepted;
|
||||
const expectedDigestCount = acceptedCount + quarantinedPreservedCount;
|
||||
if (digests.count !== expectedDigestCount) {
|
||||
throw new Error(
|
||||
`mirror digest count ${digests.count} does not match accepted or preserved total ${expectedDigestCount}`,
|
||||
);
|
||||
}
|
||||
if (facets.count < digests.count) {
|
||||
throw new Error("mirror facets do not cover every digest category fallback");
|
||||
}
|
||||
const digestExternalIds = new Set(digests.documents.map((document) => document.externalId));
|
||||
for (const externalId of SKILLS_SH_MIRROR_CONTROLLED_EXTERNAL_IDS) {
|
||||
if (!digestExternalIds.has(externalId)) {
|
||||
throw new Error(`mirror proof lacks controlled supplement ${externalId}`);
|
||||
}
|
||||
}
|
||||
const quarantineSamples = liveRunConflicts
|
||||
.filter(
|
||||
(conflict) => conflict.runId === firstRun.runId && conflict.kind === "source-quarantine",
|
||||
)
|
||||
.slice(0, 50);
|
||||
if (quarantineSamples.length !== Math.min(quarantinedCount, 50)) {
|
||||
throw new Error("mirror status did not expose the latest run quarantine samples");
|
||||
}
|
||||
const sampleIndexes = [0, Math.floor(digests.count / 2), digests.count - 1];
|
||||
const sampleExternalIds = sampleIndexes.map((index) => {
|
||||
const externalId = digests.documents[index]?.externalId;
|
||||
if (typeof externalId !== "string") throw new Error("mirror digest lacks externalId");
|
||||
return externalId;
|
||||
});
|
||||
const indexedReads = await measureReads(sampleExternalIds);
|
||||
const isolationAfter = (await call({ operation: "isolation" })).payload;
|
||||
if (JSON.stringify(isolationBefore) !== JSON.stringify(isolationAfter)) {
|
||||
throw new Error("mirror proof changed scan isolation state");
|
||||
}
|
||||
const firstOperations = firstRun.run.operations as Record<string, number>;
|
||||
const serializedStorageBytes =
|
||||
digests.serializedBytes +
|
||||
details.serializedBytes +
|
||||
facets.serializedBytes +
|
||||
Number(sourceCaptureSummary.serializedBytes ?? 0);
|
||||
const rows = digests.count;
|
||||
const sourceRows = Number(firstRun.source.total);
|
||||
const sourceMeasurementRequests = sourceEvidence.pagination.requestedPages.length + 4;
|
||||
const perRow = {
|
||||
serializedStorageBytes: serializedStorageBytes / rows,
|
||||
dbWrites: (firstOperations.dbWrites + sourceCapture.actualDbWrites) / sourceRows,
|
||||
dbReads:
|
||||
(firstOperations.dbReads + Number(sourceCaptureSummary.pageDocuments ?? 0)) / sourceRows,
|
||||
sourceRequests: (firstOperations.sourceRequests + sourceMeasurementRequests) / sourceRows,
|
||||
sourceBytes:
|
||||
(firstOperations.sourceBytes + Number(sourceCaptureSummary.sourceBytes ?? 0)) / sourceRows,
|
||||
};
|
||||
proof = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
target: {
|
||||
environment: "permanent Test",
|
||||
gateUrl: targetUrl,
|
||||
productionWrites: 0,
|
||||
schedules: 0,
|
||||
publicVisibility: false,
|
||||
installability: false,
|
||||
publisherAttachment: false,
|
||||
scanPlanning: false,
|
||||
scanAdmission: false,
|
||||
},
|
||||
sourceContract: {
|
||||
authenticated: "Vercel OIDC",
|
||||
scope: "paginated skills.sh leaderboard",
|
||||
databaseEnumeration: "out of v1 scope; exhaustive feed not required",
|
||||
endpointExhausted: true,
|
||||
databaseCoverageProven: false,
|
||||
measuredLeaderboardTotal: firstRun.source.catalogTotal,
|
||||
proofSourceTotal: firstRun.source.total,
|
||||
measurementRequests: sourceMeasurementRequests,
|
||||
endpointExhaustion: sourceEvidence.pagination,
|
||||
rawFieldSets: sourceEvidence.fields,
|
||||
taxonomyEnrichment: {
|
||||
upstreamNormalizedFields: sourceEvidence.fields.normalizedUpstreamTaxonomyFields,
|
||||
fallbackApplied: true,
|
||||
fallbackGenerator: "ClawHub local deterministic catalog classifier",
|
||||
llmGenerated: false,
|
||||
},
|
||||
controlledSupplement: {
|
||||
authority: "immutable GitHub content",
|
||||
environment: "permanent Test only",
|
||||
productionFullSyncStrategy: false,
|
||||
requiredCount: firstRun.source.controlledExternalIds.length,
|
||||
observedRequiredExternalIds: firstRun.source.controlledExternalIds,
|
||||
overlayExternalIds: firstRun.source.controlledOverlayExternalIds,
|
||||
supplementExternalIds: firstRun.source.controlledSupplementExternalIds,
|
||||
contractRequiredExternalIds: SKILLS_SH_MIRROR_CONTROLLED_EXTERNAL_IDS,
|
||||
},
|
||||
durableSourceCapture: sourceCapture,
|
||||
pageSize: firstRun.source.pageSize,
|
||||
measuredAt: firstRun.source.measuredAt,
|
||||
documentedRateLimitRequestsPerMinute: 600,
|
||||
fetchPolicy: {
|
||||
mirrorBatchRows: 50,
|
||||
sourcePageRows: 500,
|
||||
detailAndPageConcurrency: 8,
|
||||
minimumApiRequestIntervalMs: 125,
|
||||
identityPage:
|
||||
"512 KiB structural HTML fallback only for exact no-install well-known owner/repo rows",
|
||||
recovery: "durable cursor retry with bounded Retry-After recovery",
|
||||
},
|
||||
},
|
||||
discardedStaleRecovery,
|
||||
firstRun,
|
||||
normalizationRun,
|
||||
identicalRerun,
|
||||
quarantine: {
|
||||
rejected: firstCounts.rejected,
|
||||
quarantined: quarantinedCount,
|
||||
preserved: quarantinedPreservedCount,
|
||||
samples: quarantineSamples,
|
||||
},
|
||||
storage: {
|
||||
digests: {
|
||||
count: digests.count,
|
||||
serializedBytes: digests.serializedBytes,
|
||||
pageReads: digests.calls,
|
||||
},
|
||||
details: {
|
||||
count: details.count,
|
||||
serializedBytes: details.serializedBytes,
|
||||
pageReads: details.calls,
|
||||
},
|
||||
facets: {
|
||||
count: facets.count,
|
||||
serializedBytes: facets.serializedBytes,
|
||||
pageReads: facets.calls,
|
||||
},
|
||||
totalSerializedBytes: serializedStorageBytes,
|
||||
bytesPerSourceRow: perRow.serializedStorageBytes,
|
||||
},
|
||||
isolation: {
|
||||
before: isolationBefore,
|
||||
after: isolationAfter,
|
||||
unchanged: true,
|
||||
},
|
||||
indexedReads,
|
||||
projectedHypotheticalDatabaseScale: {
|
||||
assumedRows: PROJECTED_SCALE,
|
||||
enumerationAvailable: false,
|
||||
proofStatus: "cost projection only, not a completed database sync",
|
||||
serializedStorageBytes: Math.ceil(perRow.serializedStorageBytes * PROJECTED_SCALE),
|
||||
dbWrites: Math.ceil(perRow.dbWrites * PROJECTED_SCALE),
|
||||
dbReads: Math.ceil(perRow.dbReads * PROJECTED_SCALE),
|
||||
sourceRequests: Math.ceil(perRow.sourceRequests * PROJECTED_SCALE),
|
||||
sourceBytes: Math.ceil(perRow.sourceBytes * PROJECTED_SCALE),
|
||||
},
|
||||
runtime: {
|
||||
elapsedMs: Date.now() - proofStartedAt,
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
const disabled = await callRaw({
|
||||
operation: "configure",
|
||||
enabled: false,
|
||||
reason: "CLAW-563 proof cleanup: retain mirror hidden and paused",
|
||||
});
|
||||
if (!disabled.ok) {
|
||||
throw new Error(`mirror cleanup failed: ${JSON.stringify(disabled.payload)}`);
|
||||
}
|
||||
}
|
||||
|
||||
await mkdir(dirname(OUTPUT_PATH), { recursive: true });
|
||||
await writeFile(OUTPUT_PATH, `${JSON.stringify(proof, null, 2)}\n`);
|
||||
console.log(JSON.stringify({ outputPath: OUTPUT_PATH, ...proof }));
|
||||
@@ -0,0 +1,638 @@
|
||||
import { getVercelOidcToken } from "@vercel/oidc";
|
||||
import { defineEventHandler, getHeader, readBody } from "h3";
|
||||
import {
|
||||
buildSkillsShMirrorProofSnapshotId,
|
||||
fetchSkillsShMirrorBatch,
|
||||
fetchSkillsShMirrorControlledBatch,
|
||||
getSkillsShCatalogTestSourcePolicy,
|
||||
measureSkillsShMirrorProofSource,
|
||||
parseSkillsShMirrorProofSnapshotId,
|
||||
skillsShSourceRetryAfterSeconds,
|
||||
} from "../../../skillsShCatalogSource";
|
||||
import {
|
||||
buildSkillsShMirrorReplayRows,
|
||||
enrichSkillsShMirrorClassifications,
|
||||
type SkillsShMirrorClassificationState,
|
||||
} from "../../../skillsShMirrorClassification";
|
||||
|
||||
const TEST_CONVEX_SITE_URL = "https://academic-chihuahua-392.convex.site";
|
||||
const OPERATOR_PATH = "/api/v1/operator/skills-sh/catalog-test";
|
||||
const SOURCE_PAGE_SIZE = 500;
|
||||
const MIRROR_BATCH_SIZE = 50;
|
||||
const MAX_TEST_SOURCE_ROWS = 50_000;
|
||||
const MAX_DETAIL_BYTES = 64 * 1024;
|
||||
const MAX_DETAIL_PAGE_ROWS = 50;
|
||||
const BATCH_LEASE_HEARTBEAT_INTERVAL_MS = 60_000;
|
||||
|
||||
type MirrorRequest = {
|
||||
operation?:
|
||||
| "configure"
|
||||
| "start"
|
||||
| "start-replay"
|
||||
| "run"
|
||||
| "step"
|
||||
| "step-replay"
|
||||
| "pause"
|
||||
| "resume"
|
||||
| "discard"
|
||||
| "reconcile"
|
||||
| "conflicts"
|
||||
| "status"
|
||||
| "isolation"
|
||||
| "read"
|
||||
| "source-summary"
|
||||
| "page"
|
||||
| "detail-page"
|
||||
| "facet-page";
|
||||
enabled?: boolean;
|
||||
externalId?: string;
|
||||
externalIds?: string[];
|
||||
cursor?: string | null;
|
||||
capturedRunId?: string;
|
||||
hasMore?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
page?: number;
|
||||
pageLength?: number;
|
||||
reason?: string;
|
||||
runId?: string;
|
||||
snapshotHash?: string;
|
||||
sourceMeasuredAt?: string;
|
||||
sourcePageSize?: number;
|
||||
sourceTotal?: number;
|
||||
};
|
||||
|
||||
function jsonResponse(payload: unknown, status = 200, extraHeaders: Record<string, string> = {}) {
|
||||
return new Response(JSON.stringify(payload), {
|
||||
status,
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
...extraHeaders,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function requireString(value: string | undefined, name: string) {
|
||||
const normalized = value?.trim();
|
||||
if (!normalized) throw new Error(`${name} is required`);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function requireInteger(value: number | undefined, name: string, min: number, max: number) {
|
||||
if (!Number.isInteger(value) || value === undefined || value < min || value > max) {
|
||||
throw new Error(`${name} must be an integer between ${min} and ${max}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function assertNoActiveMirrorRun(status: Record<string, unknown>) {
|
||||
if (!Array.isArray(status.runs)) {
|
||||
throw new Error("skills.sh mirror status response lacks runs");
|
||||
}
|
||||
const activeRun = status.runs.find((value) => {
|
||||
if (value === null || typeof value !== "object") return false;
|
||||
const runStatus = (value as Record<string, unknown>).status;
|
||||
return runStatus === "running" || runStatus === "paused" || runStatus === "reconciling";
|
||||
});
|
||||
if (activeRun === undefined) return;
|
||||
const runId =
|
||||
activeRun !== null &&
|
||||
typeof activeRun === "object" &&
|
||||
typeof (activeRun as Record<string, unknown>).runId === "string"
|
||||
? `: ${(activeRun as Record<string, unknown>).runId}`
|
||||
: "";
|
||||
throw new Error(`skills.sh mirror already has an active run${runId}`);
|
||||
}
|
||||
|
||||
async function callConvexOperator(authorization: string, body: Record<string, unknown>) {
|
||||
const response = await fetch(`${TEST_CONVEX_SITE_URL}${OPERATOR_PATH}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: authorization,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`Convex Test mirror operator returned HTTP ${response.status}: ${text}`);
|
||||
}
|
||||
return JSON.parse(text) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function createBatchLeaseHeartbeat(args: {
|
||||
authorization: string;
|
||||
runId: string;
|
||||
page: number;
|
||||
offset: number;
|
||||
leaseToken: string;
|
||||
}) {
|
||||
let nextHeartbeatAt = Date.now() + BATCH_LEASE_HEARTBEAT_INTERVAL_MS;
|
||||
let pending: Promise<void> | null = null;
|
||||
return async () => {
|
||||
if (Date.now() < nextHeartbeatAt) return;
|
||||
if (pending) return await pending;
|
||||
pending = callConvexOperator(args.authorization, {
|
||||
operation: "mirror-batch-claim",
|
||||
runId: args.runId,
|
||||
page: args.page,
|
||||
offset: args.offset,
|
||||
leaseToken: args.leaseToken,
|
||||
})
|
||||
.then(() => undefined)
|
||||
.finally(() => {
|
||||
nextHeartbeatAt = Date.now() + BATCH_LEASE_HEARTBEAT_INTERVAL_MS;
|
||||
pending = null;
|
||||
});
|
||||
await pending;
|
||||
};
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const policy = getSkillsShCatalogTestSourcePolicy(process.env);
|
||||
if (!policy.allowed) return jsonResponse({ error: "not_found" }, 404);
|
||||
const authorization = getHeader(event, "authorization")?.trim() ?? "";
|
||||
if (!authorization.toLowerCase().startsWith("bearer ")) {
|
||||
return jsonResponse({ error: "unauthorized" }, 401);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = (await readBody(event)) as MirrorRequest;
|
||||
const operation = body.operation;
|
||||
if (operation === "status") {
|
||||
return jsonResponse(await callConvexOperator(authorization, { operation: "mirror-status" }));
|
||||
}
|
||||
if (operation === "run") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-run",
|
||||
runId: requireString(body.runId, "runId"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "isolation") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, { operation: "mirror-isolation" }),
|
||||
);
|
||||
}
|
||||
if (operation === "read") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-read",
|
||||
externalId: requireString(body.externalId, "externalId"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "source-summary") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-source-summary",
|
||||
snapshotHash: requireString(body.snapshotHash, "snapshotHash"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "conflicts") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-conflicts",
|
||||
runId: requireString(body.runId, "runId"),
|
||||
limit: requireInteger(body.limit, "limit", 1, 50),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "page") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-page",
|
||||
cursor: body.cursor ?? null,
|
||||
limit: requireInteger(body.limit, "limit", 1, 500),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "detail-page") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-detail-page",
|
||||
cursor: body.cursor ?? null,
|
||||
limit: requireInteger(body.limit, "limit", 1, MAX_DETAIL_PAGE_ROWS),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "facet-page") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-facet-page",
|
||||
cursor: body.cursor ?? null,
|
||||
limit: requireInteger(body.limit, "limit", 1, 500),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "configure") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-configure",
|
||||
enabled: body.enabled === true,
|
||||
reason: requireString(body.reason, "reason"),
|
||||
confirm: "enable-skills-sh-mirror-test",
|
||||
maxRowsPerRun: MAX_TEST_SOURCE_ROWS,
|
||||
maxRowsPerBatch: MIRROR_BATCH_SIZE,
|
||||
maxDetailBytes: MAX_DETAIL_BYTES,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "start") {
|
||||
assertNoActiveMirrorRun(
|
||||
await callConvexOperator(authorization, { operation: "mirror-status" }),
|
||||
);
|
||||
const oidcToken = await getVercelOidcToken();
|
||||
const sourceMeasuredAt = new Date().toISOString();
|
||||
const source = await measureSkillsShMirrorProofSource({ oidcToken });
|
||||
if (source.catalogTotal < 1 || source.catalogTotal > MAX_TEST_SOURCE_ROWS) {
|
||||
throw new Error(
|
||||
`skills.sh source total ${source.catalogTotal} exceeds the Test mirror capacity`,
|
||||
);
|
||||
}
|
||||
const sourceTotal = source.catalogTotal + source.controlledSupplementExternalIds.length;
|
||||
if (sourceTotal > MAX_TEST_SOURCE_ROWS) {
|
||||
throw new Error(`skills.sh proof source total ${sourceTotal} exceeds the Test capacity`);
|
||||
}
|
||||
const snapshotId = buildSkillsShMirrorProofSnapshotId(source);
|
||||
const snapshot = parseSkillsShMirrorProofSnapshotId(snapshotId);
|
||||
const sourceSnapshotHash = requireString(snapshot.sourceSnapshotHash, "sourceSnapshotHash");
|
||||
let sourceCaptureWrites = 0;
|
||||
for (const page of source.sourcePages) {
|
||||
const stored = await callConvexOperator(authorization, {
|
||||
operation: "mirror-source-page-store",
|
||||
snapshotHash: sourceSnapshotHash,
|
||||
...page,
|
||||
});
|
||||
if (stored.stored === true) sourceCaptureWrites += 1;
|
||||
}
|
||||
const sourceCapture = await callConvexOperator(authorization, {
|
||||
operation: "mirror-source-summary",
|
||||
snapshotHash: sourceSnapshotHash,
|
||||
});
|
||||
const result = await callConvexOperator(authorization, {
|
||||
operation: "mirror-start",
|
||||
reason: requireString(body.reason, "reason"),
|
||||
snapshotId,
|
||||
sourceSnapshotHash,
|
||||
sourceCaptureWrites,
|
||||
sourceTotal,
|
||||
sourcePageSize: SOURCE_PAGE_SIZE,
|
||||
sourceMeasuredAt,
|
||||
});
|
||||
return jsonResponse({
|
||||
...result,
|
||||
sourceTotal,
|
||||
sourceCatalogTotal: source.catalogTotal,
|
||||
controlledOverlayTotal: source.controlledOverlayExternalIds.length,
|
||||
controlledSupplementTotal: source.controlledSupplementExternalIds.length,
|
||||
sourceMeasurementRequests: source.sourceRequests,
|
||||
sourceCapture: {
|
||||
...sourceCapture,
|
||||
requestDbWrites: sourceCaptureWrites,
|
||||
},
|
||||
sourceMeasuredAt,
|
||||
sourcePageSize: SOURCE_PAGE_SIZE,
|
||||
});
|
||||
}
|
||||
if (operation === "start-replay") {
|
||||
const sourceMeasuredAt = requireString(body.sourceMeasuredAt, "sourceMeasuredAt");
|
||||
if (Number.isNaN(Date.parse(sourceMeasuredAt))) {
|
||||
throw new Error("sourceMeasuredAt must be an ISO timestamp");
|
||||
}
|
||||
const sourceTotal = requireInteger(body.sourceTotal, "sourceTotal", 1, MAX_TEST_SOURCE_ROWS);
|
||||
const sourcePageSize = requireInteger(
|
||||
body.sourcePageSize,
|
||||
"sourcePageSize",
|
||||
1,
|
||||
SOURCE_PAGE_SIZE,
|
||||
);
|
||||
const result = await callConvexOperator(authorization, {
|
||||
operation: "mirror-start",
|
||||
reason: requireString(body.reason, "reason"),
|
||||
snapshotId: `skills-sh-captured:${requireString(body.capturedRunId, "capturedRunId")}`,
|
||||
sourceTotal,
|
||||
sourcePageSize,
|
||||
sourceMeasuredAt,
|
||||
});
|
||||
return jsonResponse({
|
||||
...result,
|
||||
sourceTotal,
|
||||
sourceMeasuredAt,
|
||||
sourcePageSize,
|
||||
captured: true,
|
||||
});
|
||||
}
|
||||
if (operation === "step") {
|
||||
const runId = requireString(body.runId, "runId");
|
||||
const page = requireInteger(body.page, "page", 0, 100_000);
|
||||
const offset = requireInteger(body.offset, "offset", 0, SOURCE_PAGE_SIZE - 1);
|
||||
const leaseToken = crypto.randomUUID();
|
||||
const lease = await callConvexOperator(authorization, {
|
||||
operation: "mirror-batch-claim",
|
||||
runId,
|
||||
page,
|
||||
offset,
|
||||
leaseToken,
|
||||
});
|
||||
try {
|
||||
const sourceTotal = requireInteger(
|
||||
typeof lease.sourceTotal === "number" ? lease.sourceTotal : undefined,
|
||||
"lease.sourceTotal",
|
||||
1,
|
||||
MAX_TEST_SOURCE_ROWS,
|
||||
);
|
||||
const snapshot = parseSkillsShMirrorProofSnapshotId(
|
||||
requireString(
|
||||
typeof lease.snapshotId === "string" ? lease.snapshotId : undefined,
|
||||
"lease.snapshotId",
|
||||
),
|
||||
);
|
||||
if (
|
||||
sourceTotal !==
|
||||
snapshot.catalogTotal + snapshot.controlledSupplementExternalIds.length
|
||||
) {
|
||||
throw new Error("skills.sh mirror run proof source metadata is inconsistent");
|
||||
}
|
||||
const controlledPage = Math.ceil(snapshot.catalogTotal / SOURCE_PAGE_SIZE);
|
||||
const beforeRequest = createBatchLeaseHeartbeat({
|
||||
authorization,
|
||||
runId,
|
||||
page,
|
||||
offset,
|
||||
leaseToken,
|
||||
});
|
||||
const batch =
|
||||
page === controlledPage && snapshot.controlledSupplementExternalIds.length > 0
|
||||
? await fetchSkillsShMirrorControlledBatch(
|
||||
{
|
||||
page,
|
||||
offset,
|
||||
limit: MIRROR_BATCH_SIZE,
|
||||
maxDetailBytes: MAX_DETAIL_BYTES,
|
||||
sourceTotal,
|
||||
externalIds: snapshot.controlledSupplementExternalIds,
|
||||
},
|
||||
{ beforeRequest },
|
||||
)
|
||||
: page < controlledPage
|
||||
? await (async () => {
|
||||
const capturedPage =
|
||||
lease.sourcePage &&
|
||||
typeof lease.sourcePage === "object" &&
|
||||
!Array.isArray(lease.sourcePage)
|
||||
? (lease.sourcePage as Record<string, unknown>)
|
||||
: null;
|
||||
const expectedPage = snapshot.evidence?.pagination.requestedPages.find(
|
||||
(entry) => entry.page === page,
|
||||
);
|
||||
if (
|
||||
!capturedPage ||
|
||||
!expectedPage ||
|
||||
capturedPage.page !== page ||
|
||||
capturedPage.sourceTotal !== snapshot.catalogTotal ||
|
||||
capturedPage.pageLength !== expectedPage.count ||
|
||||
capturedPage.hasMore !== expectedPage.hasMore ||
|
||||
capturedPage.identityHash !== expectedPage.identityHash ||
|
||||
capturedPage.contentHash !== expectedPage.contentHash ||
|
||||
!Array.isArray(capturedPage.rows)
|
||||
) {
|
||||
throw new Error(
|
||||
`captured skills.sh leaderboard page does not match the proof: ${page}`,
|
||||
);
|
||||
}
|
||||
const oidcToken = await getVercelOidcToken();
|
||||
const catalogBatch = await fetchSkillsShMirrorBatch(
|
||||
{ page, offset, limit: MIRROR_BATCH_SIZE, maxDetailBytes: MAX_DETAIL_BYTES },
|
||||
{
|
||||
oidcToken,
|
||||
beforeRequest,
|
||||
sourcePage: {
|
||||
data: capturedPage.rows as never,
|
||||
pagination: {
|
||||
page,
|
||||
perPage: SOURCE_PAGE_SIZE,
|
||||
total: snapshot.catalogTotal,
|
||||
hasMore: expectedPage.hasMore,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
if (catalogBatch.sourceTotal !== snapshot.catalogTotal) {
|
||||
throw new Error("skills.sh catalog source total changed during the run");
|
||||
}
|
||||
if (
|
||||
expectedPage.count !== catalogBatch.pageLength ||
|
||||
expectedPage.hasMore !== catalogBatch.hasMore ||
|
||||
expectedPage.identityHash !== catalogBatch.sourcePageIdentityHash
|
||||
) {
|
||||
throw new Error(
|
||||
`skills.sh ordered leaderboard page changed during the run: ${page}`,
|
||||
);
|
||||
}
|
||||
const controlledOverlayExternalIds = new Set<string>(
|
||||
snapshot.controlledOverlayExternalIds,
|
||||
);
|
||||
const overlayExternalIds = catalogBatch.rows.flatMap((row) =>
|
||||
controlledOverlayExternalIds.has(row.externalId) ? [row.externalId] : [],
|
||||
);
|
||||
const overlay =
|
||||
overlayExternalIds.length > 0
|
||||
? await fetchSkillsShMirrorControlledBatch(
|
||||
{
|
||||
page,
|
||||
offset: 0,
|
||||
limit: overlayExternalIds.length,
|
||||
maxDetailBytes: MAX_DETAIL_BYTES,
|
||||
sourceTotal,
|
||||
externalIds: overlayExternalIds,
|
||||
},
|
||||
{ beforeRequest },
|
||||
)
|
||||
: null;
|
||||
const overlayByExternalId = new Map(
|
||||
overlay?.rows.map((row) => [row.externalId, row]),
|
||||
);
|
||||
return {
|
||||
...catalogBatch,
|
||||
sourceTotal,
|
||||
hasMore:
|
||||
catalogBatch.hasMore || snapshot.controlledSupplementExternalIds.length > 0,
|
||||
sourceRequests: catalogBatch.sourceRequests + (overlay?.sourceRequests ?? 0),
|
||||
sourceBytes: catalogBatch.sourceBytes + (overlay?.sourceBytes ?? 0),
|
||||
rows: catalogBatch.rows.map((row) => {
|
||||
const controlled = overlayByExternalId.get(row.externalId);
|
||||
if (!controlled) return row;
|
||||
if ("quarantined" in row) return controlled;
|
||||
return {
|
||||
...controlled,
|
||||
upstreamSourceType: row.upstreamSourceType,
|
||||
upstreamInstalls: row.upstreamInstalls,
|
||||
upstreamScanners: row.upstreamScanners,
|
||||
};
|
||||
}),
|
||||
};
|
||||
})()
|
||||
: (() => {
|
||||
throw new Error("skills.sh mirror cursor is beyond the proof source");
|
||||
})();
|
||||
const externalIds = batch.rows.flatMap((row) =>
|
||||
"quarantined" in row ? [] : [row.externalId],
|
||||
);
|
||||
const classificationState =
|
||||
externalIds.length === 0
|
||||
? { states: [] }
|
||||
: await callConvexOperator(authorization, {
|
||||
operation: "mirror-classification-states",
|
||||
externalIds,
|
||||
});
|
||||
if (!Array.isArray(classificationState.states)) {
|
||||
throw new Error("Convex Test mirror classification state is invalid");
|
||||
}
|
||||
const rows = enrichSkillsShMirrorClassifications(
|
||||
batch.rows as Parameters<typeof enrichSkillsShMirrorClassifications>[0],
|
||||
classificationState.states as SkillsShMirrorClassificationState[],
|
||||
);
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-batch",
|
||||
runId,
|
||||
leaseToken,
|
||||
...batch,
|
||||
rows,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
try {
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-batch-release",
|
||||
runId,
|
||||
page,
|
||||
offset,
|
||||
leaseToken,
|
||||
});
|
||||
} catch {
|
||||
// The five-minute durable lease remains the crash/outage recovery path.
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (operation === "step-replay") {
|
||||
const runId = requireString(body.runId, "runId");
|
||||
const page = requireInteger(body.page, "page", 0, 100_000);
|
||||
const offset = requireInteger(body.offset, "offset", 0, SOURCE_PAGE_SIZE - 1);
|
||||
const pageLength = requireInteger(body.pageLength, "pageLength", 1, SOURCE_PAGE_SIZE);
|
||||
const sourceTotal = requireInteger(body.sourceTotal, "sourceTotal", 1, MAX_TEST_SOURCE_ROWS);
|
||||
if (typeof body.hasMore !== "boolean") throw new Error("hasMore is required");
|
||||
if (
|
||||
!Array.isArray(body.externalIds) ||
|
||||
body.externalIds.length < 1 ||
|
||||
body.externalIds.length > MIRROR_BATCH_SIZE ||
|
||||
body.externalIds.some((externalId) => typeof externalId !== "string" || !externalId.trim())
|
||||
) {
|
||||
throw new Error(`externalIds must contain between 1 and ${MIRROR_BATCH_SIZE} strings`);
|
||||
}
|
||||
const leaseToken = crypto.randomUUID();
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-batch-claim",
|
||||
runId,
|
||||
page,
|
||||
offset,
|
||||
leaseToken,
|
||||
});
|
||||
try {
|
||||
const captured = await callConvexOperator(authorization, {
|
||||
operation: "mirror-replay-rows",
|
||||
externalIds: body.externalIds,
|
||||
});
|
||||
if (!Array.isArray(captured.rows)) {
|
||||
throw new Error("Convex Test mirror replay rows are invalid");
|
||||
}
|
||||
const rows = buildSkillsShMirrorReplayRows(captured.rows as never);
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-batch",
|
||||
runId,
|
||||
page,
|
||||
offset,
|
||||
leaseToken,
|
||||
pageLength,
|
||||
hasMore: body.hasMore,
|
||||
sourceTotal,
|
||||
sourceRequests: 0,
|
||||
sourceBytes: 0,
|
||||
rows,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
try {
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-batch-release",
|
||||
runId,
|
||||
page,
|
||||
offset,
|
||||
leaseToken,
|
||||
});
|
||||
} catch {
|
||||
// The five-minute durable lease remains the crash/outage recovery path.
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (operation === "pause" || operation === "resume") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-pause",
|
||||
runId: requireString(body.runId, "runId"),
|
||||
paused: operation === "pause",
|
||||
reason: requireString(body.reason, "reason"),
|
||||
confirm: "set-skills-sh-mirror-pause",
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "discard") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-cancel",
|
||||
runId: requireString(body.runId, "runId"),
|
||||
reason: requireString(body.reason, "reason"),
|
||||
confirm: "cancel-skills-sh-mirror-test-run",
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "reconcile") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-reconcile",
|
||||
runId: requireString(body.runId, "runId"),
|
||||
limit: requireInteger(body.limit ?? 250, "limit", 1, 250),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return jsonResponse({ error: "unknown_operation" }, 400);
|
||||
} catch (error) {
|
||||
const retryAfterSeconds = skillsShSourceRetryAfterSeconds(error);
|
||||
if (retryAfterSeconds !== null) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error: "skills_sh_source_rate_limited",
|
||||
message: error instanceof Error ? error.message : "skills.sh source rate limited",
|
||||
retryAfterSeconds,
|
||||
},
|
||||
429,
|
||||
{ "Retry-After": String(retryAfterSeconds) },
|
||||
);
|
||||
}
|
||||
return jsonResponse(
|
||||
{
|
||||
error: "skills_sh_mirror_test_failed",
|
||||
message: error instanceof Error ? error.message : "Unknown Test mirror failure",
|
||||
},
|
||||
502,
|
||||
);
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+2104
-23
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,353 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CLASSIFIER_VERSION, TOPIC_CLASSIFIER_VERSION } from "../convex/lib/catalogClassifier.mjs";
|
||||
import {
|
||||
buildSkillsShMirrorReplayRows,
|
||||
enrichSkillsShMirrorClassifications,
|
||||
} from "./skillsShMirrorClassification";
|
||||
|
||||
const row = {
|
||||
externalId: "patrick-erichsen/skills/html",
|
||||
slug: "html",
|
||||
displayName: "HTML",
|
||||
sourceContentHash: "a".repeat(64),
|
||||
detail: {
|
||||
content: "# HTML\n\nBuild interactive HTML artifacts and frontend prototypes.",
|
||||
},
|
||||
};
|
||||
|
||||
describe("skills.sh mirror classification enrichment", () => {
|
||||
it("classifies bounded mirror detail content with the native inference contract", () => {
|
||||
const [classified] = enrichSkillsShMirrorClassifications([row], [], 123);
|
||||
|
||||
expect(classified).toMatchObject({
|
||||
externalId: row.externalId,
|
||||
inferredCategories: expect.any(Array),
|
||||
inferredTopics: expect.any(Array),
|
||||
inferredCategoryConfidence: expect.stringMatching(/^(high|medium|low)$/),
|
||||
inferredTopicConfidence: expect.stringMatching(/^(high|medium|low)$/),
|
||||
inferredClassifierVersion: CLASSIFIER_VERSION,
|
||||
inferredTopicClassifierVersion: TOPIC_CLASSIFIER_VERSION,
|
||||
inferredInputHash: expect.any(String),
|
||||
inferredTopicInputHash: expect.any(String),
|
||||
inferredAt: 123,
|
||||
});
|
||||
expect(classified.inferredCategories.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("reuses inference when source identity, content hash, and classifier versions match", () => {
|
||||
const classify = vi.fn();
|
||||
const inference = {
|
||||
inferredCategories: ["development"],
|
||||
inferredTopics: ["html"],
|
||||
inferredCategoryConfidence: "high" as const,
|
||||
inferredTopicConfidence: "medium" as const,
|
||||
inferredClassifierVersion: CLASSIFIER_VERSION,
|
||||
inferredTopicClassifierVersion: TOPIC_CLASSIFIER_VERSION,
|
||||
inferredInputHash: "input-hash",
|
||||
inferredTopicInputHash: "topic-input-hash",
|
||||
inferredAt: 100,
|
||||
};
|
||||
|
||||
const [classified] = enrichSkillsShMirrorClassifications(
|
||||
[row],
|
||||
[
|
||||
{
|
||||
externalId: row.externalId,
|
||||
slug: row.slug,
|
||||
displayName: row.displayName,
|
||||
sourceContentHash: row.sourceContentHash,
|
||||
...inference,
|
||||
},
|
||||
],
|
||||
200,
|
||||
classify,
|
||||
);
|
||||
|
||||
expect(classified).toMatchObject(inference);
|
||||
expect(classify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reclassifies when the source content hash or classifier version changes", () => {
|
||||
const classify = vi.fn(() => ({
|
||||
categories: [],
|
||||
topics: [],
|
||||
confidence: "low" as const,
|
||||
topicConfidence: "low" as const,
|
||||
classifierVersion: CLASSIFIER_VERSION,
|
||||
topicClassifierVersion: TOPIC_CLASSIFIER_VERSION,
|
||||
inputHash: "new-input",
|
||||
topicInputHash: "new-topic-input",
|
||||
}));
|
||||
const staleState = {
|
||||
externalId: row.externalId,
|
||||
slug: row.slug,
|
||||
displayName: row.displayName,
|
||||
sourceContentHash: "b".repeat(64),
|
||||
inferredCategories: ["development"],
|
||||
inferredTopics: ["html"],
|
||||
inferredCategoryConfidence: "high" as const,
|
||||
inferredTopicConfidence: "high" as const,
|
||||
inferredClassifierVersion: "taxonomy-old",
|
||||
inferredTopicClassifierVersion: TOPIC_CLASSIFIER_VERSION,
|
||||
inferredInputHash: "old-input",
|
||||
inferredTopicInputHash: "old-topic-input",
|
||||
inferredAt: 100,
|
||||
};
|
||||
|
||||
const [classified] = enrichSkillsShMirrorClassifications([row], [staleState], 200, classify);
|
||||
|
||||
expect(classify).toHaveBeenCalledOnce();
|
||||
expect(classified).toMatchObject({
|
||||
inferredCategories: ["other"],
|
||||
inferredTopics: [],
|
||||
inferredClassifierVersion: CLASSIFIER_VERSION,
|
||||
inferredTopicClassifierVersion: TOPIC_CLASSIFIER_VERSION,
|
||||
inferredAt: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it("reclassifies changed detail when a legacy state has no content hash", () => {
|
||||
const classify = vi.fn(() => ({
|
||||
categories: ["development"],
|
||||
topics: ["html"],
|
||||
confidence: "high" as const,
|
||||
topicConfidence: "high" as const,
|
||||
classifierVersion: CLASSIFIER_VERSION,
|
||||
topicClassifierVersion: TOPIC_CLASSIFIER_VERSION,
|
||||
inputHash: "new-input",
|
||||
topicInputHash: "new-topic-input",
|
||||
}));
|
||||
const unhashedRow = {
|
||||
...row,
|
||||
sourceContentHash: undefined,
|
||||
detail: { content: "# HTML\n\nChanged content." },
|
||||
};
|
||||
const state = {
|
||||
externalId: row.externalId,
|
||||
slug: row.slug,
|
||||
displayName: row.displayName,
|
||||
inferredCategories: ["other"],
|
||||
inferredTopics: [],
|
||||
inferredCategoryConfidence: "low" as const,
|
||||
inferredTopicConfidence: "low" as const,
|
||||
inferredClassifierVersion: CLASSIFIER_VERSION,
|
||||
inferredTopicClassifierVersion: TOPIC_CLASSIFIER_VERSION,
|
||||
inferredInputHash: "old-input",
|
||||
inferredTopicInputHash: "old-topic-input",
|
||||
inferredAt: 100,
|
||||
};
|
||||
|
||||
const [classified] = enrichSkillsShMirrorClassifications([unhashedRow], [state], 200, classify);
|
||||
|
||||
expect(classify).toHaveBeenCalledOnce();
|
||||
expect(classified).toMatchObject({
|
||||
inferredCategories: ["development"],
|
||||
inferredAt: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it("reclassifies from the display-name stub when retained detail disappears", () => {
|
||||
const [previous] = enrichSkillsShMirrorClassifications([row], [], 100);
|
||||
const classify = vi.fn(() => ({
|
||||
categories: [],
|
||||
topics: [],
|
||||
confidence: "low" as const,
|
||||
topicConfidence: "low" as const,
|
||||
classifierVersion: CLASSIFIER_VERSION,
|
||||
topicClassifierVersion: TOPIC_CLASSIFIER_VERSION,
|
||||
inputHash: "stub-input",
|
||||
topicInputHash: "stub-topic-input",
|
||||
}));
|
||||
const withoutDetail = {
|
||||
externalId: row.externalId,
|
||||
slug: row.slug,
|
||||
displayName: row.displayName,
|
||||
};
|
||||
|
||||
const [classified] = enrichSkillsShMirrorClassifications(
|
||||
[withoutDetail],
|
||||
[previous],
|
||||
200,
|
||||
classify,
|
||||
);
|
||||
|
||||
expect(classify).toHaveBeenCalledWith({
|
||||
slug: "html",
|
||||
text: "---\nname: HTML\n---\n# HTML",
|
||||
});
|
||||
expect(classified).toMatchObject({
|
||||
inferredCategories: ["other"],
|
||||
inferredInputHash: "stub-input",
|
||||
inferredAt: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses classification when the same no-detail stub is observed again", () => {
|
||||
const withoutDetail = {
|
||||
externalId: row.externalId,
|
||||
slug: row.slug,
|
||||
displayName: row.displayName,
|
||||
};
|
||||
const [previous] = enrichSkillsShMirrorClassifications([withoutDetail], [], 100);
|
||||
const classify = vi.fn();
|
||||
|
||||
const [classified] = enrichSkillsShMirrorClassifications(
|
||||
[withoutDetail],
|
||||
[previous],
|
||||
200,
|
||||
classify,
|
||||
);
|
||||
|
||||
expect(classify).not.toHaveBeenCalled();
|
||||
expect(classified.inferredAt).toBe(100);
|
||||
});
|
||||
|
||||
it("rebuilds bounded rows from the captured digest and detail snapshot", () => {
|
||||
const [replayed] = buildSkillsShMirrorReplayRows(
|
||||
[
|
||||
{
|
||||
digest: {
|
||||
...row,
|
||||
sourceType: "github",
|
||||
upstreamSourceType: "github",
|
||||
owner: "patrick-erichsen",
|
||||
repo: "skills",
|
||||
sourceUrl: "https://skills.sh/patrick-erichsen/skills/html",
|
||||
canonicalRepoUrl: "https://github.com/patrick-erichsen/skills",
|
||||
upstreamInstalls: 42,
|
||||
upstreamScanners: {
|
||||
genAgentTrustHub: { status: "unavailable" },
|
||||
socket: { status: "unavailable" },
|
||||
snyk: { status: "unavailable" },
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
contentKind: "skill-md",
|
||||
path: "skills/html/SKILL.md",
|
||||
content: row.detail.content,
|
||||
contentBytes: Buffer.byteLength(row.detail.content),
|
||||
sourceBytes: Buffer.byteLength(row.detail.content),
|
||||
sourceFileCount: 1,
|
||||
truncated: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
456,
|
||||
);
|
||||
|
||||
expect(replayed).toMatchObject({
|
||||
externalId: row.externalId,
|
||||
owner: "patrick-erichsen",
|
||||
repo: "skills",
|
||||
detail: {
|
||||
path: "skills/html/SKILL.md",
|
||||
content: row.detail.content,
|
||||
},
|
||||
inferredCategories: expect.any(Array),
|
||||
inferredAt: 456,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves stale replay rows as quarantine observations", () => {
|
||||
expect(
|
||||
buildSkillsShMirrorReplayRows([
|
||||
{
|
||||
quarantined: true,
|
||||
externalId: "larksuite/cli/lark-doc",
|
||||
upstreamSourceType: "well-known",
|
||||
reason: "identity-page-fetch-failed",
|
||||
},
|
||||
] as never),
|
||||
).toEqual([
|
||||
{
|
||||
quarantined: true,
|
||||
externalId: "larksuite/cli/lark-doc",
|
||||
upstreamSourceType: "well-known",
|
||||
reason: "identity-page-fetch-failed",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("synthesizes the same bounded content hash while replaying legacy detail", () => {
|
||||
const [replayed] = buildSkillsShMirrorReplayRows(
|
||||
[
|
||||
{
|
||||
digest: {
|
||||
externalId: row.externalId,
|
||||
sourceType: "github",
|
||||
upstreamSourceType: "github",
|
||||
owner: "patrick-erichsen",
|
||||
repo: "skills",
|
||||
slug: row.slug,
|
||||
displayName: row.displayName,
|
||||
sourceUrl: "https://skills.sh/patrick-erichsen/skills/html",
|
||||
canonicalRepoUrl: "https://github.com/patrick-erichsen/skills",
|
||||
upstreamInstalls: 42,
|
||||
upstreamScanners: {
|
||||
genAgentTrustHub: { status: "unavailable" },
|
||||
socket: { status: "unavailable" },
|
||||
snyk: { status: "unavailable" },
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
contentKind: "skill-md",
|
||||
path: "SKILL.md",
|
||||
content: "abc",
|
||||
contentBytes: 3,
|
||||
sourceBytes: 3,
|
||||
sourceFileCount: 1,
|
||||
truncated: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
456,
|
||||
);
|
||||
|
||||
if ("quarantined" in replayed) {
|
||||
throw new Error("legacy detail replay was quarantined");
|
||||
}
|
||||
expect(replayed.sourceContentHash).toBe(
|
||||
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not synthesize a full-content hash from truncated legacy detail", () => {
|
||||
const [replayed] = buildSkillsShMirrorReplayRows([
|
||||
{
|
||||
digest: {
|
||||
externalId: row.externalId,
|
||||
sourceType: "github",
|
||||
upstreamSourceType: "github",
|
||||
owner: "patrick-erichsen",
|
||||
repo: "skills",
|
||||
slug: row.slug,
|
||||
displayName: row.displayName,
|
||||
sourceUrl: "https://skills.sh/patrick-erichsen/skills/html",
|
||||
canonicalRepoUrl: "https://github.com/patrick-erichsen/skills",
|
||||
upstreamInstalls: 42,
|
||||
upstreamScanners: {
|
||||
genAgentTrustHub: { status: "unavailable" },
|
||||
socket: { status: "unavailable" },
|
||||
snyk: { status: "unavailable" },
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
contentKind: "skill-md",
|
||||
path: "SKILL.md",
|
||||
content: "bounded prefix",
|
||||
contentBytes: 14,
|
||||
sourceBytes: 128_000,
|
||||
sourceFileCount: 1,
|
||||
truncated: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if ("quarantined" in replayed) {
|
||||
throw new Error("legacy detail replay was quarantined");
|
||||
}
|
||||
expect(replayed.sourceContentHash).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,264 @@
|
||||
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);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,17 +20,37 @@ ip:<client ip>
|
||||
This keeps a user id and IP with the same visible string in separate hash
|
||||
domains for dedupe and local diagnostics.
|
||||
|
||||
## Counters
|
||||
## Source-Attributed Counters
|
||||
|
||||
The dedupe table does not store user-vs-IP counters. It only gates whether a
|
||||
download should emit the existing skill or package stat event. Public counters
|
||||
still store one total:
|
||||
for native ClawHub downloads still use the canonical skill download field:
|
||||
|
||||
```text
|
||||
downloads
|
||||
statsDownloads
|
||||
```
|
||||
|
||||
Existing historical counts are not estimated or rewritten in this phase.
|
||||
Skills mirrored from skills.sh store the upstream install count separately in
|
||||
`statsSkillsShInstalls`. Public skill Downloads are computed at serialization:
|
||||
|
||||
```text
|
||||
native skill: statsDownloads
|
||||
skills.sh indexed: statsDownloads + statsSkillsShInstalls
|
||||
```
|
||||
|
||||
The combined value is never written back into `statsDownloads`. Existing
|
||||
historical ClawHub downloads remain intact, and search ranking continues to use
|
||||
the native field.
|
||||
|
||||
OpenClaw install telemetry remains in `statsInstallsCurrent` and
|
||||
`statsInstallsAllTime`; it is not added to public Downloads. GitHub popularity
|
||||
is stored in `statsGithubStars`. Existing `stars` rows and `statsStars` count
|
||||
ClawHub Bookmarks and retain those storage/API names for compatibility.
|
||||
|
||||
Source refresh, adoption, content replacement, and GitHub synchronization may
|
||||
update source metadata or upstream counters, but must not reset or rewrite any
|
||||
other metric source. Publisher dashboards receive the source breakdown while
|
||||
ordinary public skill shapes expose only the combined Downloads value.
|
||||
|
||||
## Daily Package Graphs
|
||||
|
||||
|
||||
@@ -98,6 +98,35 @@ not create or mutate `skills` rows during its planning gates.
|
||||
- `githubScanStatus`: `pending`, `clean`, `suspicious`, `malicious`, or `failed`
|
||||
- `githubRemovedAt`
|
||||
|
||||
## Permanent external skills.sh mirror
|
||||
|
||||
The full authenticated skills.sh mirror is separate from both native `skills`
|
||||
and the controlled scan-admission catalog. It stores source observations in
|
||||
`skillsShMirrorDigests` and bounded detail content in `skillsShMirrorDetails`;
|
||||
controls, durable run cursors, and conflicts remain in their own mirror tables.
|
||||
|
||||
- GitHub identities are exact `owner/repo/slug` values. Well-known identities
|
||||
are exact `sourceHost/slug` values and must not invent a repository owner.
|
||||
- Every digest is permanently `publicVisible: false` and `installable: false`.
|
||||
Mirror ingestion never creates native skills, publisher attachment, claims,
|
||||
scan plans, or scan jobs.
|
||||
- The digest stores normalized slug/display-name fields and a lean
|
||||
`searchText`. Exact, prefix, first-token, popularity, freshness, and
|
||||
full-text indexes are staged before activation on the permanent Test corpus.
|
||||
- Gen Agent Trust Hub, Socket, and Snyk observations are stored independently
|
||||
with a bounded status plus optional source timestamp and source link. These
|
||||
are upstream claims only and must never be serialized as a ClawHub verdict.
|
||||
- Detail storage retains at most one preferred `SKILL.md` or `README.md`,
|
||||
capped at 64 KiB. The complete upstream file tree is never persisted for an
|
||||
unclaimed mirror row.
|
||||
- Snapshot ingestion uses bounded page/offset cursors. Pause is checked before
|
||||
another source batch is fetched; resume continues at the exact stored cursor.
|
||||
Reconciliation tombstones disappeared rows and reactivates later
|
||||
observations without deleting or changing native skills.
|
||||
- The mirror has no scheduler in this stage. Production activation, public
|
||||
search/detail/install behavior, claims, and publisher attachment require
|
||||
separately accepted work.
|
||||
|
||||
## Publisher Gate
|
||||
|
||||
The legacy `NVIDIA/skills` source keeps its existing production behavior.
|
||||
|
||||
+15
-2
@@ -52,7 +52,18 @@ read_when:
|
||||
- `moderationFlags`: `string[]` (automatic detection)
|
||||
- `moderationNotes`, `moderationReason`
|
||||
- `hiddenAt`, `hiddenBy`, `lastReviewedAt`, `reportCount`
|
||||
- `stats`: `{ downloads, stars, versions, comments }` (`comments` is retained as a historical stat field; skill comments are retired)
|
||||
- `stats`: legacy nested compatibility counters; canonical migrated counters live
|
||||
in top-level fields.
|
||||
- `statsDownloads`: native ClawHub downloads.
|
||||
- `statsSkillsShInstalls`: upstream skills.sh installs, stored separately.
|
||||
- `statsInstallsCurrent`, `statsInstallsAllTime`: OpenClaw install telemetry.
|
||||
- `statsGithubStars`: upstream GitHub popularity.
|
||||
- `statsStars`: ClawHub Bookmarks; existing `stars` rows and API names remain for
|
||||
compatibility.
|
||||
- Public Downloads are `statsDownloads` for native-only skills and
|
||||
`statsDownloads + statsSkillsShInstalls` for skills.sh-indexed skills.
|
||||
- Search ranking continues to use native counters rather than the combined
|
||||
presentation value.
|
||||
- `createdAt`, `updatedAt`
|
||||
|
||||
### SkillVersion
|
||||
@@ -95,9 +106,11 @@ From SKILL.md frontmatter + AgentSkills + Clawdis extensions:
|
||||
detail page and as hover text; 70 is a presentation rule, not a storage,
|
||||
publish, API, or sync constraint.
|
||||
|
||||
### Star
|
||||
### Bookmark
|
||||
|
||||
- `skillId`, `userId`, `createdAt`
|
||||
- Stored in the legacy `stars` table and exposed through compatibility API
|
||||
routes named `stars`; user-facing product language is Bookmark.
|
||||
|
||||
### AuditLog
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ async function readWorkflow() {
|
||||
};
|
||||
jobs?: Record<string, WorkflowJob>;
|
||||
on?: {
|
||||
pull_request?: {
|
||||
types?: string[];
|
||||
};
|
||||
workflow_dispatch?: unknown;
|
||||
workflow_run?: {
|
||||
branches?: string[];
|
||||
@@ -36,27 +39,49 @@ async function readWorkflow() {
|
||||
}
|
||||
|
||||
describe("Test deploy workflow", () => {
|
||||
it("runs only after successful main CI or a manual dispatch", async () => {
|
||||
it("admits main CI and exact guarded CLAW-563 branch deploys", async () => {
|
||||
const workflow = await readWorkflow();
|
||||
const job = workflow.jobs?.["deploy-test"];
|
||||
const steps = job?.steps ?? [];
|
||||
const revision = steps.find((step) => step.name === "Resolve deployment revision")?.run ?? "";
|
||||
|
||||
expect(workflow.on?.workflow_run).toEqual({
|
||||
workflows: ["CI"],
|
||||
types: ["completed"],
|
||||
branches: ["main"],
|
||||
});
|
||||
expect(workflow.on?.pull_request).toEqual({
|
||||
types: ["synchronize", "labeled"],
|
||||
});
|
||||
expect(workflow.on?.workflow_dispatch).toBeDefined();
|
||||
expect(workflow.concurrency).toEqual({
|
||||
group: "deploy-test",
|
||||
"cancel-in-progress": false,
|
||||
});
|
||||
expect(job?.if).toContain("github.event_name == 'workflow_dispatch'");
|
||||
expect(job?.if).toContain("github.ref == 'refs/heads/main'");
|
||||
expect(job?.if).toContain("github.ref == 'refs/heads/pe/claw-563-skills-sh-mirror-10k'");
|
||||
expect(job?.if).toContain("inputs.branch_test_confirm == 'deploy-claw-563-to-permanent-test'");
|
||||
expect(job?.if).toContain("inputs.expected_sha != ''");
|
||||
expect(job?.if).toContain("github.event_name == 'pull_request'");
|
||||
expect(job?.if).toContain(
|
||||
"github.event.pull_request.head.ref == 'pe/claw-563-skills-sh-mirror-10k'",
|
||||
);
|
||||
expect(job?.if).toContain("github.event.pull_request.head.repo.full_name == github.repository");
|
||||
expect(job?.if).toContain("github.actor == 'Patrick-Erichsen'");
|
||||
expect(job?.if).toContain(
|
||||
"contains(github.event.pull_request.labels.*.name, 'test-mirror-load')",
|
||||
);
|
||||
expect(job?.if).toContain("github.event.workflow_run.conclusion == 'success'");
|
||||
expect(job?.if).toContain("github.event.workflow_run.event == 'push'");
|
||||
expect(job?.if).toContain("github.ref == 'refs/heads/main'");
|
||||
expect(steps.find((step) => step.name === "Resolve deployment revision")?.run).toContain(
|
||||
'deploy_sha" != "$main_sha',
|
||||
);
|
||||
expect(revision).toContain('deploy_sha" != "$main_sha');
|
||||
expect(revision).toContain("refs/heads/pe/claw-563-skills-sh-mirror-10k");
|
||||
expect(revision).toContain("Patrick-Erichsen");
|
||||
expect(revision).toContain("deploy-claw-563-to-permanent-test");
|
||||
expect(revision).toContain("${{ inputs.expected_sha }}");
|
||||
expect(revision).toContain("${{ github.event.pull_request.head.sha }}");
|
||||
expect(revision).toContain("${{ github.event.pull_request.head.repo.full_name }}");
|
||||
expect(revision).toContain("$GITHUB_REPOSITORY");
|
||||
});
|
||||
|
||||
it("uses only the Test environment and narrowly scoped secrets", async () => {
|
||||
@@ -114,6 +139,9 @@ describe("Test deploy workflow", () => {
|
||||
expect(deployStep?.run).toContain("--target=preview");
|
||||
expect(deployStep?.run).toContain('--scope "$VERCEL_SCOPE"');
|
||||
expect(deployStep?.run).toContain("--build-env CONVEX_DEPLOY_KEY=");
|
||||
expect(deployStep?.run).not.toContain("--build-env VERCEL_ENV=");
|
||||
expect(deployStep?.run).toContain("--build-env VERCEL_TARGET_ENV=test");
|
||||
expect(deployStep?.run).toContain("--env VERCEL_TARGET_ENV=test");
|
||||
expect(deployStep?.run).toContain("--build-env CLAWHUB_SKILLS_SH_ROLLOUT_MODE=test");
|
||||
expect(deployStep?.run).toContain("--build-env CLAWHUB_GITHUB_SKILL_SYNC_ROLLOUT_MODE=test");
|
||||
expect(deployStep?.run).toContain("--env CLAWHUB_SKILLS_SH_ROLLOUT_MODE=test");
|
||||
@@ -137,4 +165,19 @@ describe("Test deploy workflow", () => {
|
||||
expect(verify?.run).toContain(".skillsSh.runtimeEnabled == true");
|
||||
expect(verify?.run).toContain(".githubSkillSync.selfServiceEnabled == true");
|
||||
});
|
||||
|
||||
it("proves both immutable controlled mirror entries before cleanup", async () => {
|
||||
const workflow = await readWorkflow();
|
||||
const step = workflow.jobs?.["claw563-mirror-load"]?.steps?.find(
|
||||
(candidate) =>
|
||||
candidate.name === "Load and prove the authenticated leaderboard mirror foundation",
|
||||
);
|
||||
const run = step?.run ?? "";
|
||||
|
||||
expect(run).toContain('"externalId":"patrick-erichsen/skills/html"');
|
||||
expect(run).toContain('"externalId":"steipete/clawdis/discrawl"');
|
||||
expect(run).toContain("050daba89f6b6636470add5cb300aac46a412cf8");
|
||||
expect(run).toContain("690ed564419291ca6e832dc69b53061300075b62");
|
||||
expect(run).toContain("claw563-discrawl-entry.json");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -687,7 +687,7 @@ describe("Header", () => {
|
||||
expect(labels).toEqual(["Home", "Skills", "Plugins", "Official", "Docs"]);
|
||||
});
|
||||
|
||||
it("links profile and starred skills from the signed-in avatar menu", () => {
|
||||
it("links profile and bookmarks from the signed-in avatar menu", () => {
|
||||
profileHandleMock.mockReturnValue("patrick-profile");
|
||||
authStatusMock.mockReturnValue({
|
||||
isAuthenticated: true,
|
||||
@@ -710,7 +710,7 @@ describe("Header", () => {
|
||||
expect(profile.compareDocumentPosition(dashboard) & Node.DOCUMENT_POSITION_FOLLOWING).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
);
|
||||
expect(screen.getByText("Stars").closest("a")?.getAttribute("href")).toBe("/stars");
|
||||
expect(screen.getByText("Bookmarks").closest("a")?.getAttribute("href")).toBe("/stars");
|
||||
expect(screen.getAllByText("Dashboard").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("Settings")).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -1789,20 +1789,20 @@ describe("SkillDetailPage", () => {
|
||||
|
||||
render(<SkillDetailPage slug="weather" />);
|
||||
|
||||
const starButton = await screen.findByRole("button", { name: "Star skill" });
|
||||
const starButton = await screen.findByRole("button", { name: "Bookmark skill" });
|
||||
expect(starButton.textContent).toContain("8");
|
||||
|
||||
fireEvent.click(starButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Unstar skill" }).textContent).toContain("9");
|
||||
expect(screen.getByRole("button", { name: "Remove bookmark" }).textContent).toContain("9");
|
||||
});
|
||||
expect(toggleStarMock).toHaveBeenCalledWith({ skillId });
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Unstar skill" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Remove bookmark" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Star skill" }).textContent).toContain("8");
|
||||
expect(screen.getByRole("button", { name: "Bookmark skill" }).textContent).toContain("8");
|
||||
});
|
||||
expect(toggleStarMock).toHaveBeenCalledTimes(2);
|
||||
expect(routerInvalidateMock).toHaveBeenCalledTimes(2);
|
||||
@@ -1869,7 +1869,7 @@ describe("SkillDetailPage", () => {
|
||||
|
||||
render(<SkillDetailPage slug="weather" />);
|
||||
|
||||
expect((await screen.findByRole("button", { name: "Unstar skill" })).textContent).toContain(
|
||||
expect((await screen.findByRole("button", { name: "Remove bookmark" })).textContent).toContain(
|
||||
"1",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("SkillsIndex", () => {
|
||||
const sortOptions = Array.from(
|
||||
screen.getByRole("radiogroup", { name: "Skill view" }).querySelectorAll('[role="radio"]'),
|
||||
).map((option) => option.textContent);
|
||||
expect(sortOptions).toEqual(["All", "Trending", "Top", "Most starred", "Featured"]);
|
||||
expect(sortOptions).toEqual(["All", "Trending", "Top", "Most bookmarked", "Featured"]);
|
||||
});
|
||||
|
||||
it("offers Top without exposing downloads as a browse view", async () => {
|
||||
@@ -144,7 +144,7 @@ describe("SkillsIndex", () => {
|
||||
fireEvent.click(screen.getByRole("combobox", { name: "Sort" }));
|
||||
const sortOptions = screen.getAllByRole("option").map((option) => option.textContent);
|
||||
|
||||
expect(views).toEqual(["All", "Trending", "Top", "Most starred", "Featured"]);
|
||||
expect(views).toEqual(["All", "Trending", "Top", "Most bookmarked", "Featured"]);
|
||||
expect(sortOptions).toEqual(["Recently updated", "Newest", "Name"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -649,7 +649,7 @@ export default function Header() {
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/stars" className="flex items-center gap-2">
|
||||
<Star size={14} aria-hidden="true" />
|
||||
Stars
|
||||
Bookmarks
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
|
||||
@@ -763,8 +763,8 @@ export function SkillDetailPage({
|
||||
});
|
||||
void router.invalidate();
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle star", error);
|
||||
toast.error(getUserFacingConvexError(error, "Unable to update star. Please try again."));
|
||||
console.error("Failed to toggle bookmark", error);
|
||||
toast.error(getUserFacingConvexError(error, "Unable to update bookmark. Please try again."));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -162,14 +162,14 @@ describe("SkillHeader", () => {
|
||||
);
|
||||
}
|
||||
|
||||
it("keeps signed-out star and report actions visible and routes clicks to sign-in", () => {
|
||||
it("keeps signed-out bookmark and report actions visible and routes clicks to sign-in", () => {
|
||||
const onToggleStar = vi.fn();
|
||||
const onOpenReport = vi.fn();
|
||||
const onRequireSignIn = vi.fn();
|
||||
|
||||
const { container } = renderHeader({ onToggleStar, onOpenReport, onRequireSignIn });
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Star skill" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Bookmark skill" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Report" }));
|
||||
|
||||
expect(onRequireSignIn).toHaveBeenCalledTimes(2);
|
||||
@@ -488,19 +488,19 @@ describe("SkillHeader", () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("places Star in the sidebar without an outline button", () => {
|
||||
it("places Bookmark in the sidebar without an outline button", () => {
|
||||
const { container } = renderHeader();
|
||||
|
||||
const starBand = container.querySelector(".skill-sidebar-star-band");
|
||||
expect(starBand).toBeTruthy();
|
||||
const starButton = within(starBand as HTMLElement).getByRole("button", {
|
||||
name: "Star skill",
|
||||
name: "Bookmark skill",
|
||||
});
|
||||
expect(starButton.className).toContain("skill-sidebar-star-action");
|
||||
expect(container.querySelector(".skill-hero-title-row .skill-title-actions")).toBeNull();
|
||||
});
|
||||
|
||||
it("places Star on the creator row on mobile detail layout", () => {
|
||||
it("places Bookmark on the creator row on mobile detail layout", () => {
|
||||
setViewportWidth(390);
|
||||
const { container } = renderHeader();
|
||||
|
||||
@@ -508,7 +508,7 @@ describe("SkillHeader", () => {
|
||||
const creator = container.querySelector(".skill-hero-creator");
|
||||
expect(creator).toBeTruthy();
|
||||
const starButton = within(creator as HTMLElement).getByRole("button", {
|
||||
name: "Star skill",
|
||||
name: "Bookmark skill",
|
||||
});
|
||||
expect(starButton.className).toContain("skill-sidebar-star-action");
|
||||
expect(starButton.closest(".skill-hero-creator-star")).toBeTruthy();
|
||||
|
||||
@@ -201,21 +201,21 @@ export function SkillHeader({
|
||||
const renderStarAction = () => (
|
||||
<SignedInActionTooltip
|
||||
isAuthenticated={isAuthenticated}
|
||||
message="You must be signed in to star a skill"
|
||||
message="You must be signed in to bookmark a skill"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="skill-sidebar-action-link skill-sidebar-star-action"
|
||||
onClick={isAuthenticated ? onToggleStar : onRequireSignIn}
|
||||
aria-pressed={Boolean(isAuthenticated && isStarred)}
|
||||
aria-label={isStarred ? "Unstar skill" : "Star skill"}
|
||||
aria-label={isStarred ? "Remove bookmark" : "Bookmark skill"}
|
||||
>
|
||||
<Star
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
fill={isAuthenticated && isStarred ? "currentColor" : "none"}
|
||||
/>
|
||||
{isAuthenticated && isStarred ? "Unstar" : "Star"}
|
||||
{isAuthenticated && isStarred ? "Bookmarked" : "Bookmark"}
|
||||
<span className="skill-action-count">{formattedStats.stars}</span>
|
||||
</button>
|
||||
</SignedInActionTooltip>
|
||||
|
||||
@@ -212,7 +212,7 @@ function UserStatsTooltipContent({
|
||||
</span>
|
||||
<span
|
||||
className="flex items-center gap-1 text-fs-xs text-ink-soft"
|
||||
title="Stars received"
|
||||
title="Bookmarks received"
|
||||
>
|
||||
<Star size={12} />
|
||||
{formatCompactStat(stats.totalStars)}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DashboardCatalogView } from "./DashboardCatalogView";
|
||||
import type { DashboardCatalogItem, DashboardSkill } from "./types";
|
||||
|
||||
const skill = {
|
||||
_id: "skills:demo",
|
||||
_creationTime: 1,
|
||||
slug: "demo",
|
||||
displayName: "Demo",
|
||||
ownerUserId: "users:owner",
|
||||
ownerPath: "owner",
|
||||
tags: {},
|
||||
badges: {},
|
||||
stats: {
|
||||
downloads: 20,
|
||||
installsCurrent: 3,
|
||||
installsAllTime: 7,
|
||||
stars: 4,
|
||||
versions: 1,
|
||||
},
|
||||
metricSources: {
|
||||
clawHubDownloads: 12,
|
||||
skillsShInstalls: 8,
|
||||
openClawInstallsCurrent: 3,
|
||||
openClawInstallsAllTime: 7,
|
||||
githubStars: 99,
|
||||
bookmarks: 4,
|
||||
},
|
||||
latestVersion: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
} as DashboardSkill;
|
||||
|
||||
const item: DashboardCatalogItem = {
|
||||
kind: "skill",
|
||||
id: skill._id,
|
||||
name: skill.displayName,
|
||||
searchText: "demo",
|
||||
data: skill,
|
||||
updatedAt: skill.updatedAt,
|
||||
installs: 7,
|
||||
downloads: 20,
|
||||
};
|
||||
|
||||
describe("DashboardCatalogView", () => {
|
||||
it("lets publishers inspect the source breakdown behind combined downloads", () => {
|
||||
render(
|
||||
<DashboardCatalogView items={[item]} view="list" ownerHandle="owner" canManage={true} />,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTitle(
|
||||
"20 downloads: 12 ClawHub downloads + 8 skills.sh installs. 7 OpenClaw installs; 99 GitHub stars; 4 bookmarks.",
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -110,6 +110,7 @@ function SkillListRow({
|
||||
secondary={packageRowSecondary(skill.updatedAt)}
|
||||
status={skillArtifactStatus(skill)}
|
||||
downloads={skill.stats?.downloads ?? 0}
|
||||
downloadTitle={skillMetricSourceLabel(skill)}
|
||||
menu={<CatalogRowMenu item={item} ownerHandle={ownerHandle} canManage={canManage} />}
|
||||
/>
|
||||
);
|
||||
@@ -149,6 +150,7 @@ function CatalogRow({
|
||||
secondary,
|
||||
status,
|
||||
downloads,
|
||||
downloadTitle,
|
||||
menu,
|
||||
}: {
|
||||
href: string;
|
||||
@@ -159,6 +161,7 @@ function CatalogRow({
|
||||
secondary: string;
|
||||
status: ArtifactDisplayStatus;
|
||||
downloads: number;
|
||||
downloadTitle?: string;
|
||||
menu: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
@@ -180,7 +183,10 @@ function CatalogRow({
|
||||
<SecurityAuditMiniStatus status={status} />
|
||||
</div>
|
||||
<div className="skill-list-item-meta">
|
||||
<span className="dashboard-catalog-downloads" title={metricLabel(downloads, "download")}>
|
||||
<span
|
||||
className="dashboard-catalog-downloads"
|
||||
title={downloadTitle ?? metricLabel(downloads, "download")}
|
||||
>
|
||||
<Download size={14} aria-hidden="true" />
|
||||
<span aria-hidden="true">{formatCompactStat(downloads)}</span>
|
||||
<span className="sr-only">{metricLabel(downloads, "download")}</span>
|
||||
@@ -216,6 +222,13 @@ function metricLabel(value: number, noun: string) {
|
||||
return `${value} ${noun}${value === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
function skillMetricSourceLabel(skill: DashboardSkill) {
|
||||
const sources = skill.metricSources;
|
||||
const downloads = skill.stats?.downloads ?? 0;
|
||||
if (!sources) return metricLabel(downloads, "download");
|
||||
return `${metricLabel(downloads, "download")}: ${sources.clawHubDownloads} ClawHub downloads + ${sources.skillsShInstalls} skills.sh installs. ${sources.openClawInstallsAllTime} OpenClaw installs; ${sources.githubStars} GitHub stars; ${sources.bookmarks} bookmarks.`;
|
||||
}
|
||||
|
||||
function visibilityIcon(label: string) {
|
||||
if (label !== "Hidden" && label !== "Removed") return undefined;
|
||||
return (
|
||||
@@ -238,6 +251,7 @@ function SkillGridCard({ skill, ownerHandle }: { skill: DashboardSkill; ownerHan
|
||||
kindLabel="Skill"
|
||||
status={skillArtifactStatus(skill)}
|
||||
downloads={skill.stats?.downloads ?? 0}
|
||||
downloadTitle={skillMetricSourceLabel(skill)}
|
||||
updatedAt={skill.updatedAt}
|
||||
/>
|
||||
);
|
||||
@@ -268,6 +282,7 @@ function DashboardCatalogGridCard({
|
||||
kindLabel,
|
||||
status,
|
||||
downloads,
|
||||
downloadTitle,
|
||||
updatedAt,
|
||||
}: {
|
||||
href: string;
|
||||
@@ -278,6 +293,7 @@ function DashboardCatalogGridCard({
|
||||
kindLabel: "Skill" | "Plugin";
|
||||
status: ArtifactDisplayStatus;
|
||||
downloads: number;
|
||||
downloadTitle?: string;
|
||||
updatedAt: number;
|
||||
}) {
|
||||
return (
|
||||
@@ -304,7 +320,7 @@ function DashboardCatalogGridCard({
|
||||
</span>
|
||||
<span
|
||||
className="dashboard-catalog-grid-card-downloads"
|
||||
title={metricLabel(downloads, "download")}
|
||||
title={downloadTitle ?? metricLabel(downloads, "download")}
|
||||
>
|
||||
<Download size={13} aria-hidden="true" />
|
||||
<span aria-hidden="true">{formatCompactStat(downloads)}</span>
|
||||
|
||||
@@ -27,6 +27,14 @@ export type DashboardSkill = Pick<
|
||||
ownerPath: string;
|
||||
detailHref?: string;
|
||||
settingsHref?: string;
|
||||
metricSources?: {
|
||||
clawHubDownloads: number;
|
||||
skillsShInstalls: number;
|
||||
openClawInstallsCurrent: number;
|
||||
openClawInstallsAllTime: number;
|
||||
githubStars: number;
|
||||
bookmarks: number;
|
||||
};
|
||||
pendingReview?: boolean;
|
||||
qualityDecision?: "pass" | "quarantine" | "reject";
|
||||
latestVersion: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Skeleton } from "../ui/skeleton";
|
||||
|
||||
export function StarsSkeleton() {
|
||||
return (
|
||||
<main className="browse-page" aria-busy="true" aria-label="Loading starred skills">
|
||||
<main className="browse-page" aria-busy="true" aria-label="Loading bookmarked skills">
|
||||
<header className="stars-header">
|
||||
<Skeleton className="h-9 w-48" />
|
||||
</header>
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("Stars", () => {
|
||||
|
||||
render(<Stars />);
|
||||
|
||||
expect(screen.getByText("Sign in to see your highlights")).toBeTruthy();
|
||||
expect(screen.getByText("Sign in to see your bookmarks")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: /sign in/i })).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -101,7 +101,7 @@ describe("Stars", () => {
|
||||
render(<Stars />);
|
||||
|
||||
expect(document.querySelector(".skeleton-list")).toBeTruthy();
|
||||
expect(screen.queryByText("No stars yet")).toBeNull();
|
||||
expect(screen.queryByText("No bookmarks yet")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows empty state when user has no stars", () => {
|
||||
@@ -112,9 +112,9 @@ describe("Stars", () => {
|
||||
|
||||
render(<Stars />);
|
||||
|
||||
expect(screen.getByText("No stars yet")).toBeTruthy();
|
||||
expect(screen.getByText("No bookmarks yet")).toBeTruthy();
|
||||
expect(screen.getByRole("link", { name: "Browse skills" })).toBeTruthy();
|
||||
expect(screen.queryByRole("combobox", { name: "Sort starred skills" })).toBeNull();
|
||||
expect(screen.queryByRole("combobox", { name: "Sort bookmarked skills" })).toBeNull();
|
||||
expect(screen.queryByRole("link", { name: "Grid view" })).toBeNull();
|
||||
expect(screen.queryByRole("link", { name: "List view" })).toBeNull();
|
||||
});
|
||||
@@ -128,8 +128,8 @@ describe("Stars", () => {
|
||||
render(<Stars />);
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Test Skill" })).toBeTruthy();
|
||||
expect(screen.getByLabelText("Unstar Test Skill")).toBeTruthy();
|
||||
expect(screen.getByText("Your highlights")).toBeTruthy();
|
||||
expect(screen.getByLabelText("Remove bookmark for Test Skill")).toBeTruthy();
|
||||
expect(screen.getByText("Your bookmarks")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("calls toggleStar when unstar button is clicked", () => {
|
||||
@@ -140,7 +140,7 @@ describe("Stars", () => {
|
||||
});
|
||||
|
||||
render(<Stars />);
|
||||
const unstarBtn = screen.getByLabelText("Unstar Test Skill");
|
||||
const unstarBtn = screen.getByLabelText("Remove bookmark for Test Skill");
|
||||
fireEvent.click(unstarBtn);
|
||||
|
||||
expect(toggleStarMock).toHaveBeenCalledWith({ skillId: "skill_1" });
|
||||
|
||||
@@ -41,7 +41,7 @@ const SKILLS_VIEW_OPTIONS = [
|
||||
{ value: "all", label: "All" },
|
||||
{ value: "trending", label: "Trending" },
|
||||
{ value: "top", label: "Top" },
|
||||
{ value: "stars", label: "Most starred" },
|
||||
{ value: "stars", label: "Most bookmarked" },
|
||||
{ value: "featured", label: "Featured" },
|
||||
];
|
||||
|
||||
|
||||
@@ -1167,8 +1167,8 @@ export function Upload() {
|
||||
<span>
|
||||
Move ownership of <strong>{trimmedSlug || "this skill"}</strong> from{" "}
|
||||
<strong>@{existingOwnerHandle}</strong> to <strong>@{ownerHandle}</strong>.
|
||||
Versions, tags, stats and stars are preserved; the old URL redirects to the
|
||||
new one.
|
||||
Versions, tags, stats and bookmarks are preserved; the old URL redirects to
|
||||
the new one.
|
||||
</span>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
+13
-13
@@ -82,8 +82,8 @@ export function Stars() {
|
||||
startTransition(() => {
|
||||
updateOptimisticSkills({ type: "restore", skill });
|
||||
});
|
||||
console.error("Failed to unstar skill:", err);
|
||||
toast.error("Unable to unstar this skill. Please try again.");
|
||||
console.error("Failed to remove skill bookmark:", err);
|
||||
toast.error("Unable to remove this bookmark. Please try again.");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -95,8 +95,8 @@ export function Stars() {
|
||||
return (
|
||||
<SignInPrompt
|
||||
icon={Star}
|
||||
title="Sign in to see your highlights"
|
||||
description="Star skills for quick access later."
|
||||
title="Sign in to see your bookmarks"
|
||||
description="Bookmark skills for quick access later."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -109,7 +109,7 @@ export function Stars() {
|
||||
<main className="browse-page">
|
||||
<header className="stars-header">
|
||||
<h1 className="stars-header-title font-display text-3xl font-black leading-none text-[color:var(--ink)]">
|
||||
Your highlights
|
||||
Your bookmarks
|
||||
</h1>
|
||||
{hasStars ? (
|
||||
<div className="stars-header-controls">
|
||||
@@ -126,24 +126,24 @@ export function Stars() {
|
||||
>
|
||||
<SelectTrigger
|
||||
className="stars-sort-trigger h-8 min-w-[140px] text-xs font-semibold"
|
||||
aria-label="Sort starred skills"
|
||||
aria-label="Sort bookmarked skills"
|
||||
>
|
||||
<ArrowDownUp className="mr-1.5 h-3.5 w-3.5" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="starred">Recently starred</SelectItem>
|
||||
<SelectItem value="starred">Recently bookmarked</SelectItem>
|
||||
<SelectItem value="updated" disabled={!canSortCompleteSet}>
|
||||
Recently updated
|
||||
</SelectItem>
|
||||
<SelectItem value="stars" disabled={!canSortCompleteSet}>
|
||||
Most stars
|
||||
Most bookmarked
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<nav
|
||||
className="publisher-filter-tabs publisher-view-tabs stars-view-tabs"
|
||||
aria-label="Starred skills view"
|
||||
aria-label="Bookmarked skills view"
|
||||
>
|
||||
<Link
|
||||
to="/stars"
|
||||
@@ -172,8 +172,8 @@ export function Stars() {
|
||||
{skills.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={Star}
|
||||
title="No stars yet"
|
||||
description="Browse skills and star your favorites."
|
||||
title="No bookmarks yet"
|
||||
description="Browse skills and bookmark your favorites."
|
||||
action={{ label: "Browse skills", href: "/skills" }}
|
||||
/>
|
||||
) : activeView === "grid" ? (
|
||||
@@ -198,7 +198,7 @@ export function Stars() {
|
||||
e.stopPropagation();
|
||||
handleUnstar(skill);
|
||||
}}
|
||||
aria-label={`Unstar ${skill.displayName}`}
|
||||
aria-label={`Remove bookmark for ${skill.displayName}`}
|
||||
className="stars-card-unstar text-[color:var(--gold)] hover:text-status-error-fg"
|
||||
>
|
||||
<Star className="h-4 w-4 fill-current" />
|
||||
@@ -220,7 +220,7 @@ export function Stars() {
|
||||
e.stopPropagation();
|
||||
handleUnstar(skill);
|
||||
}}
|
||||
aria-label={`Unstar ${skill.displayName}`}
|
||||
aria-label={`Remove bookmark for ${skill.displayName}`}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-[color:var(--gold)] hover:text-status-error-fg"
|
||||
>
|
||||
<Star className="h-4 w-4 fill-current" />
|
||||
|
||||
@@ -156,7 +156,7 @@ const DEFAULT_PUBLISHER_BIO = "Publisher on Clawhub.";
|
||||
const PROFILE_CATALOG_SORT_OPTIONS = [
|
||||
{ value: "downloads", label: "Most downloaded" },
|
||||
{ value: "recent", label: "Recent" },
|
||||
{ value: "stars", label: "Stars" },
|
||||
{ value: "stars", label: "Bookmarks" },
|
||||
] as const;
|
||||
|
||||
const DEFAULT_PROFILE_CATALOG_SORT: ProfileCatalogSort = "downloads";
|
||||
@@ -193,7 +193,7 @@ function buildCatalogTabOptions(publisher: PublicPublisherProfileItem) {
|
||||
if (publisher.kind === "user") {
|
||||
options.push({
|
||||
value: "stars",
|
||||
label: "Starred",
|
||||
label: "Bookmarks",
|
||||
count:
|
||||
publisher.starredCount === undefined
|
||||
? undefined
|
||||
@@ -285,7 +285,7 @@ export function buildPublisherStatCards(
|
||||
{
|
||||
key: "stars",
|
||||
value: formatCompactStat(publisher.stats.stars),
|
||||
label: "stars",
|
||||
label: "bookmarks",
|
||||
icon: Star,
|
||||
},
|
||||
];
|
||||
@@ -954,7 +954,7 @@ export function PublisherProfilePage({
|
||||
: catalogSearch.trim().length > 0
|
||||
? "No matching items"
|
||||
: catalogTab === "stars"
|
||||
? "No starred items yet"
|
||||
? "No bookmarks yet"
|
||||
: catalogTab === "plugins"
|
||||
? "No published plugins yet"
|
||||
: "No published skills yet"
|
||||
|
||||
Reference in New Issue
Block a user