mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-18 23:16:38 +03:00
feat: add open-knowledge-format skill
Google's Open Knowledge Format (OKF) v0.1 — an open, vendor-neutral spec for representing knowledge as markdown files with YAML frontmatter, designed for AI agent consumption. - SKILL.md: core instructions, frontmatter spec, conformance criteria, cross-linking rules, relationship to other formats, quick start - references/spec-summary.md: full OKF v0.1 specification reference - references/bundle-architecture.md: directory structure and conventions - references/use-cases.md: real-world adoption and patterns - scripts/okf-bundle-validate.py: validate OKF bundle structure - assets/concept-template.md: template for creating concept files - assets/example-bundle/: minimal conformant OKF bundle
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
okf-bundle-validate.py — Validate an OKF v0.1 knowledge bundle.
|
||||
|
||||
Checks:
|
||||
1. Every non-reserved .md file has parseable YAML frontmatter
|
||||
2. Every frontmatter block has a non-empty "type" field
|
||||
3. Reserved filenames (index.md, log.md) follow their defined structure
|
||||
4. Cross-links point to existing concept files (soft warning)
|
||||
5. No filename collisions between concept IDs
|
||||
|
||||
Usage:
|
||||
python3 okf-bundle-validate.py <bundle-directory>
|
||||
python3 okf-bundle-validate.py <bundle-directory> --fix-missing-types
|
||||
python3 okf-bundle-validate.py <bundle-directory> --json
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
RESERVED = {"index.md", "log.md"}
|
||||
DATE_HEADING_RE = re.compile(r"^##\s+\d{4}-\d{2}-\d{2}")
|
||||
LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
|
||||
MD_LINK_RE = re.compile(r"\]\(((?:/|\./|\.\./)?[^)]+\.md)\)")
|
||||
|
||||
|
||||
class ValidationResult:
|
||||
def __init__(self):
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
self.info = []
|
||||
self.stats = {
|
||||
"concept_files": 0,
|
||||
"reserved_files": 0,
|
||||
"cross_links": 0,
|
||||
"broken_links": 0,
|
||||
}
|
||||
|
||||
def error(self, path, msg):
|
||||
self.errors.append({"path": str(path), "message": msg})
|
||||
|
||||
def warning(self, path, msg):
|
||||
self.warnings.append({"path": str(path), "message": msg})
|
||||
|
||||
def add_info(self, msg):
|
||||
self.info.append(msg)
|
||||
|
||||
@property
|
||||
def passed(self):
|
||||
return len(self.errors) == 0
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"passed": self.passed,
|
||||
"errors": self.errors,
|
||||
"warnings": self.warnings,
|
||||
"info": self.info,
|
||||
"stats": self.stats,
|
||||
}
|
||||
|
||||
def summary(self):
|
||||
parts = []
|
||||
if self.errors:
|
||||
parts.append(f"FAILED: {len(self.errors)} error(s)")
|
||||
else:
|
||||
parts.append("PASSED")
|
||||
parts.append(f"{self.stats['concept_files']} concepts")
|
||||
parts.append(f"{self.stats['reserved_files']} reserved files")
|
||||
parts.append(f"{self.stats['cross_links']} cross-links")
|
||||
if self.stats["broken_links"]:
|
||||
parts.append(f"{self.stats['broken_links']} broken link(s)")
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def parse_frontmatter(content):
|
||||
"""Parse YAML frontmatter from markdown content. Returns (fields, body, error)."""
|
||||
if not content.startswith("---"):
|
||||
return None, content, "File does not start with frontmatter delimiter"
|
||||
|
||||
# Find closing ---
|
||||
end_idx = content.find("\n---", 3)
|
||||
if end_idx == -1:
|
||||
return None, content, "Frontmatter not closed (missing closing ---)"
|
||||
|
||||
yaml_block = content[3:end_idx].strip()
|
||||
body = content[end_idx + 4:].strip()
|
||||
|
||||
if not yaml_block:
|
||||
return {}, body, None
|
||||
|
||||
# Simple YAML parser for the subset OKF uses
|
||||
fields = {}
|
||||
current_key = None
|
||||
list_mode = False
|
||||
|
||||
for line in yaml_block.split("\n"):
|
||||
# Skip blank lines and comments
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
|
||||
# Tag list continuation
|
||||
if list_mode and stripped.startswith("- "):
|
||||
if isinstance(fields.get(current_key), list):
|
||||
fields[current_key].append(stripped[2:])
|
||||
continue
|
||||
else:
|
||||
list_mode = False
|
||||
|
||||
# Key-value pair
|
||||
if ":" in stripped and not stripped.startswith("- "):
|
||||
colon_idx = stripped.index(":")
|
||||
key = stripped[:colon_idx].strip()
|
||||
value = stripped[colon_idx + 1:].strip()
|
||||
current_key = key
|
||||
|
||||
if not value or value == "|" or value == ">":
|
||||
fields[key] = ""
|
||||
elif value.startswith("[") and value.endswith("]"):
|
||||
# Inline list
|
||||
inner = value[1:-1]
|
||||
items = [item.strip().strip("'\"") for item in inner.split(",") if item.strip()]
|
||||
fields[key] = items
|
||||
elif value.startswith("- "):
|
||||
list_mode = True
|
||||
fields[key] = [value[2:]]
|
||||
elif value.lower() == "true":
|
||||
fields[key] = True
|
||||
elif value.lower() == "false":
|
||||
fields[key] = False
|
||||
else:
|
||||
# Remove surrounding quotes
|
||||
if (value.startswith("'") and value.endswith("'")) or \
|
||||
(value.startswith('"') and value.endswith('"')):
|
||||
value = value[1:-1]
|
||||
fields[key] = value
|
||||
|
||||
return fields, body, None
|
||||
|
||||
|
||||
def collect_md_files(bundle_path):
|
||||
"""Collect all .md files in the bundle directory."""
|
||||
md_files = []
|
||||
for root, dirs, files in os.walk(bundle_path):
|
||||
# Skip hidden dirs
|
||||
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||
for f in files:
|
||||
if f.endswith(".md"):
|
||||
abs_path = Path(root) / f
|
||||
rel_path = abs_path.relative_to(bundle_path)
|
||||
md_files.append(rel_path)
|
||||
return sorted(md_files)
|
||||
|
||||
|
||||
def check_index_file(path, content, result):
|
||||
"""Check an index.md file follows OKF conventions."""
|
||||
lines = content.strip().split("\n")
|
||||
|
||||
# Check for frontmatter (should NOT have it, except at bundle root)
|
||||
if content.startswith("---"):
|
||||
# Only allowed at bundle root for okf_version
|
||||
if str(path) != "index.md":
|
||||
result.warning(path, "index.md in subdirectory contains frontmatter (should be frontmatter-free)")
|
||||
else:
|
||||
# Has frontmatter - check it only contains okf_version
|
||||
end_idx = content.find("\n---", 3)
|
||||
if end_idx != -1:
|
||||
yaml_block = content[3:end_idx].strip()
|
||||
if yaml_block and "okf_version" not in yaml_block:
|
||||
result.warning(path, "Root index.md frontmatter should only contain okf_version")
|
||||
|
||||
|
||||
def check_log_file(path, content, result):
|
||||
"""Check a log.md file follows OKF conventions."""
|
||||
lines = content.strip().split("\n")
|
||||
date_headings = 0
|
||||
|
||||
for line in lines:
|
||||
if DATE_HEADING_RE.match(line.strip()):
|
||||
date_headings += 1
|
||||
|
||||
if date_headings == 0:
|
||||
result.warning(path, "log.md has no ISO 8601 date headings (## YYYY-MM-DD)")
|
||||
|
||||
|
||||
def validate_bundle(bundle_path, fix_missing_types=False):
|
||||
"""Validate an OKF v0.1 bundle directory."""
|
||||
result = ValidationResult()
|
||||
bundle_path = Path(bundle_path).resolve()
|
||||
|
||||
if not bundle_path.is_dir():
|
||||
result.error(bundle_path, "Not a directory")
|
||||
return result
|
||||
|
||||
result.add_info(f"Bundle path: {bundle_path}")
|
||||
|
||||
md_files = collect_md_files(bundle_path)
|
||||
if not md_files:
|
||||
result.error(bundle_path, "No markdown files found in bundle")
|
||||
return result
|
||||
|
||||
# Build concept ID map
|
||||
concept_ids = {}
|
||||
for rel_path in md_files:
|
||||
stem = str(rel_path.with_suffix(""))
|
||||
concept_ids[stem] = rel_path
|
||||
|
||||
# Validate each file
|
||||
for rel_path in md_files:
|
||||
abs_path = bundle_path / rel_path
|
||||
filename = rel_path.name
|
||||
|
||||
try:
|
||||
content = abs_path.read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
result.error(rel_path, f"Cannot read file: {e}")
|
||||
continue
|
||||
|
||||
is_reserved = filename in RESERVED
|
||||
|
||||
if is_reserved:
|
||||
result.stats["reserved_files"] += 1
|
||||
else:
|
||||
result.stats["concept_files"] += 1
|
||||
|
||||
# Check frontmatter
|
||||
fields, body, parse_err = parse_frontmatter(content)
|
||||
|
||||
if parse_err:
|
||||
if is_reserved:
|
||||
result.add_info(f"Reserved file {rel_path} has no frontmatter (expected for {filename})")
|
||||
else:
|
||||
result.error(rel_path, parse_err)
|
||||
continue
|
||||
|
||||
# Check reserved file structure
|
||||
if filename == "index.md" and is_reserved:
|
||||
check_index_file(rel_path, content, result)
|
||||
elif filename == "log.md" and is_reserved:
|
||||
check_log_file(rel_path, content, result)
|
||||
|
||||
if is_reserved:
|
||||
continue
|
||||
|
||||
# Check required 'type' field
|
||||
if not fields or "type" not in fields or not fields.get("type"):
|
||||
if fix_missing_types:
|
||||
# We don't actually fix here — this is informational
|
||||
result.error(rel_path, "Missing required 'type' field in frontmatter")
|
||||
else:
|
||||
result.error(rel_path, "Missing required 'type' field in frontmatter")
|
||||
continue
|
||||
|
||||
# Check cross-links
|
||||
md_links = MD_LINK_RE.findall(body) if body else []
|
||||
for link in md_links:
|
||||
result.stats["cross_links"] += 1
|
||||
|
||||
# Resolve relative links
|
||||
if link.startswith("/"):
|
||||
# Bundle-relative
|
||||
target = link[1:].replace(".md", "")
|
||||
elif link.startswith("./") or link.startswith("../"):
|
||||
# Relative — resolve from file's directory
|
||||
file_dir = str(rel_path.parent) if str(rel_path.parent) != "." else ""
|
||||
target = os.path.normpath(os.path.join(file_dir, link)).replace(".md", "")
|
||||
else:
|
||||
# External URL or same-file anchor — skip
|
||||
continue
|
||||
|
||||
# Remove anchors
|
||||
target = target.split("#")[0]
|
||||
|
||||
if target and target not in concept_ids:
|
||||
result.stats["broken_links"] += 1
|
||||
result.warning(rel_path, f"Broken link to '{target}.md' (concept ID: {target})")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate an OKF v0.1 knowledge bundle"
|
||||
)
|
||||
parser.add_argument(
|
||||
"bundle",
|
||||
help="Path to the OKF bundle directory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fix-missing-types",
|
||||
action="store_true",
|
||||
help="Flag missing type fields as errors (default: errors)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Output results as JSON"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
result = validate_bundle(args.bundle, fix_missing_types=args.fix_missing_types)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result.to_dict(), indent=2))
|
||||
else:
|
||||
print("\n" + "=" * 60)
|
||||
print(f" OKF Bundle Validation: {result.summary()}")
|
||||
print("=" * 60)
|
||||
|
||||
if result.errors:
|
||||
print(f"\n Errors ({len(result.errors)}):")
|
||||
for err in result.errors:
|
||||
print(f" ✗ {err['path']}: {err['message']}")
|
||||
|
||||
if result.warnings:
|
||||
print(f"\n Warnings ({len(result.warnings)}):")
|
||||
for warn in result.warnings:
|
||||
print(f" ⚠ {warn['path']}: {warn['message']}")
|
||||
|
||||
if result.info:
|
||||
print(f"\n Info:")
|
||||
for msg in result.info:
|
||||
print(f" ℹ {msg}")
|
||||
|
||||
print()
|
||||
|
||||
return 0 if result.passed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user