Files
Moshe Abramovitch b554b1be31 feat(versions): publish per-skill content provenance for consumers
Consumers that mirror skills have no way to learn a mirrored skill
changed. The catalog has no releases or tags, metadata.json carries no
version or digest, and the README's Version column is markdown and
per-component. Detecting that 4 of 30 mirrored skills moved means
walking 343 directories and diffing them.

Add versions.json at repo root: one entry per skill with a content
digest, the last catalog commit touching it, and that commit's date.
A consumer stores the digests it mirrored and compares on refresh.

The digest comes from each skill's signed manifest, not from the
upstream sha the sync observed. When content changes without a
signature refresh the sync reverts the skill and the old content stays
published; keying on skill.oms.sig describes what is published, so a
reverted update never surfaces as an update. Coverage is 343/343 and
every manifest's subject name matches its directory.

Sorting the file list before hashing keeps the digest stable across a
re-sign that shuffles manifest order. A NUL between name and digest
stops two different file lists serializing identically.

Removals are gated: if a skill disappears from a regeneration the
generator refuses to write unless --allow-removals is passed. A
generated file staying schema-valid with one fewer entry is how
cuopt-multi-objective-exploration silently vanished from metadata.json
on 2026-08-03.

Kept separate from metadata.json deliberately. That file is taxonomy,
changes rarely, and is partly AI-enriched; this is mechanical and
changes hourly. Separating them stops taxonomy churning on every sync,
keeps a provenance bug away from the marketplace index, and keeps AI
out of the path for data that has nothing to do with it.

The check job's checkout gains fetch-depth: 0 — the commit fields read
git log per skill directory, which a shallow clone cannot answer.

22 unit tests cover digest stability, order independence, separator
collisions, removal detection and schema closure.

Refs #491

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Moshe Abramovitch <moshea@nvidia.com>
2026-09-02 11:52:44 -05:00

57 lines
2.0 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://developer.nvidia.com/schemas/versions.schema.json",
"title": "versions.json",
"description": "Per-skill content provenance for consumers that mirror catalog skills. Generated by .github/scripts/generate_versions.py from committed catalog state.",
"$comment": "SPDX-License-Identifier: Apache-2.0. Copyright (c) 2026 NVIDIA Corporation.",
"type": "object",
"additionalProperties": false,
"required": ["$schema", "skills"],
"properties": {
"$schema": {
"type": "string",
"minLength": 1,
"description": "URI of this schema."
},
"skills": {
"type": "array",
"description": "Every published skill, sorted by name.",
"items": { "$ref": "#/$defs/skill" }
}
},
"$defs": {
"skill": {
"type": "object",
"additionalProperties": false,
"required": ["name", "path", "content_digest", "last_commit", "last_modified"],
"properties": {
"name": {
"type": "string",
"minLength": 1,
"description": "Catalog directory name, unique across the catalog."
},
"path": {
"type": "string",
"pattern": "^skills/[^/]+$",
"description": "Repository-relative path to the skill directory."
},
"content_digest": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$",
"description": "Digest over the skill's signed file list. Changes if and only if published content changes. Compare against a previously mirrored value to detect updates."
},
"last_commit": {
"type": "string",
"pattern": "^[0-9a-f]{40}$",
"description": "Most recent catalog commit touching this skill directory."
},
"last_modified": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
"description": "Commit date of last_commit, ISO 8601 date."
}
}
}
}
}