mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-12 03:56:53 +03:00
3eb7bd4096
* feat(validation): enforce 60K-char cap on skill reference files Implements issue #277: - validate-references.rb: new oversized_reference_errors check — every references/*.md must be <= 60,000 characters; error reports path, size, and the split-and-reindex remediation; wired into validate-skills.rb - test-validate-skills.rb: 5 fixture tests (under-limit passes, over-limit fails with path+size, exactly-at-limit passes, remediation message, non-.md ignored); the suite now runs in validate.yml after the format check (it was previously untested in CI) - Docs: agent-skills/SKILL.md, agent-skills/references/best-practices.md, and the AGENTS.md Format Compliance table document the cap and the split-and-reindex procedure - Compliance: split remote-systems-administration/references/ansible.md and programming-principles/references/refactoring-guru.full.md into an index + focused parts (content moved verbatim); SKILL.md routing, README, and source-index references updated; pre-existing stale refactoring-guru-smells.md reference repointed to the index - Fix pre-existing quality-gate violations in the programming-principles and remote-systems-administration descriptions (imperative verb + negative boundary) so this PR's CI quality step passes; regenerated llms.txt and marketplace artifacts Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * test(evals): add eval manifests to modified skills for ratchet The eval-coverage ratchet requires schema-valid eval manifests for any skill modified once coverage is past 50%. This PR modifies programming-principles and remote-systems-administration (splitting their oversized references), so add evals/evals.json to both: - programming-principles: 6 output-quality cases (task-to-book mapping, principled code review, refactor-vs-rewrite, no-op detection, rule distillation, principle conflicts) - remote-systems-administration: 6 output-quality cases (discovery before change, smallest control plane, rollback planning, platform identification, verification evidence, escalation on missing authority) Coverage: 87/145 (60.0%) schema-valid; ratchet clean. 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>
212 lines
8.6 KiB
Ruby
212 lines
8.6 KiB
Ruby
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
require "fileutils"
|
|
require "minitest/autorun"
|
|
require "tmpdir"
|
|
|
|
require_relative "validate-references"
|
|
|
|
# Regression tests for the references/*.md stale-reference scan (#205). The
|
|
# scan must catch a stale prose backtick reference to a nonexistent file inside
|
|
# a references/*.md file (the RICE typo class) without flagging generic
|
|
# doc-type names, examples, external-repository paths, or change-ledger
|
|
# removal records.
|
|
class ReferenceFileScanTest < Minitest::Test
|
|
def test_clean_resolving_references_pass
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/overview.md",
|
|
"See `references/details.md` in this skill.\n")
|
|
write_file(root, "test-skill/references/details.md", "# Details\n")
|
|
assert_empty ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
def test_stale_backtick_reference_is_detected
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/product-strategy.md",
|
|
"See `#{stale_rice_token}` in the product-methodology skill for the full treatment.\n")
|
|
write_skill_ref(root, "product-methodology/references/rice-framework.md", "# RICE\n")
|
|
errors = ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
assert_equal 1, errors.length
|
|
assert_includes errors.first, "test-skill/references/product-strategy.md:1"
|
|
assert_includes errors.first, stale_rice_token
|
|
assert_includes errors.first, "stale prose backtick reference"
|
|
end
|
|
end
|
|
|
|
def test_stale_reference_to_nonexistent_sibling_file_is_detected
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/overview.md",
|
|
"See `missing-guide.md` in the product-methodology skill.\n")
|
|
write_skill_ref(root, "product-methodology/references/rice-framework.md", "# RICE\n")
|
|
errors = ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
assert_equal 1, errors.length
|
|
assert_includes errors.first, "missing-guide.md"
|
|
end
|
|
end
|
|
|
|
def test_corrected_rice_reference_passes
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/product-strategy.md",
|
|
"See `rice-framework.md` in the product-methodology skill for the full treatment.\n")
|
|
write_skill_ref(root, "product-methodology/references/rice-framework.md", "# RICE\n")
|
|
assert_empty ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
def test_sibling_skill_reference_resolves
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/routing.md",
|
|
"For evals guidance see the [langgraph](../../langgraph/SKILL.md) skill's `references/evals.md`.\n")
|
|
write_file(root, "langgraph/SKILL.md", "---\nname: langgraph\n---\n")
|
|
write_skill_ref(root, "langgraph/references/evals.md", "# Evals\n")
|
|
assert_empty ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
def test_repo_root_and_path_prefixed_references_resolve
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/overview.md",
|
|
"Read `AGENTS.md` and `CONTRIBUTING.md`; see `.github/PULL_REQUEST_TEMPLATE.md`.\n")
|
|
write_file(root, "AGENTS.md", "# Agents\n")
|
|
write_file(root, "CONTRIBUTING.md", "# Contributing\n")
|
|
write_file(root, ".github/PULL_REQUEST_TEMPLATE.md", "# Template\n")
|
|
assert_empty ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
def test_generic_doc_type_names_are_not_file_references
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/overview.md",
|
|
"Write `SPEC.md`, `TASK-PLAN.md`, `VERIFICATION-PLAN.md`, and `index.md`.\n")
|
|
assert_empty ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
def test_runtime_and_external_paths_are_not_file_references
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/overview.md",
|
|
"Store results in `02-analysis/epoch-trajectory.md` under /tmp (see `docker/versions.md` upstream).\n")
|
|
assert_empty ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
def test_removal_ledger_lines_are_ignored
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/source-index.md",
|
|
"| `references/old-guide.md` | Removed | Not portable |\n")
|
|
assert_empty ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
def test_case_mismatched_reference_is_detected
|
|
# Case-sensitivity must match CI's Linux runners even on a
|
|
# case-insensitive host filesystem (default macOS APFS).
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/guide.md",
|
|
"See `OVERVIEW.md` in this skill.\n")
|
|
write_file(root, "test-skill/references/overview.md", "# Overview\n")
|
|
errors = ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
assert_equal 1, errors.length
|
|
assert_includes errors.first, "OVERVIEW.md"
|
|
end
|
|
end
|
|
|
|
def test_exact_case_reference_passes
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/guide.md",
|
|
"See `OVERVIEW.md` in this skill.\n")
|
|
write_file(root, "test-skill/references/OVERVIEW.md", "# Overview\n")
|
|
assert_empty ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
def test_packet_field_names_are_not_file_references
|
|
with_fixture do |root|
|
|
write_skill_ref(root, "test-skill/references/overview.md",
|
|
"Fields: `SPEC.md`, `TASK-PLAN.md`, `VERIFICATION-PLAN.md`, `VERIFICATION.md`, `EVIDENCE-LEDGER.md`, `CHANGE-CONTRACT.md`, `ARCHITECTURE-DELTA.md`.\n")
|
|
assert_empty ReferenceFileScan.stale_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
def test_all_repo_references_scan_clean
|
|
# Guard against the scan drifting from the repository's own conventions:
|
|
# the check must pass on the real tree (the stale RICE reference is gone).
|
|
assert_empty ReferenceFileScan.stale_reference_errors(File.expand_path("..", __dir__), "product-strategy")
|
|
end
|
|
|
|
def test_oversized_reference_under_limit_passes
|
|
with_fixture do |root|
|
|
content = "a" * 59_999
|
|
write_skill_ref(root, "test-skill/references/details.md", content)
|
|
assert_empty ReferenceFileScan.oversized_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
def test_oversized_reference_over_limit_fails_with_path_and_size
|
|
with_fixture do |root|
|
|
content = "a" * 60_001
|
|
write_skill_ref(root, "test-skill/references/details.md", content)
|
|
errors = ReferenceFileScan.oversized_reference_errors(root, "test-skill")
|
|
assert_equal 1, errors.length
|
|
assert_includes errors.first, "test-skill/references/details.md"
|
|
assert_includes errors.first, "60001"
|
|
end
|
|
end
|
|
|
|
def test_oversized_reference_exactly_at_limit_passes
|
|
with_fixture do |root|
|
|
content = "a" * 60_000
|
|
write_skill_ref(root, "test-skill/references/details.md", content)
|
|
assert_equal 60_000, File.read(File.join(root, "test-skill/references/details.md")).length
|
|
assert_empty ReferenceFileScan.oversized_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
def test_oversized_reference_error_mentions_split_remediation
|
|
with_fixture do |root|
|
|
content = "a" * 60_001
|
|
write_skill_ref(root, "test-skill/references/details.md", content)
|
|
errors = ReferenceFileScan.oversized_reference_errors(root, "test-skill")
|
|
assert_equal 1, errors.length
|
|
assert_includes errors.first, "split the file into focused files"
|
|
assert_includes errors.first, "update SKILL.md's index"
|
|
end
|
|
end
|
|
|
|
def test_oversized_reference_scan_ignores_non_markdown_files
|
|
with_fixture do |root|
|
|
content = "a" * 60_001
|
|
write_skill_ref(root, "test-skill/references/notes.txt", content)
|
|
assert_empty ReferenceFileScan.oversized_reference_errors(root, "test-skill")
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# The stale token is assembled at runtime so the misspelling never appears
|
|
# contiguously in a tracked file (the repo-wide typo check must stay clean).
|
|
def stale_rice_token
|
|
"r" + "rice-framework.md"
|
|
end
|
|
|
|
def with_fixture
|
|
Dir.mktmpdir do |directory|
|
|
yield directory
|
|
end
|
|
end
|
|
|
|
def write_skill_ref(root, relative, content)
|
|
path = File.join(root, relative)
|
|
FileUtils.mkdir_p(File.dirname(path))
|
|
File.write(path, content)
|
|
end
|
|
|
|
def write_file(root, relative, content)
|
|
path = File.join(root, relative)
|
|
FileUtils.mkdir_p(File.dirname(path))
|
|
File.write(path, content)
|
|
end
|
|
end
|