2026-07-22 01:29:32 -04:00
|
|
|
"""Immutable generic project, node, edge, and context contracts."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-07-22 11:50:49 -04:00
|
|
|
from collections.abc import Mapping
|
2026-07-22 01:29:32 -04:00
|
|
|
from dataclasses import asdict, dataclass
|
|
|
|
|
from pathlib import Path
|
2026-07-25 19:08:39 -04:00
|
|
|
from typing import Protocol, runtime_checkable
|
2026-07-22 01:29:32 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class Limits:
|
|
|
|
|
max_source_bytes: int = 1_000_000
|
|
|
|
|
max_nodes: int = 10_000
|
|
|
|
|
max_query_chars: int = 500
|
|
|
|
|
max_results: int = 100
|
|
|
|
|
max_traversal_depth: int = 8
|
|
|
|
|
max_context_tokens: int = 32_000
|
|
|
|
|
max_tool_output_chars: int = 200_000
|
2026-07-22 02:58:51 -04:00
|
|
|
max_changesets: int = 1_000
|
|
|
|
|
max_changeset_operations: int = 100
|
|
|
|
|
max_changeset_bytes: int = 1_000_000
|
2026-07-22 03:32:05 -04:00
|
|
|
max_render_views: int = 100
|
|
|
|
|
max_template_bytes: int = 1_000_000
|
|
|
|
|
max_render_bytes: int = 1_000_000
|
2026-07-22 02:58:51 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class ProposalWriter:
|
|
|
|
|
writer_id: str
|
|
|
|
|
families: tuple[str, ...]
|
|
|
|
|
operations: tuple[str, ...]
|
2026-07-22 01:29:32 -04:00
|
|
|
|
|
|
|
|
|
2026-07-22 03:32:05 -04:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class RenderView:
|
|
|
|
|
view_id: str
|
|
|
|
|
renderer: str
|
|
|
|
|
template_path: Path
|
|
|
|
|
output_path: Path
|
|
|
|
|
title: str
|
|
|
|
|
families: tuple[str, ...]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class RenderConfig:
|
|
|
|
|
template_root: Path
|
|
|
|
|
preview_root: Path
|
|
|
|
|
views: tuple[RenderView, ...]
|
|
|
|
|
|
|
|
|
|
|
2026-07-22 01:29:32 -04:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class ContextProfile:
|
|
|
|
|
profile_id: str
|
|
|
|
|
families: tuple[str, ...]
|
|
|
|
|
statuses: tuple[str, ...]
|
|
|
|
|
required_nodes: tuple[str, ...]
|
|
|
|
|
token_budget: int
|
|
|
|
|
dependency_depth: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class ProjectDescriptor:
|
|
|
|
|
schema_version: int
|
|
|
|
|
project_id: str
|
|
|
|
|
title: str
|
|
|
|
|
adapter: str
|
|
|
|
|
root: Path
|
|
|
|
|
descriptor_path: Path
|
|
|
|
|
descriptor_hash: str
|
|
|
|
|
content_roots: tuple[Path, ...]
|
|
|
|
|
authority_files: tuple[Path, ...]
|
|
|
|
|
cache_root: Path
|
|
|
|
|
index_path: Path
|
2026-07-22 02:58:51 -04:00
|
|
|
changeset_root: Path
|
|
|
|
|
proposal_writers: tuple[ProposalWriter, ...]
|
2026-07-22 03:32:05 -04:00
|
|
|
render: RenderConfig | None
|
2026-07-22 01:29:32 -04:00
|
|
|
allowed_relations: tuple[str, ...]
|
|
|
|
|
profiles: tuple[ContextProfile, ...]
|
|
|
|
|
limits: Limits
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class Node:
|
|
|
|
|
node_id: str
|
|
|
|
|
title: str
|
|
|
|
|
family: str
|
|
|
|
|
authority: str
|
|
|
|
|
status: str
|
|
|
|
|
tags: tuple[str, ...]
|
|
|
|
|
summary: str
|
|
|
|
|
content: str
|
|
|
|
|
source_path: str
|
|
|
|
|
source_anchor: str | None
|
|
|
|
|
content_hash: str
|
|
|
|
|
|
|
|
|
|
def as_dict(self, *, include_content: bool = True) -> dict[str, object]:
|
|
|
|
|
result = asdict(self)
|
|
|
|
|
if not include_content:
|
|
|
|
|
result.pop("content")
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class Edge:
|
|
|
|
|
source_id: str
|
|
|
|
|
relation: str
|
|
|
|
|
target_id: str
|
|
|
|
|
|
|
|
|
|
def as_dict(self) -> dict[str, str]:
|
|
|
|
|
return asdict(self)
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 19:08:39 -04:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class LogicNode:
|
|
|
|
|
"""One function-scoped control-flow node kept outside the primary graph."""
|
|
|
|
|
|
|
|
|
|
logic_id: str
|
|
|
|
|
kind: str
|
|
|
|
|
label: str
|
|
|
|
|
source_anchor: str | None
|
|
|
|
|
|
|
|
|
|
def as_dict(self) -> dict[str, object]:
|
|
|
|
|
return asdict(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class LogicEdge:
|
|
|
|
|
"""One directed control-flow transition with an explicit branch label."""
|
|
|
|
|
|
|
|
|
|
source_id: str
|
|
|
|
|
relation: str
|
|
|
|
|
target_id: str
|
|
|
|
|
label: str | None = None
|
|
|
|
|
ordinal: int = 0
|
|
|
|
|
|
|
|
|
|
def as_dict(self) -> dict[str, object]:
|
|
|
|
|
return asdict(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class LogicProjection:
|
|
|
|
|
"""A lazy control-flow projection owned by one primary graph symbol."""
|
|
|
|
|
|
|
|
|
|
owner_node_id: str
|
|
|
|
|
source_id: str
|
|
|
|
|
nodes: tuple[LogicNode, ...]
|
|
|
|
|
edges: tuple[LogicEdge, ...]
|
|
|
|
|
|
|
|
|
|
def as_dict(self) -> dict[str, object]:
|
|
|
|
|
return {
|
|
|
|
|
"owner_node_id": self.owner_node_id,
|
|
|
|
|
"source_id": self.source_id,
|
|
|
|
|
"nodes": [node.as_dict() for node in self.nodes],
|
|
|
|
|
"edges": [edge.as_dict() for edge in self.edges],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-07-22 01:29:32 -04:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class ProjectSnapshot:
|
|
|
|
|
descriptor: ProjectDescriptor
|
|
|
|
|
nodes: tuple[Node, ...]
|
|
|
|
|
edges: tuple[Edge, ...]
|
|
|
|
|
source_hash: str
|
|
|
|
|
revision: str
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 19:08:39 -04:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class ProjectState:
|
|
|
|
|
"""Cheap canonical identity used to prove a derived snapshot is current."""
|
|
|
|
|
|
|
|
|
|
source_hash: str
|
|
|
|
|
revision: str
|
|
|
|
|
|
|
|
|
|
|
2026-07-22 04:17:05 -04:00
|
|
|
class ProjectService(Protocol):
|
|
|
|
|
"""Minimum immutable project boundary required by derived read services."""
|
|
|
|
|
|
|
|
|
|
descriptor: ProjectDescriptor
|
|
|
|
|
|
|
|
|
|
def load(self) -> ProjectSnapshot: ...
|
|
|
|
|
|
2026-07-22 05:59:20 -04:00
|
|
|
def canonical_source_paths(self) -> tuple[Path, ...]: ...
|
|
|
|
|
|
2026-07-22 11:50:49 -04:00
|
|
|
def validate_proposal(
|
|
|
|
|
self,
|
|
|
|
|
base: ProjectSnapshot,
|
|
|
|
|
projected: ProjectSnapshot,
|
|
|
|
|
operations: tuple[Mapping[str, object], ...],
|
|
|
|
|
) -> None: ...
|
|
|
|
|
|
2026-07-22 04:17:05 -04:00
|
|
|
|
2026-07-25 19:08:39 -04:00
|
|
|
@runtime_checkable
|
|
|
|
|
class BuildReportingProject(ProjectService, Protocol):
|
|
|
|
|
"""Optional project boundary exposing extraction metrics for builds."""
|
|
|
|
|
|
|
|
|
|
def build_report(self) -> dict[str, object]: ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@runtime_checkable
|
|
|
|
|
class IncrementalStateProject(ProjectService, Protocol):
|
|
|
|
|
"""Optional project boundary for manifest-only stale-state checks."""
|
|
|
|
|
|
|
|
|
|
def incremental_state(self) -> ProjectState | None: ...
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 21:08:43 -04:00
|
|
|
@runtime_checkable
|
|
|
|
|
class LogicProject(ProjectService, Protocol):
|
|
|
|
|
"""Optional project boundary exposing logic from its most recent validated load."""
|
|
|
|
|
|
|
|
|
|
def logic_projections(self) -> tuple[LogicProjection, ...]: ...
|
|
|
|
|
|
|
|
|
|
|
2026-07-22 01:29:32 -04:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class ContextEntry:
|
|
|
|
|
node_id: str
|
|
|
|
|
reason: str
|
|
|
|
|
estimated_tokens: int
|
|
|
|
|
source_path: str
|
|
|
|
|
content_hash: str
|
|
|
|
|
text: str
|
|
|
|
|
|
|
|
|
|
def as_dict(self) -> dict[str, object]:
|
|
|
|
|
return asdict(self)
|