mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
add OKF conformance validator
This commit is contained in:
parent
d694bcd191
commit
f448100888
1 changed files with 90 additions and 0 deletions
90
surfsense_backend/app/services/okf/validator.py
Normal file
90
surfsense_backend/app/services/okf/validator.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
"""Minimal OKF v0.1 conformance checks.
|
||||||
|
|
||||||
|
A bundle conforms if every non-reserved ``.md`` file has a parseable YAML
|
||||||
|
frontmatter block whose ``type`` is a non-empty string. Reserved files
|
||||||
|
(``index.md``, ``log.md``) are exempt from the frontmatter requirement.
|
||||||
|
Consumers must stay permissive, so this is for producers to self-check what they
|
||||||
|
emit, not to reject foreign bundles.
|
||||||
|
|
||||||
|
Spec: https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from app.services.okf.serializer import INDEX_FILENAME, LOG_FILENAME
|
||||||
|
|
||||||
|
_DELIMITER = "---"
|
||||||
|
RESERVED_FILENAMES = frozenset({INDEX_FILENAME, LOG_FILENAME})
|
||||||
|
|
||||||
|
# The OKF v0.1 frontmatter contract, shared by serializer and validator. Only
|
||||||
|
# ``type`` is mandatory; the rest are recommended.
|
||||||
|
REQUIRED_FRONTMATTER_KEYS: tuple[str, ...] = ("type",)
|
||||||
|
RECOMMENDED_FRONTMATTER_KEYS: tuple[str, ...] = (
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"resource",
|
||||||
|
"tags",
|
||||||
|
"timestamp",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_frontmatter(text: str) -> tuple[dict | None, str | None]:
|
||||||
|
"""Split an OKF concept into ``(frontmatter_dict, error)``.
|
||||||
|
|
||||||
|
Returns ``(dict, None)`` on success or ``(None, reason)`` if the frontmatter
|
||||||
|
block is missing or not a parseable YAML mapping.
|
||||||
|
"""
|
||||||
|
if not text.startswith(_DELIMITER + "\n"):
|
||||||
|
return None, "missing opening '---' frontmatter delimiter"
|
||||||
|
rest = text[len(_DELIMITER) + 1 :]
|
||||||
|
end = rest.find("\n" + _DELIMITER)
|
||||||
|
if end == -1:
|
||||||
|
return None, "missing closing '---' frontmatter delimiter"
|
||||||
|
block = rest[:end]
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(block)
|
||||||
|
except yaml.YAMLError as exc:
|
||||||
|
return None, f"frontmatter is not valid YAML: {exc}"
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None, "frontmatter is not a YAML mapping"
|
||||||
|
return data, None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_concept(text: str) -> list[str]:
|
||||||
|
"""Return conformance errors for a single concept document (empty == conforms)."""
|
||||||
|
frontmatter, error = parse_frontmatter(text)
|
||||||
|
if error:
|
||||||
|
return [error]
|
||||||
|
errors: list[str] = []
|
||||||
|
for key in REQUIRED_FRONTMATTER_KEYS:
|
||||||
|
value = frontmatter.get(key)
|
||||||
|
if not isinstance(value, str) or not value.strip():
|
||||||
|
errors.append(f"frontmatter '{key}' is missing or empty")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def is_conformant_concept(text: str) -> bool:
|
||||||
|
"""True if a single concept document conforms to OKF v0.1."""
|
||||||
|
return not validate_concept(text)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_bundle(files: dict[str, str]) -> dict[str, list[str]]:
|
||||||
|
"""Validate a bundle given a mapping of relative path -> file contents.
|
||||||
|
|
||||||
|
Only non-reserved ``.md`` files are checked for concept conformance. Returns
|
||||||
|
a mapping of path -> errors for every non-conformant file (an empty mapping
|
||||||
|
means the whole bundle conforms).
|
||||||
|
"""
|
||||||
|
problems: dict[str, list[str]] = {}
|
||||||
|
for path, contents in files.items():
|
||||||
|
if not path.endswith(".md"):
|
||||||
|
continue
|
||||||
|
filename = path.rsplit("/", 1)[-1]
|
||||||
|
if filename in RESERVED_FILENAMES:
|
||||||
|
continue
|
||||||
|
errors = validate_concept(contents)
|
||||||
|
if errors:
|
||||||
|
problems[path] = errors
|
||||||
|
return problems
|
||||||
Loading…
Add table
Add a link
Reference in a new issue