fix(skills): correct legacy-modernizer path in fastapi-expert; add ReferencePathChecker

A strict path audit following #225 found one more broken reference:
fastapi-expert's migration-from-django.md cited an absolute-style path
that resolves nowhere. To guard this recurring bug class, add
ReferencePathChecker to validate-skills.py: every backtick or
markdown-link .md path in skill files must resolve relative to the
containing file or the skill root. Runs by default, so CI and
make validate exercise it on every push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E6LA4sndtVGyvoXqwYeHoB
This commit is contained in:
Jeff Smolinski
2026-08-07 12:48:51 -05:00
parent cfce89847a
commit d9101ef77c
4 changed files with 70 additions and 1 deletions
+11
View File
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- `ReferencePathChecker` in `scripts/validate-skills.py`: validates that file paths cited in skill markdown (backtick paths and markdown links) resolve relative to the containing file or the skill root. Broken paths previously failed silently when an agent tried to load deferred reference content; this class of bug has now recurred across several releases and is guarded automatically in CI and `make validate`
### Fixed
- `vue-expert-js/SKILL.md`: three shared-Vue reference paths pointed at `vue-expert/references/*.md`, which does not resolve from the skill directory; corrected to `../vue-expert/references/*.md` (#225)
- `react-expert/references/migration-class-to-modern.md`: self-referencing path `react-expert/references/server-components.md` corrected to `references/server-components.md` (#225)
- `fastapi-expert/references/migration-from-django.md`: cross-reference to legacy-modernizer used an absolute-style path (`/skills/legacy-modernizer/...`) that resolves nowhere; corrected to `../legacy-modernizer/references/migration-strategies.md`. Found by the new `ReferencePathChecker` audit
### Contributors
- @vasugarg09 — Fixed broken relative reference paths in `vue-expert-js` and `react-expert` (#225)
## [0.4.15] - 2026-05-20
### Fixed
+1
View File
@@ -254,6 +254,7 @@ The script validates:
- **Name format** - Letters, numbers, hyphens only
- **Description** - Max 1024 chars, must contain "Use when" trigger clause
- **References** - Directory exists, has files, proper headers
- **Reference paths** - File paths cited in skill markdown resolve relative to the containing file or the skill root
- **Count consistency** - Skills/reference counts match across documentation
**Options:**
+57
View File
@@ -813,6 +813,62 @@ class NonStandardHeadersChecker(BaseChecker):
return issues
class ReferencePathChecker(BaseChecker):
"""Validates that relative file paths cited in skill markdown resolve.
Skill files cite paths to deferred content in backticks (e.g.
`references/testing.md` in routing tables) and markdown links. Agents
resolve these relative to the containing file or the skill root; a path
that resolves from neither base fails silently when an agent tries to
load it. Cross-skill references must use the ../other-skill/ form so
they resolve from those same bases.
"""
name = "reference-paths"
category = "references"
BACKTICK_REF = re.compile(r"`([^`\s]+\.md)`")
MARKDOWN_LINK_REF = re.compile(r"\]\(([^)\s#]+\.md)(?:#[^)]*)?\)")
FENCED_CODE_BLOCK = re.compile(r"^\s*```.*?^\s*```[^\n]*", re.MULTILINE | re.DOTALL)
def check(self, skill_path: Path, skill_name: str) -> list[ValidationIssue]:
issues = []
for md_file in sorted(skill_path.rglob("*.md")):
text = md_file.read_text()
# Fenced code blocks may cite hypothetical example paths; real
# cross-references live in prose, bullets, and routing tables.
prose = self.FENCED_CODE_BLOCK.sub("", text)
refs = set(self.BACKTICK_REF.findall(prose)) | set(self.MARKDOWN_LINK_REF.findall(prose))
lines = text.split("\n")
for ref in sorted(refs):
if self._is_exempt(ref):
continue
if (md_file.parent / ref).exists() or (skill_path / ref).exists():
continue
lineno = next((i for i, line in enumerate(lines, 1) if ref in line), None)
location = f" (line {lineno})" if lineno else ""
issues.append(
ValidationIssue(
skill=skill_name,
check=self.name,
severity=Severity.ERROR,
message=f"Unresolvable file reference '{ref}'{location} - "
"path must resolve relative to the containing file or the skill root",
file=str(md_file),
)
)
return issues
@staticmethod
def _is_exempt(ref: str) -> bool:
"""Skip URLs, bare filenames, and template placeholders."""
if ref.startswith(("http://", "https://")):
return True
if "/" not in ref:
return True
return any(c in ref for c in "{<*")
class MetadataEnumChecker(BaseChecker):
"""Generic checker for metadata enum fields."""
@@ -2001,6 +2057,7 @@ class SkillValidator:
ReferencesDirectoryChecker(),
ReferenceFileCountChecker(),
NonStandardHeadersChecker(),
ReferencePathChecker(),
]
# Filter by category if specified
@@ -961,7 +961,7 @@ async def create_user(user: UserCreate, db: AsyncSession):
## Cross-Reference
For comprehensive migration strategies and modernization patterns:
- **Legacy Modernizer**: `/skills/legacy-modernizer/references/migration-strategies.md`
- **Legacy Modernizer**: `../legacy-modernizer/references/migration-strategies.md`
- Strangler pattern implementation
- Feature flag strategies
- Rollback procedures