1
0
Fork 0

feat(okf): bundle-import iterator (mode b) — per-concept validate+stamp, aggregate disposition, log.md (T7, TDD, +8)

This commit is contained in:
Kjell Tore Guttormsen 2026-07-06 09:32:50 +02:00
commit 320a40244f
2 changed files with 171 additions and 0 deletions

View file

@ -38,9 +38,12 @@ __all__ = [
"trust_for",
"stamp_concept",
"format_log_entry",
"import_bundle",
"Origin",
"Channel",
"ProvenanceStamp",
"ConceptResult",
"BundleResult",
"OKFError",
"OKFFrontmatterError",
"OKFPathError",
@ -273,6 +276,99 @@ def format_log_entry(stamp, *, timestamp=None):
return "\t".join(fields)
# --- T7: bundle-import iterator (mode b) -------------------------------------
# WARN < QUARANTINE_REVIEW < FAIL_SECURE — the aggregate is the most severe.
_DISPOSITION_ORDER = (
Disposition.WARN,
Disposition.QUARANTINE_REVIEW,
Disposition.FAIL_SECURE,
)
@dataclass(frozen=True)
class ConceptResult:
"""The outcome of validating one concept in a bundle.
``error`` is ``None`` on a concept that passed the gates (and then carries a
``stamp``); a non-``None`` ``error`` means a hard reject (bad path, unsafe
frontmatter, or a non-https ``resource``) ``disposition`` is FAIL_SECURE
and no stamp is produced, so the concept must not be merged.
"""
path: str
concept_id: str | None
disposition: Disposition
stamp: ProvenanceStamp | None
report: Report
error: str | None
@dataclass(frozen=True)
class BundleResult:
"""Per-concept results plus the bundle's aggregate (most-severe) disposition."""
concepts: tuple
disposition: Disposition
def log(self):
"""The ``log.md`` body — one line per concept, rejected ones marked."""
lines = []
for c in self.concepts:
if c.stamp is not None:
lines.append(format_log_entry(c.stamp))
else:
lines.append("\t".join([c.path, "REJECTED", c.disposition.value, c.error or ""]))
return "\n".join(lines)
def import_bundle(bundle, *, origin=Origin.EXTERNAL, channel=Channel.AUTOMATIC):
"""Validate a received OKF bundle concept-by-concept before merge (mode b).
``bundle`` maps concept path (e.g. ``tables/users.md``) to its raw document
text. Each concept runs the full per-concept gate path/reserved-name (T4),
frontmatter parse-safety (T2), ``resource`` allowlist (T3), whole-concept
scan (T1) and provenance stamping (T6). A concept that fails a hard gate is
rejected (FAIL_SECURE) and recorded, but iteration continues, so the caller
sees every issue in the bundle, not only the first. The bundle disposition is
the most severe across its concepts.
"""
results = tuple(
_validate_concept(path, bundle[path], origin, channel)
for path in sorted(bundle)
)
aggregate = _most_severe(r.disposition for r in results)
return BundleResult(results, aggregate)
def _validate_concept(path, doc, origin, channel):
try:
concept_id = validate_concept_path(path)
except OKFPathError as exc:
return ConceptResult(path, None, Disposition.FAIL_SECURE, None, Report(), str(exc))
try:
frontmatter, _body = parse_frontmatter(doc)
except OKFFrontmatterError as exc:
return ConceptResult(path, concept_id, Disposition.FAIL_SECURE, None, Report(), str(exc))
resource = frontmatter.get("resource")
if isinstance(resource, str):
try:
validate_resource_url(resource)
except OKFResourceError as exc:
return ConceptResult(path, concept_id, Disposition.FAIL_SECURE, None, Report(), str(exc))
report = scan_concept(doc)
stamp = stamp_concept(concept_id, report, origin, channel)
return ConceptResult(path, concept_id, stamp.disposition, stamp, report, None)
def _most_severe(dispositions):
worst = Disposition.WARN
for disposition in dispositions:
if _DISPOSITION_ORDER.index(disposition) > _DISPOSITION_ORDER.index(worst):
worst = disposition
return worst
def _parse_flat(fm_lines):
result = {}
i = 0