Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .console/log.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,37 @@
# Log
## 2026-06-15 — Capability plane charter (docs)

Added `docs/capability-plane-charter.md` (indexed in docs/README.md) — the
"LAST" doc from the capability-registry design. States the capability plane is
**fleet-level** and must NOT be migrated into topology (it's not a RepoNode) or
execution (not a TeamExecutor lane / CoreRunner job / OC task); OC/SwitchBoard/
CoreRunner are consumers, not owners. Restates the four load-bearing invariants
(one owns edge; invocation.ref resolves via Custodian CAP1; target_scope
trichotomy; risk gates a lane) and the language/instances/reality-check layering.

## 2026-06-15 — Capability registry: schema + read-model projection

Added the fleet capability plane as a graph-native module (`src/repograph/capabilities/`):
capabilities are first-class nodes with typed edges (owns/targets/executes/requires/
validates/produces). Ergonomic flat authoring fields compile to the canonical node+edge
graph — the graph is the truth, the fields are sugar.

- `SchemaKind.CAPABILITIES` @ 1.0.0; `EntityKind.CAPABILITY`; "Capability" added to the
public_safe/public_docs projection entity kinds; `project_capability_registry` drops
non-public capabilities; `to_payload()` gives a deterministic hash for boundary coverage.
- Invariants: exactly one `owns` edge per capability (accountability is one, participation
is many); `target_scope` in {repo (resolves), repo_set (selector validated, membership NOT
expanded), fleet (no id/selector)}; risk >= mutates_fleet requires an explicit
preferred_lane; unknown enums fail closed. Repo stays import-free of the fleet:
`invocation.ref` is opaque, resolved later by Custodian (the CAP1 detector, separate PR).
- Tests: `tests/test_capability_registry.py`. Honoured the repo's "genuine tests over
over-excluding" preference — T1/T6 satisfied by direct-import per-symbol tests; only T7
excluded for `capabilities/**` (this repo uses a flat tests/ layout; no parallel-file
tree exists for any subpackage).
- Genericised the illustrative example off a private-repo name to the neutral `MediaForge`
placeholder (B1 boundary).
- 98 tests pass; `custodian-multi` clean (0 findings).

## 2026-06-04 — Console reconciliation: scrub + enforce

Reconciled `.console/` per the console-reconciliation spec (scrub-target boundary, I2).
Expand Down
4 changes: 4 additions & 0 deletions .custodian/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ audit:
- "src/repograph/schema/**"
- "src/repograph/topography/**"
- "src/repograph/topology/**"
# T7 expects tests/unit/repograph/capabilities/test_*.py; this repo uses a
# flat tests/ layout (no parallel-file tree exists for any subpackage).
# T1/T6 are satisfied by genuine direct-import tests, not excluded.
- "src/repograph/capabilities/**"
- "src/repograph/diff.py"
- "src/repograph/errors.py"

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Documentation for the RepoGraph graph-semantics library.

- [Capability Plane Charter](capability-plane-charter.md) — what the capability plane is, the fleet-level ownership boundary, and its invariants
- [Policy Plane](policy-plane.md) — policy boundary and ownership rules
- [Explorer Spec](repograph-explorer-spec.md) — public-safe explorer contract
- [Schema Governance](schema-governance.md) — schema versioning and breaking change rules
64 changes: 64 additions & 0 deletions docs/capability-plane-charter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Capability Plane Charter

Status: active · Schema: `capabilities` @ 1.0.0 · Module: `src/repograph/capabilities/`

## What the capability plane is

The capability plane answers one question: **what can the fleet do, who owns it,
what does it need, how risky is it, and where should it run?** A capability is a
named, ownable *action* — `repo_health_audit`, `board_unblock`, `session_gc` —
modelled as a first-class graph **node** with typed **edges**
(`owns` / `targets` / `executes` / `requires` / `validates` / `produces`).

It exists to make an already-running fleet **legible**, not to add a runtime. The
first consumer is an operator or agent reading the registry into context at
session start. RepoGraph owns the *language*; PlatformManifest authors the
*instances*; Custodian checks them against *reality* (CAP1). The graph is the
truth — the flat authoring fields are sugar that compile to nodes and edges.

## What it is NOT — the ownership boundary (the point of this charter)

Capabilities are a **fleet-level** plane. They are deliberately **not** owned by,
and must **not** be migrated into, any single subsystem:

- **Not topology.** A capability is not a `RepoNode` and does not belong to the
repo-graph. Repos *host* and *own* capabilities (via the `owns` edge); they do
not contain them. The capability plane sits beside topology, not inside it.
- **Not execution.** A capability is not a TeamExecutor lane, a CoreRunner job, or
an OperationsCenter task. `executes` / `requires` edges and `preferred_lane`
*reference* those subsystems; they do not move the capability into them.
`routing.preferred_lane` is descriptive metadata in v1, not a dispatch hook.
- **Not a behaviour catalogue per repo.** There are no per-behaviour `SKILL.md`
files and no "harness runtime" repo. The runtime already exists; this plane
describes it.

Concretely: do not relocate `src/repograph/capabilities/` into OperationsCenter,
SwitchBoard, TeamExecutor, or CoreRunner. Those are **consumers** of the plane
(out of scope for v1), not its home. Centralising the definition is what keeps
"who owns this action?" answerable in one place.

## Load-bearing invariants

1. **Exactly one `owns` edge per capability.** Accountability is singular;
participation (`executes` / `requires` / `validates`) is plural. This edge is
the answer to "does this action belong in repo X?".
2. **`invocation.ref` must resolve in the owning repo.** RepoGraph keeps the ref
opaque (it never imports the fleet); Custodian's **CAP1** detector binds it to
real code in the owning repo. Read-models rot silently — this check is what
stops the registry from drifting into fiction.
3. **`target_scope` trichotomy.** `repo` (resolves an id) · `repo_set` (validates
a selector, does **not** expand membership) · `fleet` (no id/selector).
4. **Risk gates a lane.** Risk ≥ `mutates_fleet` requires an explicit
`preferred_lane`. Unknown enums fail closed.

## Layer boundary

| Layer | Repo | Responsibility |
|-------|------|----------------|
| Language | RepoGraph | capability nodes/edges, schema, validation, projection — **import-free of the fleet** |
| Instances | PlatformManifest | authored capabilities + read-model CLI |
| Reality check | Custodian | CAP1 — `invocation.ref` resolves in the owning repo |
| Consumers (later) | OC / SwitchBoard / CoreRunner | read the registry; **not** owners — see above |

v1 is a **read-model**: no mutation API, no dispatch. Consumers and any
write-path are explicitly future work.
34 changes: 34 additions & 0 deletions src/repograph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,24 @@
RepoEdgeType,
RepoGraph,
)
from .capabilities import (
CapabilityCategory,
CapabilityEdge,
CapabilityEdgeKind,
CapabilityInput,
CapabilityInvocation,
CapabilityInvocationKind,
CapabilityNode,
CapabilityRegistry,
CapabilityRisk,
TargetScope,
TargetScopeKind,
compile_capability,
compile_registry,
load_capability_registry,
project_capability_registry,
validate_capability_registry,
)

