diff --git a/Makefile b/Makefile index a36a5f4..c7844a9 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,12 @@ -.PHONY: validate validate-json validate-schemas validate-control-plane validate-eventctl validate-event-store validate-events validate-identity validate-process-provenance validate-service-graph install-dev +.PHONY: validate validate-json validate-schemas validate-control-plane validate-eventctl validate-event-store validate-events validate-identity validate-process-provenance validate-service-graph validate-semantic-enterprise-state-integrity install-dev -validate: validate-json validate-schemas validate-control-plane validate-eventctl validate-event-store validate-events validate-identity validate-process-provenance validate-service-graph +validate: validate-json validate-schemas validate-control-plane validate-eventctl validate-event-store validate-events validate-identity validate-process-provenance validate-service-graph validate-semantic-enterprise-state-integrity install-dev: python3 -m pip install -r requirements-dev.txt validate-json: - python3 - <<'PY' - import json - import pathlib - import sys - - failed = False - for root in (pathlib.Path('schemas'), pathlib.Path('examples')): - if not root.exists(): - continue - for path in sorted(root.rglob('*.json')): - try: - json.loads(path.read_text()) - except Exception as exc: - print(f'{path}: invalid JSON: {exc}', file=sys.stderr) - failed = True - if failed: - raise SystemExit(1) - print('JSON syntax validated.') - PY + python3 tools/validate_json_syntax.py validate-schemas: python3 tools/validate_json_schemas.py @@ -63,3 +45,6 @@ validate-process-provenance: validate-service-graph: python3 tools/sourceos_service_graph.py validate examples/services/*.json python3 tools/sourceos_service_graph.py graph examples/services/*.json --json >/dev/null + +validate-semantic-enterprise-state-integrity: + python3 tools/validate_semantic_enterprise_state_integrity.py diff --git a/docs/semantic-enterprise-state-integrity.md b/docs/semantic-enterprise-state-integrity.md new file mode 100644 index 0000000..a576546 --- /dev/null +++ b/docs/semantic-enterprise-state-integrity.md @@ -0,0 +1,68 @@ +# Semantic Enterprise State Integrity Mapping v0.1 + +`sourceos-syncd` consumes `semantic-enterprise-v0.1.0` from `SocioProphet/ontogenesis` as a state-integrity mapping surface. + +The local fixture is: + +- `examples/semantic-enterprise/v0.1/state-integrity-mapping.example.json` + +The validator is: + +- `tools/validate_semantic_enterprise_state_integrity.py` + +## Source release + +- Repository: `SocioProphet/ontogenesis` +- Release/tag: `semantic-enterprise-v0.1.0` +- Manifest: `manifests/semantic_enterprise_v0_1_manifest.json` +- Rollup registry: `catalog/semantic_enterprise_v0_1_registry.ttl` +- Supply-chain module: `Domains/supply-chain.ttl` +- Named graph fixture: `examples/named-graphs/semantic_sector_named_graphs.ttl` + +## State integrity surfaces + +The v0.1 mapping covers: + +- artifact lineage +- release provenance +- repair lineage +- rollback evidence +- local-first state context +- named graph governance + +## Semantic bindings + +The fixture maps SourceOS state surfaces to Semantic Enterprise concepts: + +- `release_artifact` -> `supply-chain:Component` +- `state_integrity_report` -> `named-graph-governance:CuratedGraph` +- `repair_plan` -> `supply-chain:MitigationAction` +- `rollback_evidence` -> `supply-chain:AlternateSource` + +## Closure boundary + +The mapping distinguishes: + +- `inside_source`: Ontogenesis authors semantic source modules and supply-chain scenarios. +- `outside_state_runtime`: SourceOS syncd maps semantic provenance into local-first state integrity evidence. +- `boundary_membrane`: release tag, source path, graph URI, trust level, access class, retention policy, and lifecycle phase survive translation. +- `feedback_surface`: SourceOS repair, rollback, and state reports remain downstream evidence. + +## Validation + +Run: + +```bash +make validate +``` + +or: + +```bash +python3 tools/validate_semantic_enterprise_state_integrity.py +``` + +## Parent work + +- `SourceOS-Linux/sourceos-syncd#17` +- `SocioProphet/delivery-excellence#21` diff --git a/examples/semantic-enterprise/v0.1/state-integrity-mapping.example.json b/examples/semantic-enterprise/v0.1/state-integrity-mapping.example.json new file mode 100644 index 0000000..c1fb912 --- /dev/null +++ b/examples/semantic-enterprise/v0.1/state-integrity-mapping.example.json @@ -0,0 +1,83 @@ +{ + "contract": "sourceos-syncd.semantic-enterprise.state-integrity", + "version": "0.1.0", + "source": { + "repository": "SocioProphet/ontogenesis", + "release": "semantic-enterprise-v0.1.0", + "manifest_path": "manifests/semantic_enterprise_v0_1_manifest.json", + "rollup_registry_path": "catalog/semantic_enterprise_v0_1_registry.ttl", + "supply_chain_module_path": "Domains/supply-chain.ttl", + "named_graph_fixture_path": "examples/named-graphs/semantic_sector_named_graphs.ttl" + }, + "state_integrity_surfaces": [ + "artifact_lineage", + "release_provenance", + "repair_lineage", + "rollback_evidence", + "local_first_state_context", + "named_graph_governance" + ], + "provenance_requirements": [ + "source_path", + "graph_uri", + "source_system", + "trust_level", + "access_class", + "retention_policy", + "lifecycle_phase", + "release_tag" + ], + "semantic_bindings": [ + { + "sourceos_surface": "release_artifact", + "semantic_enterprise_class": "supply-chain:Component", + "evidence_role": "software-artifact-component", + "required_fields": ["artifact_id", "source_path", "release_tag", "trust_level"] + }, + { + "sourceos_surface": "state_integrity_report", + "semantic_enterprise_class": "named-graph-governance:CuratedGraph", + "evidence_role": "curated-state-perspective", + "required_fields": ["graph_uri", "source_system", "access_class", "retention_policy"] + }, + { + "sourceos_surface": "repair_plan", + "semantic_enterprise_class": "supply-chain:MitigationAction", + "evidence_role": "non-destructive-repair-mitigation", + "required_fields": ["plan_id", "source_path", "affected_component", "approval_state"] + }, + { + "sourceos_surface": "rollback_evidence", + "semantic_enterprise_class": "supply-chain:AlternateSource", + "evidence_role": "alternate-source-or-rollback-reference", + "required_fields": ["rollback_id", "source_path", "previous_state_ref", "trust_level"] + } + ], + "named_graph_contexts": [ + { + "sector": "supply-chain", + "source_path": "examples/scenarios/supply_chain_resilience_demo.ttl", + "graph_uri_fragment": "graphs/scenarios/supply-chain-resilience", + "source_system": "Ontogenesis semantic-enterprise scenario fixture", + "access_class": "internal", + "trust_level": "curated-demo", + "retention_policy": "retain-current-plus-audit-history", + "lifecycle_phase": "KnowledgeCuration" + } + ], + "sourceos_fixture": { + "artifact_id": "sourceos-demo-release-component", + "release_tag": "semantic-enterprise-v0.1.0", + "state_context": "local-first-state-integrity-demo", + "graph_uri_fragment": "graphs/scenarios/supply-chain-resilience", + "trust_level": "curated-demo", + "access_class": "internal", + "operator_narrative": "Semantic Enterprise supply-chain provenance is mapped into SourceOS state integrity evidence without mutating Ontogenesis source semantics." + }, + "closure_model": { + "inside_source": "Ontogenesis authors the semantic source modules and supply-chain scenario.", + "outside_state_runtime": "SourceOS syncd maps semantic provenance into local-first state integrity evidence.", + "boundary_membrane": "Release tag, source path, graph URI, trust level, access class, retention policy, and lifecycle phase must survive translation.", + "feedback_surface": "SourceOS repair, rollback, and state reports remain downstream evidence and do not mutate Ontogenesis source semantics." + } +} diff --git a/src/sourceos_syncd/contracts.py b/src/sourceos_syncd/contracts.py index 9c63ca6..a837be9 100644 --- a/src/sourceos_syncd/contracts.py +++ b/src/sourceos_syncd/contracts.py @@ -65,10 +65,10 @@ class JsonContract: """Base class for JSON-serializable contracts.""" schema: ClassVar[str] - required_fields: ClassVar[set[str]] = set() - controlled_fields: ClassVar[dict[str, set[str]]] = {} - list_fields: ClassVar[set[str]] = set() - mapping_fields: ClassVar[set[str]] = set() + required_fields: ClassVar[set[str]] + controlled_fields: ClassVar[dict[str, set[str]]] + list_fields: ClassVar[set[str]] + mapping_fields: ClassVar[set[str]] def to_dict(self) -> dict[str, Any]: data = dataclasses.asdict(self) @@ -79,12 +79,12 @@ def to_dict(self) -> dict[str, Any]: def validate_dict(cls, record: dict[str, Any]) -> dict[str, Any]: if not isinstance(record, dict): raise ContractError(f"{cls.__name__} must be a JSON object") - require_fields(record, cls.required_fields, cls.__name__) - for field_name, allowed in cls.controlled_fields.items(): + require_fields(record, getattr(cls, "required_fields", set()), cls.__name__) + for field_name, allowed in getattr(cls, "controlled_fields", {}).items(): require_allowed(str(record.get(field_name)), allowed, field_name, cls.__name__) - for field_name in cls.list_fields: + for field_name in getattr(cls, "list_fields", set()): require_list(record, field_name, cls.__name__) - for field_name in cls.mapping_fields: + for field_name in getattr(cls, "mapping_fields", set()): require_mapping(record, field_name, cls.__name__) return record diff --git a/tools/validate_json_syntax.py b/tools/validate_json_syntax.py new file mode 100644 index 0000000..d49fca8 --- /dev/null +++ b/tools/validate_json_syntax.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Validate JSON syntax for repository schemas and examples.""" +from __future__ import annotations + +import json +from pathlib import Path +import sys + +ROOT = Path(__file__).resolve().parents[1] + + +def main() -> int: + failed = False + for root_name in ("schemas", "examples"): + root = ROOT / root_name + if not root.exists(): + continue + for path in sorted(root.rglob("*.json")): + try: + json.loads(path.read_text(encoding="utf-8")) + except Exception as exc: # noqa: BLE001 - syntax validator should report any parse/read failure. + rel = path.relative_to(ROOT) + print(f"{rel}: invalid JSON: {exc}", file=sys.stderr) + failed = True + if failed: + return 1 + print("JSON syntax validated.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/validate_orchestration_examples.py b/tools/validate_orchestration_examples.py index db49499..f52a19d 100644 --- a/tools/validate_orchestration_examples.py +++ b/tools/validate_orchestration_examples.py @@ -25,11 +25,15 @@ OUTCOMES = {"allowed", "denied", "requires_approval", "requires_local_only", "redacted", "degraded"} -def load_json(path: pathlib.Path) -> dict[str, Any]: +def load_json(path: pathlib.Path) -> Any: try: - data = json.loads(path.read_text()) + return json.loads(path.read_text()) except Exception as exc: raise SystemExit(f"{path}: invalid JSON: {exc}") from exc + + +def load_json_object(path: pathlib.Path) -> dict[str, Any]: + data = load_json(path) if not isinstance(data, dict): raise SystemExit(f"{path}: expected top-level JSON object") return data @@ -49,7 +53,7 @@ def collect_ids(items: list[dict[str, Any]], field: str, path: pathlib.Path, err def validate_bundle(path: pathlib.Path) -> list[str]: - obj = load_json(path) + obj = load_json_object(path) errors: list[str] = [] missing = REQUIRED_TOP_LEVEL - set(obj) @@ -123,10 +127,83 @@ def validate_bundle(path: pathlib.Path) -> list[str]: return errors +def validate_event_capability_records(path: pathlib.Path) -> list[str]: + data = load_json(path) + errors: list[str] = [] + if not isinstance(data, list): + return [f"{path}: expected top-level JSON array for *.records.json"] + if not data: + return [f"{path}: record set must not be empty"] + + record_ids: set[str] = set() + for index, record in enumerate(data): + if not isinstance(record, dict): + errors.append(f"{path}: record {index} must be an object") + continue + record_id = record.get("record_id") + if not isinstance(record_id, str) or not record_id: + errors.append(f"{path}: record {index} missing record_id") + elif record_id in record_ids: + errors.append(f"{path}: duplicate record_id {record_id}") + else: + record_ids.add(record_id) + + if record.get("mode") != "event-capability-evidence-v0": + errors.append(f"{path}: record {record_id} mode must be event-capability-evidence-v0") + + event = record.get("event") + capability = record.get("capability") + reaction = record.get("reaction") + if not isinstance(event, dict): + errors.append(f"{path}: record {record_id} missing event object") + continue + if not isinstance(capability, dict): + errors.append(f"{path}: record {record_id} missing capability object") + continue + if not isinstance(reaction, dict): + errors.append(f"{path}: record {record_id} missing reaction object") + continue + + for key in ("event_id", "event_type", "target_node_id"): + if not event.get(key): + errors.append(f"{path}: record {record_id} event missing {key}") + causality = event.get("causality") or {} + for key in ("idempotency_key", "policy_epoch"): + if not causality.get(key): + errors.append(f"{path}: record {record_id} event.causality missing {key}") + + for key in ("capability_id", "display_name", "effect_class", "required_policy_outcome", "approval_mode"): + if not capability.get(key): + errors.append(f"{path}: record {record_id} capability missing {key}") + if capability.get("required_policy_outcome") not in OUTCOMES: + errors.append(f"{path}: record {record_id} capability has invalid required_policy_outcome") + + for key in ("reaction_id", "event_id", "capability_id", "policy_outcome", "status"): + if not reaction.get(key): + errors.append(f"{path}: record {record_id} reaction missing {key}") + if reaction.get("event_id") != event.get("event_id"): + errors.append(f"{path}: record {record_id} reaction event_id must match event.event_id") + if reaction.get("capability_id") != capability.get("capability_id"): + errors.append(f"{path}: record {record_id} reaction capability_id must match capability.capability_id") + if reaction.get("policy_outcome") not in OUTCOMES: + errors.append(f"{path}: record {record_id} reaction has invalid policy_outcome") + if not isinstance(reaction.get("receipt_refs"), list) or not reaction.get("receipt_refs"): + errors.append(f"{path}: record {record_id} reaction must include receipt_refs") + if not isinstance(reaction.get("dead_letter_on_failure"), bool): + errors.append(f"{path}: record {record_id} reaction.dead_letter_on_failure must be boolean") + if not isinstance(record.get("evidence_refs"), list) or not record.get("evidence_refs"): + errors.append(f"{path}: record {record_id} must include evidence_refs") + + return errors + + def main() -> int: errors: list[str] = [] for path in sorted(BUNDLE_DIR.glob("*.json")): - errors.extend(validate_bundle(path)) + if path.name.endswith(".records.json"): + errors.extend(validate_event_capability_records(path)) + else: + errors.extend(validate_bundle(path)) if errors: for error in errors: diff --git a/tools/validate_semantic_enterprise_state_integrity.py b/tools/validate_semantic_enterprise_state_integrity.py new file mode 100644 index 0000000..f1c809d --- /dev/null +++ b/tools/validate_semantic_enterprise_state_integrity.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""Validate SourceOS syncd's Semantic Enterprise v0.1 state-integrity mapping fixture.""" +from __future__ import annotations + +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +FIXTURE = ROOT / "examples/semantic-enterprise/v0.1/state-integrity-mapping.example.json" + +REQUIRED_SURFACES = { + "artifact_lineage", + "release_provenance", + "repair_lineage", + "rollback_evidence", + "local_first_state_context", + "named_graph_governance", +} +REQUIRED_PROVENANCE = { + "source_path", + "graph_uri", + "source_system", + "trust_level", + "access_class", + "retention_policy", + "lifecycle_phase", + "release_tag", +} +REQUIRED_BINDINGS = { + "release_artifact", + "state_integrity_report", + "repair_plan", + "rollback_evidence", +} +REQUIRED_CLOSURE_KEYS = { + "inside_source", + "outside_state_runtime", + "boundary_membrane", + "feedback_surface", +} + + +def main() -> int: + errors: list[str] = [] + if not FIXTURE.is_file(): + print(f"missing fixture: {FIXTURE}") + return 1 + + try: + data = json.loads(FIXTURE.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + print(f"invalid JSON: {exc}") + return 1 + + if data.get("contract") != "sourceos-syncd.semantic-enterprise.state-integrity": + errors.append("unexpected contract identifier") + if data.get("version") != "0.1.0": + errors.append("unexpected contract version") + + source = data.get("source") + if not isinstance(source, dict): + errors.append("source must be an object") + else: + expected = { + "repository": "SocioProphet/ontogenesis", + "release": "semantic-enterprise-v0.1.0", + "manifest_path": "manifests/semantic_enterprise_v0_1_manifest.json", + "rollup_registry_path": "catalog/semantic_enterprise_v0_1_registry.ttl", + "supply_chain_module_path": "Domains/supply-chain.ttl", + "named_graph_fixture_path": "examples/named-graphs/semantic_sector_named_graphs.ttl", + } + for key, value in expected.items(): + if source.get(key) != value: + errors.append(f"source.{key} expected {value!r}, got {source.get(key)!r}") + + surfaces = set(data.get("state_integrity_surfaces") or []) + if not REQUIRED_SURFACES.issubset(surfaces): + errors.append(f"state_integrity_surfaces missing: {sorted(REQUIRED_SURFACES.difference(surfaces))}") + + provenance = set(data.get("provenance_requirements") or []) + if not REQUIRED_PROVENANCE.issubset(provenance): + errors.append(f"provenance_requirements missing: {sorted(REQUIRED_PROVENANCE.difference(provenance))}") + + bindings = data.get("semantic_bindings") + if not isinstance(bindings, list): + errors.append("semantic_bindings must be a list") + else: + binding_surfaces = {binding.get("sourceos_surface") for binding in bindings if isinstance(binding, dict)} + if binding_surfaces != REQUIRED_BINDINGS: + errors.append(f"expected sourceos surfaces {sorted(REQUIRED_BINDINGS)}, got {sorted(binding_surfaces)}") + for binding in bindings: + if not isinstance(binding, dict): + errors.append("semantic binding must be an object") + continue + surface = binding.get("sourceos_surface") + if not binding.get("semantic_enterprise_class"): + errors.append(f"{surface} missing semantic_enterprise_class") + if not binding.get("evidence_role"): + errors.append(f"{surface} missing evidence_role") + required_fields = binding.get("required_fields") + if not isinstance(required_fields, list) or not required_fields: + errors.append(f"{surface} must include required_fields") + + contexts = data.get("named_graph_contexts") + if not isinstance(contexts, list) or not contexts: + errors.append("named_graph_contexts must be a non-empty list") + else: + for context in contexts: + if not isinstance(context, dict): + errors.append("named graph context must be an object") + continue + if context.get("sector") != "supply-chain": + errors.append("SourceOS v0.1 mapping should bind the supply-chain sector first") + if not str(context.get("source_path", "")).startswith("examples/scenarios/"): + errors.append("named graph context source_path must point to examples/scenarios") + if not str(context.get("graph_uri_fragment", "")).startswith("graphs/scenarios/"): + errors.append("named graph context graph_uri_fragment must point to graphs/scenarios") + for key in ["source_system", "access_class", "trust_level", "retention_policy", "lifecycle_phase"]: + if not context.get(key): + errors.append(f"named graph context missing {key}") + + fixture = data.get("sourceos_fixture") + if not isinstance(fixture, dict): + errors.append("sourceos_fixture must be an object") + else: + for key in ["artifact_id", "release_tag", "state_context", "graph_uri_fragment", "trust_level", "access_class", "operator_narrative"]: + if not fixture.get(key): + errors.append(f"sourceos_fixture missing {key}") + if fixture.get("release_tag") != "semantic-enterprise-v0.1.0": + errors.append("sourceos_fixture release_tag mismatch") + + closure = data.get("closure_model") + if not isinstance(closure, dict): + errors.append("closure_model must be an object") + else: + missing = REQUIRED_CLOSURE_KEYS.difference(closure) + if missing: + errors.append(f"closure_model missing keys: {sorted(missing)}") + for key in REQUIRED_CLOSURE_KEYS.intersection(closure): + if not isinstance(closure.get(key), str) or not closure[key].strip(): + errors.append(f"closure_model.{key} must be a non-empty string") + + if errors: + print("Semantic Enterprise state-integrity validation failed:") + for error in errors: + print(f"- {error}") + return 1 + + print("Semantic Enterprise state-integrity validation passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())