Files
crank/scripts/validate-capability-inventory.py

716 lines
27 KiB
Python

#!/usr/bin/env python3
"""Validate the versioned Crank Community Capability Inventory contract."""
from __future__ import annotations
import argparse
import heapq
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import Any, Iterable, Iterator
SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema"
MAX_DOCUMENT_BYTES = 4_194_304
MAX_FLOWS = 10_000
MAX_REQUIREMENTS_PER_FLOW = 64
MAX_CAPABILITIES_PER_FLOW = 16
MAX_EVIDENCE_LINKS_PER_KIND = 64
MAX_ID_LENGTH = 128
MAX_REQUIREMENT_LENGTH = 128
MAX_CAPABILITY_LENGTH = 128
MAX_OWNER_LENGTH = 256
MAX_OUTCOME_LENGTH = 4_096
MAX_NOTES_LENGTH = 4_096
MAX_EVIDENCE_PATH_LENGTH = 1_024
MAX_DIAGNOSTICS = 1_000
MAX_REPORT_BYTES = 65_536
FLOW_TYPES = ("ui", "api", "mcp")
FLOW_STATUSES = ("implemented", "planned", "gap", "blocked")
ALLOWED_CAPABILITIES = ("tools", "resources", "prompts", "tasks", "load_runs")
FORBIDDEN_CAPABILITIES = (
"multi_workspace", # community-scope: allow=multi-workspace
"enterprise_rbac", # community-scope: allow=enterprise
"sso", # community-scope: allow=sso
"non_rest_upstream", # community-scope: allow=non-rest-upstream
"arbitrary_distributed_load_targets", # community-scope: allow=distributed-load-targets
)
TOP_LEVEL_FIELDS = frozenset({"schema_version", "product", "flows"})
FLOW_FIELDS = frozenset(
{
"id",
"type",
"requirements",
"user_outcome",
"owner",
"status",
"capabilities",
"evidence",
"notes",
}
)
EVIDENCE_FIELDS = frozenset({"automated", "manual"})
SCHEMA_ALLOWED_KEYS: dict[tuple[str, ...], frozenset[str]] = {
(): frozenset(
{
"$schema",
"$id",
"title",
"type",
"additionalProperties",
"required",
"properties",
"$defs",
}
),
("properties",): frozenset({"schema_version", "product", "flows"}),
("properties", "schema_version"): frozenset({"const"}),
("properties", "product"): frozenset({"const"}),
("properties", "flows"): frozenset({"type", "minItems", "maxItems", "items"}),
("properties", "flows", "items"): frozenset({"$ref"}),
("$defs",): frozenset({"flow", "evidence", "evidencePaths"}),
("$defs", "flow"): frozenset(
{"type", "additionalProperties", "required", "properties"}
),
("$defs", "flow", "properties"): FLOW_FIELDS,
("$defs", "flow", "properties", "id"): frozenset(
{"type", "minLength", "maxLength", "pattern"}
),
("$defs", "flow", "properties", "type"): frozenset({"enum"}),
("$defs", "flow", "properties", "requirements"): frozenset(
{"type", "minItems", "maxItems", "uniqueItems", "items"}
),
("$defs", "flow", "properties", "requirements", "items"): frozenset(
{"type", "minLength", "maxLength", "pattern"}
),
("$defs", "flow", "properties", "user_outcome"): frozenset(
{"type", "minLength", "maxLength", "pattern"}
),
("$defs", "flow", "properties", "owner"): frozenset(
{"type", "minLength", "maxLength", "pattern"}
),
("$defs", "flow", "properties", "status"): frozenset({"enum"}),
("$defs", "flow", "properties", "capabilities"): frozenset(
{"type", "minItems", "maxItems", "uniqueItems", "items"}
),
("$defs", "flow", "properties", "capabilities", "items"): frozenset({"enum"}),
("$defs", "flow", "properties", "evidence"): frozenset({"$ref"}),
("$defs", "flow", "properties", "notes"): frozenset({"type", "maxLength"}),
("$defs", "evidence"): frozenset(
{"type", "additionalProperties", "required", "properties"}
),
("$defs", "evidence", "properties"): EVIDENCE_FIELDS,
("$defs", "evidence", "properties", "automated"): frozenset({"$ref"}),
("$defs", "evidence", "properties", "manual"): frozenset({"$ref"}),
("$defs", "evidencePaths"): frozenset(
{"type", "minItems", "maxItems", "uniqueItems", "items"}
),
("$defs", "evidencePaths", "items"): frozenset(
{"type", "minLength", "maxLength", "pattern"}
),
}
FLOW_ID = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
REQUIREMENT_ID = re.compile(r"^FR-[1-9][0-9]*$")
NON_WHITESPACE_PATTERN = r"[\s\S]*\S[\s\S]*"
EVIDENCE_PATH_PATTERN = r"^(?!/)(?!.*//)(?!.*(?:^|/)\.\.?($|/))(?!.*\\).+$"
@dataclass(frozen=True, order=True)
class Diagnostic:
code: str
pointer: str
message: str
@dataclass(frozen=True)
class _ReverseDiagnostic:
diagnostic: Diagnostic
def __lt__(self, other: "_ReverseDiagnostic") -> bool:
return self.diagnostic > other.diagnostic
class DiagnosticCollector:
"""Retain only the lexicographically first diagnostics with an exact omitted count."""
def __init__(self) -> None:
self._heap: list[_ReverseDiagnostic] = []
self.total = 0
def append(self, diagnostic: Diagnostic) -> None:
self.total += 1
wrapped = _ReverseDiagnostic(diagnostic)
if len(self._heap) < MAX_DIAGNOSTICS:
heapq.heappush(self._heap, wrapped)
elif diagnostic < self._heap[0].diagnostic:
heapq.heapreplace(self._heap, wrapped)
def extend(self, diagnostics: Iterable[Diagnostic] | "DiagnosticCollector") -> None:
if isinstance(diagnostics, DiagnosticCollector):
for diagnostic in diagnostics.ordered():
self.append(diagnostic)
self.total += diagnostics.omitted
return
for diagnostic in diagnostics:
self.append(diagnostic)
@property
def omitted(self) -> int:
return self.total - len(self._heap)
def ordered(self) -> list[Diagnostic]:
return sorted(item.diagnostic for item in self._heap)
def __bool__(self) -> bool:
return self.total > 0
class DuplicateJsonMemberError(ValueError):
pass
def reject_duplicate_json_members(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
value: dict[str, Any] = {}
for key, item in pairs:
if key in value:
raise DuplicateJsonMemberError("duplicate JSON object member")
value[key] = item
return value
def reject_nonstandard_json_constant(_: str) -> None:
raise ValueError("non-standard JSON numeric constant")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path.cwd())
parser.add_argument("--inventory", required=True)
parser.add_argument("--schema", required=True)
parser.add_argument("--required-fr", action="append", default=[])
return parser.parse_args()
def path_has_symlink(root: Path, logical: PurePosixPath) -> bool:
current = root
for part in logical.parts:
current = current / part
if current.is_symlink():
return True
return False
def resolve_regular_file(root: Path, raw: str) -> Path | None:
if not raw or len(raw) > MAX_EVIDENCE_PATH_LENGTH or "\\" in raw:
return None
logical = PurePosixPath(raw)
if logical.is_absolute() or any(part in {"", ".", ".."} for part in logical.parts):
return None
if str(logical) != raw or path_has_symlink(root, logical):
return None
candidate = root.joinpath(*logical.parts)
try:
resolved = candidate.resolve(strict=True)
resolved.relative_to(root)
except (FileNotFoundError, OSError, RuntimeError, ValueError):
return None
if not resolved.is_file():
return None
return resolved
def read_json_document(
root: Path,
raw_path: str,
invalid_code: str,
) -> tuple[Any | None, Diagnostic | None]:
path = resolve_regular_file(root, raw_path)
pointer = "/schema" if invalid_code == "INVALID_SCHEMA" else "/inventory"
if path is None:
return None, Diagnostic(invalid_code, pointer, "document path is unavailable")
try:
if path.stat().st_size > MAX_DOCUMENT_BYTES:
return None, Diagnostic("INPUT_TOO_LARGE", pointer, "document exceeds limit")
data = path.read_bytes()
if len(data) > MAX_DOCUMENT_BYTES:
return None, Diagnostic("INPUT_TOO_LARGE", pointer, "document exceeds limit")
text = data.decode("utf-8")
return (
json.loads(
text,
object_pairs_hook=reject_duplicate_json_members,
parse_constant=reject_nonstandard_json_constant,
),
None,
)
except (OSError, UnicodeDecodeError, ValueError, RecursionError, json.JSONDecodeError):
return None, Diagnostic(invalid_code, pointer, "document is not valid UTF-8 JSON")
def nested(document: Any, *parts: str) -> Any:
current = document
for part in parts:
if not isinstance(current, dict) or part not in current:
return None
current = current[part]
return current
def contract_values_equal(actual: Any, expected: Any) -> bool:
if isinstance(expected, list):
return (
isinstance(actual, list)
and all(isinstance(item, str) for item in actual)
and len(actual) == len(set(actual))
and set(actual) == set(expected)
)
return type(actual) is type(expected) and actual == expected
def validate_required_keyword(
schema: dict[str, Any], parts: tuple[str, ...], expected: frozenset[str]
) -> list[Diagnostic]:
raw = nested(schema, *parts)
valid = (
isinstance(raw, list)
and all(isinstance(item, str) for item in raw)
and len(raw) == len(set(raw))
and set(raw) == expected
)
if valid:
return []
return [
Diagnostic(
"INVALID_SCHEMA_CONTRACT",
"/schema/" + "/".join(parts),
"required fields differ from validator",
)
]
def validate_schema_contract(schema: Any) -> list[Diagnostic]:
expected: list[tuple[tuple[str, ...], Any]] = [
(("$schema",), SCHEMA_DIALECT),
(("$id",), "https://crank.local/schemas/capability-inventory.schema.json"),
(("title",), "Crank Community Capability Inventory"),
(("type",), "object"),
(("additionalProperties",), False),
(("properties", "schema_version", "const"), 1),
(("properties", "product", "const"), "crank-community"),
(("properties", "flows", "type"), "array"),
(("properties", "flows", "maxItems"), MAX_FLOWS),
(("properties", "flows", "minItems"), 1),
(("properties", "flows", "items", "$ref"), "#/$defs/flow"),
(("$defs", "flow", "type"), "object"),
(("$defs", "flow", "additionalProperties"), False),
(("$defs", "flow", "properties", "id", "type"), "string"),
(("$defs", "flow", "properties", "id", "minLength"), 1),
(("$defs", "flow", "properties", "id", "maxLength"), MAX_ID_LENGTH),
(("$defs", "flow", "properties", "id", "pattern"), FLOW_ID.pattern),
(("$defs", "flow", "properties", "type", "enum"), list(FLOW_TYPES)),
(("$defs", "flow", "properties", "status", "enum"), list(FLOW_STATUSES)),
(
("$defs", "flow", "properties", "requirements", "maxItems"),
MAX_REQUIREMENTS_PER_FLOW,
),
(("$defs", "flow", "properties", "requirements", "type"), "array"),
(("$defs", "flow", "properties", "requirements", "minItems"), 1),
(("$defs", "flow", "properties", "requirements", "uniqueItems"), True),
(
("$defs", "flow", "properties", "requirements", "items", "type"),
"string",
),
(("$defs", "flow", "properties", "requirements", "items", "minLength"), 1),
(
("$defs", "flow", "properties", "requirements", "items", "maxLength"),
MAX_REQUIREMENT_LENGTH,
),
(
("$defs", "flow", "properties", "requirements", "items", "pattern"),
REQUIREMENT_ID.pattern,
),
(
("$defs", "flow", "properties", "capabilities", "maxItems"),
MAX_CAPABILITIES_PER_FLOW,
),
(("$defs", "flow", "properties", "capabilities", "type"), "array"),
(("$defs", "flow", "properties", "capabilities", "minItems"), 1),
(("$defs", "flow", "properties", "capabilities", "uniqueItems"), True),
(
("$defs", "flow", "properties", "capabilities", "items", "enum"),
list(ALLOWED_CAPABILITIES),
),
(("$defs", "flow", "properties", "owner", "type"), "string"),
(("$defs", "flow", "properties", "owner", "minLength"), 1),
(("$defs", "flow", "properties", "owner", "maxLength"), MAX_OWNER_LENGTH),
(("$defs", "flow", "properties", "owner", "pattern"), NON_WHITESPACE_PATTERN),
(("$defs", "flow", "properties", "user_outcome", "type"), "string"),
(("$defs", "flow", "properties", "user_outcome", "minLength"), 1),
(
("$defs", "flow", "properties", "user_outcome", "maxLength"),
MAX_OUTCOME_LENGTH,
),
(
("$defs", "flow", "properties", "user_outcome", "pattern"),
NON_WHITESPACE_PATTERN,
),
(("$defs", "flow", "properties", "evidence", "$ref"), "#/$defs/evidence"),
(("$defs", "flow", "properties", "notes", "type"), "string"),
(("$defs", "flow", "properties", "notes", "maxLength"), MAX_NOTES_LENGTH),
(("$defs", "evidence", "type"), "object"),
(("$defs", "evidence", "additionalProperties"), False),
(
("$defs", "evidence", "properties", "automated", "$ref"),
"#/$defs/evidencePaths",
),
(
("$defs", "evidence", "properties", "manual", "$ref"),
"#/$defs/evidencePaths",
),
(("$defs", "evidencePaths", "type"), "array"),
(("$defs", "evidencePaths", "minItems"), 1),
(("$defs", "evidencePaths", "maxItems"), MAX_EVIDENCE_LINKS_PER_KIND),
(("$defs", "evidencePaths", "uniqueItems"), True),
(("$defs", "evidencePaths", "items", "type"), "string"),
(("$defs", "evidencePaths", "items", "minLength"), 1),
(("$defs", "evidencePaths", "items", "maxLength"), MAX_EVIDENCE_PATH_LENGTH),
(("$defs", "evidencePaths", "items", "pattern"), EVIDENCE_PATH_PATTERN),
]
if not isinstance(schema, dict):
return [Diagnostic("INVALID_SCHEMA_CONTRACT", "/schema", "schema must be an object")]
diagnostics = []
for parts, allowed_keys in SCHEMA_ALLOWED_KEYS.items():
node = schema if not parts else nested(schema, *parts)
if not isinstance(node, dict):
continue
if any(key not in allowed_keys for key in node):
pointer = "/schema" + ("/" + "/".join(parts) if parts else "")
diagnostics.append(
Diagnostic(
"INVALID_SCHEMA_CONTRACT",
f"{pointer}/<unsupported>",
"schema contains an unsupported keyword",
)
)
for parts, value in expected:
if not contract_values_equal(nested(schema, *parts), value):
diagnostics.append(
Diagnostic(
"INVALID_SCHEMA_CONTRACT",
"/schema/" + "/".join(parts),
"schema contract differs from validator",
)
)
diagnostics.extend(
validate_required_keyword(
schema,
("required",),
frozenset({"schema_version", "product", "flows"}),
)
)
diagnostics.extend(
validate_required_keyword(
schema,
("$defs", "flow", "required"),
frozenset(FLOW_FIELDS - {"notes"}),
)
)
diagnostics.extend(
validate_required_keyword(
schema,
("$defs", "evidence", "required"),
EVIDENCE_FIELDS,
)
)
return diagnostics
def is_nonempty_string(value: Any, maximum: int) -> bool:
return isinstance(value, str) and bool(value.strip()) and len(value) <= maximum
def validate_string_list(
value: Any,
pointer: str,
maximum_items: int,
maximum_length: int,
pattern: re.Pattern[str] | None = None,
) -> list[Diagnostic]:
if not isinstance(value, list) or not value:
return [Diagnostic("MISSING_REQUIRED_FIELD", pointer, "non-empty list is required")]
diagnostics: list[Diagnostic] = []
if len(value) > maximum_items:
diagnostics.append(Diagnostic("LIMIT_EXCEEDED", pointer, "collection exceeds limit"))
seen: set[str] = set()
for index, item in enumerate(value[: maximum_items + 1]):
if not is_nonempty_string(item, maximum_length):
diagnostics.append(
Diagnostic(
"LIMIT_EXCEEDED",
f"{pointer}/{index}",
"string is empty, invalid, or exceeds limit",
)
)
continue
if pattern is not None and pattern.fullmatch(item) is None:
diagnostics.append(
Diagnostic("INVALID_FORMAT", f"{pointer}/{index}", "string format is invalid")
)
if item in seen:
diagnostics.append(
Diagnostic(
"DUPLICATE_LIST_ITEM",
f"{pointer}/{index}",
"collection item is duplicated",
)
)
seen.add(item)
return diagnostics
def reject_unknown_fields(
value: dict[str, Any], allowed: frozenset[str], pointer: str
) -> Iterator[Diagnostic]:
for field in value:
if field not in allowed:
yield Diagnostic("UNKNOWN_FIELD", f"{pointer}/<unknown>", "field is not allowed")
def validate_evidence(root: Path, value: Any, pointer: str) -> Iterator[Diagnostic]:
if not isinstance(value, dict):
yield Diagnostic("MISSING_REQUIRED_FIELD", pointer, "evidence object is required")
return
yield from reject_unknown_fields(value, EVIDENCE_FIELDS, pointer)
for kind in ("automated", "manual"):
paths = value.get(kind)
path_pointer = f"{pointer}/{kind}"
yield from validate_string_list(
paths,
path_pointer,
MAX_EVIDENCE_LINKS_PER_KIND,
MAX_EVIDENCE_PATH_LENGTH,
)
if not isinstance(paths, list):
continue
for index, raw_path in enumerate(paths[: MAX_EVIDENCE_LINKS_PER_KIND + 1]):
if not isinstance(raw_path, str) or resolve_regular_file(root, raw_path) is None:
yield Diagnostic(
"BROKEN_EVIDENCE_LINK",
f"{path_pointer}/{index}",
"evidence path is unavailable",
)
def validate_inventory(
root: Path,
inventory: Any,
required_frs: list[str],
) -> tuple[DiagnosticCollector, dict[str, int]]:
counts = {status: 0 for status in FLOW_STATUSES}
diagnostics = DiagnosticCollector()
if not isinstance(inventory, dict):
diagnostics.append(Diagnostic("MISSING_REQUIRED_FIELD", "/", "inventory must be an object"))
return diagnostics, counts
diagnostics.extend(reject_unknown_fields(inventory, TOP_LEVEL_FIELDS, ""))
schema_version = inventory.get("schema_version")
if type(schema_version) is not int or schema_version != 1:
diagnostics.append(
Diagnostic("MISSING_REQUIRED_FIELD", "/schema_version", "schema_version 1 is required")
)
if inventory.get("product") != "crank-community":
diagnostics.append(
Diagnostic("MISSING_REQUIRED_FIELD", "/product", "product identity is required")
)
flows = inventory.get("flows")
if not isinstance(flows, list) or not flows:
diagnostics.append(Diagnostic("MISSING_REQUIRED_FIELD", "/flows", "non-empty flows are required"))
return diagnostics, counts
if len(flows) > MAX_FLOWS:
diagnostics.append(Diagnostic("LIMIT_EXCEEDED", "/flows", "flow collection exceeds limit"))
seen_ids: set[str] = set()
observed_frs: set[str] = set()
required_fields = (
"id",
"type",
"requirements",
"user_outcome",
"owner",
"status",
"capabilities",
"evidence",
)
for index, flow in enumerate(flows[: MAX_FLOWS + 1]):
pointer = f"/flows/{index}"
if not isinstance(flow, dict):
diagnostics.append(Diagnostic("MISSING_REQUIRED_FIELD", pointer, "flow must be an object"))
continue
diagnostics.extend(reject_unknown_fields(flow, FLOW_FIELDS, pointer))
for field in required_fields:
if field not in flow:
code = "MISSING_OWNER" if field == "owner" else "MISSING_REQUIRED_FIELD"
diagnostics.append(Diagnostic(code, f"{pointer}/{field}", "field is required"))
flow_id = flow.get("id")
if not is_nonempty_string(flow_id, MAX_ID_LENGTH) or not FLOW_ID.fullmatch(flow_id):
diagnostics.append(Diagnostic("LIMIT_EXCEEDED", f"{pointer}/id", "flow id is invalid"))
elif flow_id in seen_ids:
diagnostics.append(Diagnostic("DUPLICATE_FLOW_ID", f"{pointer}/id", "flow id is duplicated"))
else:
seen_ids.add(flow_id)
flow_type = flow.get("type")
if flow_type not in FLOW_TYPES:
diagnostics.append(Diagnostic("UNKNOWN_FLOW_TYPE", f"{pointer}/type", "flow type is unknown"))
owner = flow.get("owner")
if "owner" in flow:
if not isinstance(owner, str) or not owner.strip():
diagnostics.append(
Diagnostic("MISSING_OWNER", f"{pointer}/owner", "owner is required")
)
elif len(owner) > MAX_OWNER_LENGTH:
diagnostics.append(
Diagnostic("LIMIT_EXCEEDED", f"{pointer}/owner", "owner exceeds limit")
)
outcome = flow.get("user_outcome")
if "user_outcome" in flow and not is_nonempty_string(outcome, MAX_OUTCOME_LENGTH):
diagnostics.append(
Diagnostic("LIMIT_EXCEEDED", f"{pointer}/user_outcome", "user outcome is invalid")
)
notes = flow.get("notes")
if notes is not None and (not isinstance(notes, str) or len(notes) > MAX_NOTES_LENGTH):
diagnostics.append(Diagnostic("LIMIT_EXCEEDED", f"{pointer}/notes", "notes exceed limit"))
status = flow.get("status")
if status not in FLOW_STATUSES:
diagnostics.append(Diagnostic("UNKNOWN_STATUS", f"{pointer}/status", "status is unknown"))
else:
counts[status] += 1
requirements = flow.get("requirements")
diagnostics.extend(
validate_string_list(
requirements,
f"{pointer}/requirements",
MAX_REQUIREMENTS_PER_FLOW,
MAX_REQUIREMENT_LENGTH,
REQUIREMENT_ID,
)
)
if isinstance(requirements, list):
for requirement in requirements[: MAX_REQUIREMENTS_PER_FLOW + 1]:
if isinstance(requirement, str) and REQUIREMENT_ID.fullmatch(requirement):
observed_frs.add(requirement)
capabilities = flow.get("capabilities")
diagnostics.extend(
validate_string_list(
capabilities,
f"{pointer}/capabilities",
MAX_CAPABILITIES_PER_FLOW,
MAX_CAPABILITY_LENGTH,
)
)
if isinstance(capabilities, list):
for cap_index, capability in enumerate(capabilities[: MAX_CAPABILITIES_PER_FLOW + 1]):
cap_pointer = f"{pointer}/capabilities/{cap_index}"
if capability in FORBIDDEN_CAPABILITIES:
diagnostics.append(
Diagnostic(
"FORBIDDEN_COMMUNITY_CAPABILITY",
cap_pointer,
f"forbidden capability: {capability}",
)
)
elif isinstance(capability, str) and capability not in ALLOWED_CAPABILITIES:
diagnostics.append(
Diagnostic("LIMIT_EXCEEDED", cap_pointer, "capability is not allowed")
)
diagnostics.extend(validate_evidence(root, flow.get("evidence"), f"{pointer}/evidence"))
for required_fr in sorted(set(required_frs)):
if not REQUIREMENT_ID.fullmatch(required_fr) or len(required_fr) > MAX_REQUIREMENT_LENGTH:
diagnostics.append(
Diagnostic("LIMIT_EXCEEDED", "/required-fr", "required FR identifier is invalid")
)
elif required_fr not in observed_frs:
diagnostics.append(
Diagnostic(
"MISSING_REQUIRED_FR",
"/requirements",
f"mandatory requirement is missing: {required_fr}",
)
)
return diagnostics, counts
def render_report(diagnostics: DiagnosticCollector) -> str:
selected = diagnostics.ordered()
lines = [f"error[{item.code}] {item.pointer}: {item.message}" for item in selected]
if diagnostics.omitted:
lines.append(f"error[TRUNCATED] /: diagnostics omitted={diagnostics.omitted}")
report = "\n".join(lines) + "\n"
encoded = report.encode("utf-8")
if len(encoded) <= MAX_REPORT_BYTES:
return report
marker = "error[TRUNCATED] /: report byte limit reached\n"
marker_bytes = marker.encode("utf-8")
budget = MAX_REPORT_BYTES - len(marker_bytes)
kept: list[str] = []
used = 0
for line in lines:
line_bytes = (line + "\n").encode("utf-8")
if used + len(line_bytes) > budget:
break
kept.append(line)
used += len(line_bytes)
return "\n".join(kept) + ("\n" if kept else "") + marker
def main() -> int:
args = parse_args()
root = args.root.resolve()
schema, schema_error = read_json_document(root, args.schema, "INVALID_SCHEMA")
inventory, inventory_error = read_json_document(root, args.inventory, "INVALID_JSON")
diagnostics = DiagnosticCollector()
diagnostics.extend(error for error in (schema_error, inventory_error) if error is not None)
if schema_error is None:
diagnostics.extend(validate_schema_contract(schema))
counts = {status: 0 for status in FLOW_STATUSES}
if inventory_error is None:
inventory_diagnostics, counts = validate_inventory(root, inventory, args.required_fr)
diagnostics.extend(inventory_diagnostics)
if diagnostics:
sys.stderr.write(render_report(diagnostics))
return 1
total = sum(counts.values())
print(
"Capability inventory validation passed "
f"total={total} implemented={counts['implemented']} "
f"planned={counts['planned']} gap={counts['gap']} "
f"blocked={counts['blocked']} pass={counts['implemented']}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())