mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-12 12:06:29 +03:00
aa893e3ec2
* feat(bundles): add bundle manifest schema, manifests, and validation (#203) Introduce a machine-readable composition contract for canonical bundles: purpose, audience, stages, included skills, prerequisites, outputs, handoffs, conflicts, and eval suite (schemas/bundle-manifest-v1.schema.json, following the evals-v1 versioned-schema convention). Ship the bounded design note (docs/bundle-manifest-design.md), a schema-conformant example, canonical manifests for the three new milestone bundles, and a stdlib-only validator (scripts/validate-bundles.rb) that rejects incomplete, contradictory, and undeclared-overlapping manifests while keeping bundles an optional layer. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * feat(bundles): add lifecycle capability matrix generator and validators (#203) Add scripts/gen-lifecycle-matrix.rb, which deterministically produces the human-readable docs/lifecycle-capability-matrix.md (one row per canonical bundle) and the machine-readable docs/lifecycle-capability-matrix.json (with per-cell source provenance) reusing the gen-*.rb conventions. Add scripts/validate-lifecycle-matrix.rb to check bundle coverage, cell traceability, artifact currency, and catalog-exactness of nested bundle helpers. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * test(bundles): add bundle manifest validation tests (#203) Add scripts/test-validate-bundles.rb covering schema conformance of the committed example, valid-manifest and declared-conflict positives, per-field incomplete-manifest rejections, contradictory-manifest rejections (missing skill, undeclared handoff artifact, non-catalog conflict), undeclared-overlap rejection naming both manifests, and matrix generator/validator completeness and drift detection. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * ci(bundles): wire bundle manifest validation into the gate (#203) Add validate-bundles.rb, test-validate-bundles.rb, the lifecycle matrix generator check, and the matrix validator to .github/workflows/validate.yml alongside the existing validator steps. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --------- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
163 lines
6.2 KiB
Ruby
163 lines
6.2 KiB
Ruby
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
# Validates the lifecycle capability matrix artifacts:
|
|
#
|
|
# docs/lifecycle-capability-matrix.md (human-readable)
|
|
# docs/lifecycle-capability-matrix.json (machine-readable)
|
|
#
|
|
# Checks:
|
|
# (a) every canonical bundle (bundles/*/SKILL.md) has a row in the JSON
|
|
# artifact (VAL-MNF-024)
|
|
# (b) every populated cell traces to its source: for manifest-derived rows
|
|
# the source manifest exists and the named field is present; for
|
|
# frontmatter-derived rows the source SKILL.md exists and the field is
|
|
# description; for migration-deferred cells the design note exists
|
|
# (VAL-MNF-011/024)
|
|
# (c) the structured artifacts are current: the generator's check mode
|
|
# passes (regenerated output matches the committed files) (VAL-MNF-024)
|
|
# (d) nested bundle helpers (bundles/<x>/skills/<sub>/SKILL.md) never appear
|
|
# in the four generated catalogs; only umbrella bundles do (VAL-MNF-020)
|
|
#
|
|
# Usage:
|
|
# ruby scripts/validate-lifecycle-matrix.rb # exit 0 when current/complete
|
|
|
|
require "json"
|
|
require "open3"
|
|
require "rbconfig"
|
|
require "set"
|
|
require "yaml"
|
|
|
|
ROOT = File.expand_path("..", __dir__)
|
|
JSON_PATH = File.join(ROOT, "docs", "lifecycle-capability-matrix.json")
|
|
GENERATOR = File.join(ROOT, "scripts", "gen-lifecycle-matrix.rb")
|
|
DESIGN_NOTE = File.join(ROOT, "docs", "bundle-manifest-design.md")
|
|
CATALOG_PATHS = [
|
|
File.join(ROOT, "llms.txt"),
|
|
File.join(ROOT, ".claude-plugin", "marketplace.json"),
|
|
File.join(ROOT, ".codex-plugin", "plugin.json"),
|
|
File.join(ROOT, ".agents", "plugins", "marketplace.json")
|
|
].freeze
|
|
MATRIX_FIELDS = %w[
|
|
purpose audience stages included_skills prerequisites outputs handoffs
|
|
conflicts eval_suite
|
|
].freeze
|
|
DOCUMENTED_DERIVATIONS = %w[manifest frontmatter-description migration-deferred].freeze
|
|
NESTED_HELPER_PATTERN = %r{bundles/[^/]+/skills/}
|
|
|
|
errors = []
|
|
|
|
# (c) structured artifacts are current: generator check mode must pass.
|
|
stdout, stderr, status = Open3.capture3(
|
|
RbConfig.ruby,
|
|
File.join("scripts", "gen-lifecycle-matrix.rb"),
|
|
chdir: ROOT
|
|
)
|
|
unless status.success?
|
|
errors << "lifecycle capability matrix is stale: #{stderr.lines.first&.strip || stdout.lines.first&.strip}"
|
|
end
|
|
|
|
unless File.file?(JSON_PATH)
|
|
warn "docs/lifecycle-capability-matrix.json is missing. Run: ruby scripts/gen-lifecycle-matrix.rb --write"
|
|
exit 1
|
|
end
|
|
|
|
begin
|
|
document = JSON.parse(File.read(JSON_PATH))
|
|
rescue JSON::ParserError => e
|
|
warn "docs/lifecycle-capability-matrix.json is invalid JSON: #{e.message.lines.first.strip}"
|
|
exit 1
|
|
end
|
|
|
|
unless document.is_a?(Hash) && document["bundles"].is_a?(Array)
|
|
errors << "docs/lifecycle-capability-matrix.json: top-level document must be a mapping with a bundles array"
|
|
end
|
|
|
|
canonical_bundles = Dir.glob("#{ROOT}/bundles/*/SKILL.md").map do |path|
|
|
File.basename(File.dirname(path))
|
|
end.sort
|
|
rows = document.is_a?(Hash) ? document["bundles"] : []
|
|
|
|
# (a) every canonical bundle has a row.
|
|
row_names = rows.map { |row| row["name"] }
|
|
missing_rows = canonical_bundles - row_names
|
|
unless missing_rows.empty?
|
|
errors << "docs/lifecycle-capability-matrix.json: missing row(s) for canonical bundle(s): #{missing_rows.join(', ')}"
|
|
end
|
|
extra_rows = row_names - canonical_bundles
|
|
unless extra_rows.empty?
|
|
errors << "docs/lifecycle-capability-matrix.json: unexpected row(s) for non-canonical bundle(s): #{extra_rows.join(', ')}"
|
|
end
|
|
|
|
# (b) every populated cell traces to its source.
|
|
rows.each do |row|
|
|
next unless row.is_a?(Hash)
|
|
|
|
name = row["name"]
|
|
fields = row["fields"]
|
|
unless fields.is_a?(Hash)
|
|
errors << "docs/lifecycle-capability-matrix.json: row #{name.inspect} has no fields mapping"
|
|
next
|
|
end
|
|
|
|
MATRIX_FIELDS.each do |field|
|
|
cell = fields[field]
|
|
next unless cell.is_a?(Hash)
|
|
next unless cell["value"].is_a?(String) && !cell["value"].strip.empty?
|
|
next if cell["value"].to_s == "migration deferred — see docs/bundle-manifest-design.md §Migration path"
|
|
|
|
source = cell["source"].to_s
|
|
derivation = cell["derivation"] || (row["derivation"] == "manifest" ? "manifest" : "unknown")
|
|
|
|
case derivation
|
|
when "manifest"
|
|
manifest_rel, pointer = source.split("#/", 2)
|
|
manifest_path = File.join(ROOT, manifest_rel.to_s)
|
|
unless File.file?(manifest_path)
|
|
errors << "docs/lifecycle-capability-matrix.json: #{name}.#{field} source manifest #{manifest_rel.inspect} does not exist"
|
|
next
|
|
end
|
|
begin
|
|
data = YAML.safe_load(File.read(manifest_path), permitted_classes: [], aliases: false)
|
|
rescue Psych::Exception
|
|
data = nil
|
|
end
|
|
if data.is_a?(Hash) && pointer && !data.key?(pointer.split("/", 2).first)
|
|
errors << "docs/lifecycle-capability-matrix.json: #{name}.#{field} source field #{pointer.inspect} is not declared in #{manifest_rel}"
|
|
elsif !data.is_a?(Hash)
|
|
errors << "docs/lifecycle-capability-matrix.json: #{name}.#{field} source manifest #{manifest_rel.inspect} is not a valid YAML mapping"
|
|
end
|
|
when "frontmatter-description"
|
|
source_path = File.join(ROOT, source.split("#/", 2).first.to_s)
|
|
unless File.file?(source_path)
|
|
errors << "docs/lifecycle-capability-matrix.json: #{name}.#{field} source #{source_path.delete_prefix("#{ROOT}/")} does not exist"
|
|
end
|
|
when "migration-deferred"
|
|
unless File.file?(DESIGN_NOTE)
|
|
errors << "docs/lifecycle-capability-matrix.json: #{name}.#{field} references #{DESIGN_NOTE.delete_prefix("#{ROOT}/")} which does not exist"
|
|
end
|
|
else
|
|
errors << "docs/lifecycle-capability-matrix.json: #{name}.#{field} has undocumented derivation #{derivation.inspect} (allowed: #{DOCUMENTED_DERIVATIONS.join(', ')})"
|
|
end
|
|
end
|
|
end
|
|
|
|
# (d) catalog-exactness: nested bundle helpers never appear in the catalogs.
|
|
catalog_matches = []
|
|
CATALOG_PATHS.each do |path|
|
|
next unless File.file?(path)
|
|
|
|
matches = File.readlines(path).grep(NESTED_HELPER_PATTERN)
|
|
next if matches.empty?
|
|
|
|
catalog_matches << "#{path.delete_prefix("#{ROOT}/")} contains nested bundle helper reference(s): #{matches.first.strip}"
|
|
end
|
|
errors.concat(catalog_matches)
|
|
|
|
if errors.empty?
|
|
puts "Validated lifecycle capability matrix (#{rows.length} bundle(s)): complete, traceable, and current."
|
|
else
|
|
warn errors.join("\n")
|
|
exit 1
|
|
end
|