mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
Update gen-lifecycle-matrix.rb and validate-lifecycle-matrix.rb to drop all
hardcoded bundles/ path references after the directory flatten:
- CANONICAL_BUNDLES now selects canonical bundles via */manifest.yaml (the
8 bundle dirs) instead of */SKILL.md (which would match every top-level
skill); keeps the 8-bundle invariant required by the validation contract.
- source strings, manifest paths, fallback frontmatter, and base dirs use
<name>/... at the repo root; the top-level JSON key stays "bundles".
- validate-lifecycle-matrix.rb re-anchors NESTED_HELPER_PATTERN to
%r{(?:^|/)skills/} so nested helpers (tailscale/skills/*,
workflow-architect/skills/*) are still detected in the catalogs, and
canonical_bundles matches the generator's */manifest.yaml glob.
- align the two matrix tests in test-validate-bundles.rb: canonical bundles
are manifest-having dirs (a manifest-less top-level skill is not a row),
and the missing-row rejection targets the demo manifest bundle.
- regenerate docs/lifecycle-capability-matrix.{md,json}.
gen check mode: "is current (8 bundles)"; validate:
"Validated lifecycle capability matrix (8 bundle(s)): complete, traceable, and current."
test-validate-bundles.rb: 14 runs, 307 assertions, 0 failures.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
163 lines
6.1 KiB
Ruby
163 lines
6.1 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 (*/manifest.yaml) 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 (<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{(?:^|/)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}/*/manifest.yaml").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
|