__all__ = [
"AlsoHostsEntry",
Expand All @@ -86,6 +104,15 @@
"resolve_private_manifest",
"BoundaryDisclosureArtifact",
"BreakingChangePolicy",
"CapabilityCategory",
"CapabilityEdge",
"CapabilityEdgeKind",
"CapabilityInput",
"CapabilityInvocation",
"CapabilityInvocationKind",
"CapabilityNode",
"CapabilityRegistry",
"CapabilityRisk",
"CompatibilityPolicy",
"CURRENT_SCHEMA_VERSIONS",
"DisclosureMode",
Expand Down Expand Up @@ -120,7 +147,14 @@
"Source",
"SchemaKind",
"SchemaVersion",
"TargetScope",
"TargetScopeKind",
"Visibility",
"compile_capability",
"compile_registry",
"load_capability_registry",
"project_capability_registry",
"validate_capability_registry",
"build_projection_profile",
"build_boundary_artifact",
"build_public_projection",
Expand Down
66 changes: 66 additions & 0 deletions src/repograph/capabilities/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 ProtocolWarden
"""Capability registry — the fleet's action plane.

A capability describes *what the ecosystem can do*, independent of how the
action executes. It is a first-class graph node with exactly one accountable
owner and typed edges to participating repos/artifacts. This package owns the
language; instances live in the platform/private manifests, and Custodian
(CAP1) verifies that ``invocation.ref`` resolves in the owning repo.
"""

from .compiler import (
compile_capability,
compile_registry,
load_capability_registry,
)
from .enums import (
LANE_REQUIRING_RISKS,
REPO_TARGETING_EDGE_KINDS,
CapabilityCategory,
CapabilityEdgeKind,
CapabilityInvocationKind,
CapabilityRisk,
TargetScopeKind,
)
from .models import (
CapabilityEdge,
CapabilityInput,
CapabilityInvocation,
CapabilityNode,
CapabilityRegistry,
TargetScope,
)
from .projection import project_capability_registry
from .validation import (
parse_capability_category,
parse_capability_invocation_kind,
parse_capability_risk,
parse_target_scope_kind,
validate_capability_registry,
)

__all__ = [
"CapabilityCategory",
"CapabilityEdge",
"CapabilityEdgeKind",
"CapabilityInput",
"CapabilityInvocation",
"CapabilityInvocationKind",
"CapabilityNode",
"CapabilityRegistry",
"CapabilityRisk",
"LANE_REQUIRING_RISKS",
"REPO_TARGETING_EDGE_KINDS",
"TargetScope",
"TargetScopeKind",
"compile_capability",
"compile_registry",
"load_capability_registry",
"parse_capability_category",
"parse_capability_invocation_kind",
"parse_capability_risk",
"parse_target_scope_kind",
"project_capability_registry",
"validate_capability_registry",
]
148 changes: 148 additions & 0 deletions src/repograph/capabilities/compiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 ProtocolWarden
"""Compile ergonomic authoring fields into the canonical capability graph.

Authors write flat fields (owner_repo_id / target_scope / executes_on /
requires / validated_by / produces); this module expands them into a
``CapabilityNode`` plus typed ``CapabilityEdge`` list. The compiled graph is the
truth — the flat fields are sugar.
"""

from __future__ import annotations

from typing import Any

from ..errors import RepoGraphConfigError
from ..ontology.enums import DisclosureMode, RepoVisibility
from ..schema.kinds import SchemaKind
from ..schema.validation import validate_schema_document
from .enums import CapabilityEdgeKind, TargetScopeKind
from .models import (
CapabilityEdge,
CapabilityInput,
CapabilityInvocation,
CapabilityNode,
CapabilityRegistry,
TargetScope,
)
from .validation import (
parse_capability_category,
parse_capability_invocation_kind,
parse_capability_risk,
parse_target_scope_kind,
)


def _compile_target_scope(idx: int, raw: dict[str, Any]) -> TargetScope:
kind = parse_target_scope_kind(idx, raw.get("kind"))
selector_raw = raw.get("selector") or {}
if not isinstance(selector_raw, dict):
raise RepoGraphConfigError(
f"capability #{idx} target_scope.selector must be a mapping"
)
selector = tuple(sorted((str(k), v) for k, v in selector_raw.items()))
return TargetScope(kind=kind, repo_id=raw.get("repo_id"), selector=selector)


def _compile_optional_enum(idx: int, raw: Any, enum_cls: Any, default: Any, field_name: str) -> Any:
if raw is None:
return default
try:
return enum_cls(raw)
except ValueError as exc:
raise RepoGraphConfigError(
f"capability #{idx} has unknown {field_name} '{raw}'"
) from exc


def compile_capability(
idx: int, raw: dict[str, Any]
) -> tuple[CapabilityNode, list[CapabilityEdge]]:
action_id = raw.get("action_id")
if not action_id:
raise RepoGraphConfigError(f"capability #{idx} requires action_id")

invocation_raw = raw.get("invocation") or {}
invocation = CapabilityInvocation(
kind=parse_capability_invocation_kind(idx, invocation_raw.get("kind")),
ref=invocation_raw.get("ref", ""),
)

inputs = tuple(
CapabilityInput(
name=item.get("name", ""),
type=item.get("type", ""),
required=bool(item.get("required", False)),
)
for item in (raw.get("inputs") or [])
)

routing = raw.get("routing") or {}
target_scope = _compile_target_scope(idx, raw.get("target_scope") or {})

node = CapabilityNode(
action_id=action_id,
name=raw.get("name", action_id),
category=parse_capability_category(idx, raw.get("category")),
risk=parse_capability_risk(idx, raw.get("risk")),
invocation=invocation,
target_scope=target_scope,
inputs=inputs,
preferred_lane=routing.get("preferred_lane"),
description=raw.get("description"),
visibility=_compile_optional_enum(
idx, raw.get("visibility"), RepoVisibility, RepoVisibility.PUBLIC, "visibility"
),
disclosure_mode=_compile_optional_enum(
idx, raw.get("disclosure_mode"), DisclosureMode, DisclosureMode.PUBLIC, "disclosure_mode"
),
)

owner = raw.get("owner_repo_id")
if not owner:
raise RepoGraphConfigError(f"capability '{action_id}' requires owner_repo_id")

edges: list[CapabilityEdge] = [
CapabilityEdge(action_id, owner, CapabilityEdgeKind.OWNS)
]
if target_scope.kind is TargetScopeKind.REPO and target_scope.repo_id:
edges.append(
CapabilityEdge(action_id, target_scope.repo_id, CapabilityEdgeKind.TARGETS)
)
if raw.get("executes_on"):
edges.append(
CapabilityEdge(action_id, raw["executes_on"], CapabilityEdgeKind.EXECUTES)
)
for repo in raw.get("requires") or []:
edges.append(CapabilityEdge(action_id, repo, CapabilityEdgeKind.REQUIRES))
for repo in raw.get("validated_by") or []:
edges.append(CapabilityEdge(action_id, repo, CapabilityEdgeKind.VALIDATES))
for artifact in raw.get("produces") or []:
edges.append(CapabilityEdge(action_id, artifact, CapabilityEdgeKind.PRODUCES))

return node, edges


def compile_registry(
documents: list[dict[str, Any]],
*,
known_repo_ids: set[str] | None = None,
) -> CapabilityRegistry:
nodes: list[CapabilityNode] = []
edges: list[CapabilityEdge] = []
for idx, raw in enumerate(documents):
node, node_edges = compile_capability(idx, raw)
nodes.append(node)
edges.extend(node_edges)
return CapabilityRegistry.build(nodes, edges, known_repo_ids=known_repo_ids)


def load_capability_registry(
payload: dict[str, Any],
*,
known_repo_ids: set[str] | None = None,
) -> CapabilityRegistry:
"""Validate a versioned ``capabilities`` document and compile it."""
validate_schema_document(payload, expected_kind=SchemaKind.CAPABILITIES)
documents = payload.get("capabilities") or []
return compile_registry(documents, known_repo_ids=known_repo_ids)
Loading
Loading