diff --git a/layers/artifact-delivery/server/tasks/sweep-skill-runs.ts b/layers/artifact-delivery/server/tasks/sweep-skill-runs.ts index 1c9deb10..d95f868b 100644 --- a/layers/artifact-delivery/server/tasks/sweep-skill-runs.ts +++ b/layers/artifact-delivery/server/tasks/sweep-skill-runs.ts @@ -12,8 +12,8 @@ import { runSkillRunSweep } from '../utils/run-sweep' const CRON = '7,22,37,52 * * * *' /** - * At most 40 checks every 15 minutes. A Skill is checked again after a push to - * its repository, or after a backstop (`RECHECK_AFTER_SECONDS`), so most runs + * At most 40 checks every 15 minutes. A Skill is checked again after a file in + * its folder changes, or after a backstop (`RECHECK_AFTER_SECONDS`), so most runs * start no checks. A settled check deletes the Resolution of the one it replaced. * Each check still writes a Resolution with about 60 KB of attestations, even * when it reuses a ready build. Checking every Skill hourly filled the 10 GB diff --git a/layers/artifact-delivery/server/utils/run-check-state.ts b/layers/artifact-delivery/server/utils/run-check-state.ts index 80c10db3..f25ea6b7 100644 --- a/layers/artifact-delivery/server/utils/run-check-state.ts +++ b/layers/artifact-delivery/server/utils/run-check-state.ts @@ -114,15 +114,15 @@ export function runCheckColumns(state: RunCheckState): RunCheckColumns { } } -/** How long a settled check stands when its repository gets no push. */ +/** How long a settled check stands when its Skill folder does not change. */ export interface RecheckAfterSeconds { ready: number failing: number } /** - * Backstops for a repository that gets no push. A check of an unchanged - * repository still writes a Resolution with about 60 KB of attestations, and + * Backstops for a Skill folder that does not change. A check of an unchanged + * Skill still writes a Resolution with about 60 KB of attestations, and * checking every Skill hourly filled the 10 GB database in November 2026. The * backstop catches a change in the checks themselves. */ @@ -135,9 +135,9 @@ export const RECHECK_AFTER_SECONDS: RecheckAfterSeconds = { export interface RecheckInput { state: RunCheckState settledAt: number | null - /** The repository revision the last check ran at. */ + /** The Skill folder revision the last check ran at. */ checkedRevision: string | null - /** The repository revision the registry holds now. */ + /** The Skill folder revision the registry holds now. */ revision: string | null } @@ -145,8 +145,8 @@ export interface RecheckInput { * Whether the sweep should check a Skill again now. * * An unchecked Skill and a transient failure, such as a spent quota, are due - * at once: neither says anything about the Skill yet. Otherwise a push to the - * repository makes the Skill due, and the backstop covers the rest. + * at once: neither says anything about the Skill yet. Otherwise a change to the + * Skill folder makes the Skill due, and the backstop covers the rest. */ export function recheckDue(input: RecheckInput, now: number, after: RecheckAfterSeconds): boolean { const { state, settledAt } = input diff --git a/layers/artifact-delivery/server/utils/run-sweep.ts b/layers/artifact-delivery/server/utils/run-sweep.ts index b643602a..8de4e710 100644 --- a/layers/artifact-delivery/server/utils/run-sweep.ts +++ b/layers/artifact-delivery/server/utils/run-sweep.ts @@ -12,7 +12,7 @@ export interface RunnableSkill { owner: string repository: string name: string - /** Changes whenever the repository gets a push. Null or absent when unknown. */ + /** Git tree SHA of the Skill folder. Null or absent when unknown. */ revision?: string | null } diff --git a/layers/registry/server/presenters/runnable-skills.ts b/layers/registry/server/presenters/runnable-skills.ts index 1aa9f49e..818eff21 100644 --- a/layers/registry/server/presenters/runnable-skills.ts +++ b/layers/registry/server/presenters/runnable-skills.ts @@ -1,15 +1,10 @@ export interface RunnableSkillsDto { - /** `revision` changes whenever the repository gets a push. Null when sync has not read it. */ + /** `revision` is the Skill folder's Git tree SHA. It changes only when a file in that folder changes. Null when sync has not read it. */ items: Array<{ owner: string, repository: string, name: string, revision: string | null }> } -export function presentRunnableSkills(rows: Array<{ owner: string, repo: string, name: string, pushedAt: number | null }>): RunnableSkillsDto { +export function presentRunnableSkills(rows: Array<{ owner: string, repo: string, name: string, revision: string | null }>): RunnableSkillsDto { return { - items: rows.map(row => ({ - owner: row.owner, - repository: row.repo, - name: row.name, - revision: row.pushedAt === null ? null : String(row.pushedAt), - })), + items: rows.map(row => ({ owner: row.owner, repository: row.repo, name: row.name, revision: row.revision })), } } diff --git a/layers/registry/server/utils/skills-registry.ts b/layers/registry/server/utils/skills-registry.ts index 90e3571a..b910277d 100644 --- a/layers/registry/server/utils/skills-registry.ts +++ b/layers/registry/server/utils/skills-registry.ts @@ -742,8 +742,8 @@ export interface SkillSitemapEntry { repo: string /** Resolved-Skill total for the repo; 1 means the hub URL is canonical. */ repoSkillCount: number - /** Unix seconds of the repository's last push, as the last sync read it. */ - pushedAt: number | null + /** Git tree SHA of the Skill folder, as the last sync read it. Null until a sync records it. */ + revision: string | null } interface SkillDuplicateRow extends DuplicateCandidate { @@ -754,6 +754,8 @@ interface SkillDuplicateRow extends DuplicateCandidate { // loadRepoSkillCounts in shared/server/trending-skills.ts). Drives the // single-Skill repo hub routing so links never point at a URL that 301s. repo_skill_count: number + /** Only the sitemap query selects it. */ + current_sha?: string | null } export interface SkillDuplicateSibling { @@ -901,7 +903,7 @@ function findDuplicateGroupInRows(rows: SkillDuplicateRow[], slug: string): Skil export function listAllSkillsForSitemap(event: H3Event): Promise { return cached({ storage: useStorage('cache'), - key: 'skills:sitemap-all:v6', + key: 'skills:sitemap-all:v7', ttlSeconds: DUPLICATE_CANDIDATES_TTL, staleSeconds: DUPLICATE_CANDIDATES_STALE_TTL, compute: () => queryAllSkillsForSitemap(getDB(event)), @@ -919,6 +921,7 @@ export async function queryAllSkillsForSitemap(db: D1Database): Promise row.repo_kind !== 'aggregator' && !weakerSlugs.has(skillSlug(row))) - .map(row => ({ name: row.name, owner: row.owner, repo: row.repo, repoSkillCount: row.repo_skill_count ?? 0, pushedAt: row.pushed_at ?? null })) + .map(row => ({ name: row.name, owner: row.owner, repo: row.repo, repoSkillCount: row.repo_skill_count ?? 0, revision: row.current_sha ?? null })) return entries } diff --git a/test/unit/trending-admission.test.ts b/test/unit/trending-admission.test.ts index ee1a9f89..cdc607f4 100644 --- a/test/unit/trending-admission.test.ts +++ b/test/unit/trending-admission.test.ts @@ -146,6 +146,18 @@ describe('sitemap equals the indexable set', () => { }) }) +describe('runnable Skill revision', () => { + it('reports the Skill folder tree SHA the last sync stored', async () => { + addSkill({ owner: 'o1', repo: 'r1', name: 'scored' }) + db().raw.prepare(`UPDATE skills SET current_sha = 'tree-abc' WHERE name = 'scored'`).run() + db().raw.prepare('DELETE FROM skill_trending_admissions').run() + + const [entry] = await queryAllSkillsForSitemap(db().db) + + expect(entry).toMatchObject({ owner: 'o1', repo: 'r1', name: 'scored', revision: 'tree-abc' }) + }) +}) + describe('empty admitted set', () => { it('lists the sitemap by the old decision, and the page agrees', async () => { addSkill({ owner: 'o1', repo: 'r1', name: 'scored' })