mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
CPL-3: architecture facts over the pinned carriers + query_code op=architecture
Five pure queries with Ouroboros self-evolution as consumer #1: owner_of(path|symbol), domain_dependencies(d), facade_consumers(sym), persistence_entities_written_by(sym), protected_contracts_affected(diff) — all over data the repo already pins (ouroboros/domains.toml, the generated facade/frozen-contract inventories, docs/PERSISTENCE.md, the runtime_mode_policy protected inventories). New D05 leaf ouroboros/code_intelligence_architecture.py beside code_intelligence (manifest row added; DOMAIN_MAP regenerated); the model consumes through the EXISTING query_code tool via op=architecture — no new registry tool. Suite: tests/test_architecture_facts.py pins every query on real examples plus completeness against each carrier in both directions.
This commit is contained in:
parent
46cf04673d
commit
1a4e763808
7 changed files with 1001 additions and 28 deletions
|
|
@ -72,7 +72,7 @@ Schema (fixed; one row per artifact-level delta family, never per commit):
|
|||
| ABI-10 | plan-item | Reviewer comma-lists → actor rows: the model is already upstream (#384); residual migration read REMOVED by the F3.1 lane D4 train (owner 5.4=A) — legacy block deleted, shipped default panel over the derived env plane (identical models on every config class), comma keys + phase-5 route envs retired via RETIRED_SETTING_KEYS, derived projection kept, bench templates migrated to structured slots with the same models | superseded-by-upstream | done | F3 | tests/test_comma_list_remnant_sweep.py (F3.3 count-anchored remnant sweep — the phase CI gate) + tests/test_comma_list_sweep.py (F3.1 migration-read sweep) + tests/test_reviewer_slot_config.py |
|
||||
| CPL-1 | plan-item | Production manifest ouroboros/domains.toml (module→domain 1:1 over all 488 tracked runtime modules; completeness = red on drift) + domain gate: strict direction matrix pinned as FACTUAL baseline data (164 pairs; new direction = red), cycle gate against the pinned SCC ceiling (today's single 20-domain strict-quotient SCC grandfathered per the Ф5 baseline discipline — current reality = baseline, target `cycle_groups = []`; growth = red), lazy/dynamic import classification pinned (92 lazy-only pairs), cross-domain literal-copy ban (baseline EMPTY — every new copied body is red); DOMAIN_MAP.md generated from the manifest (gen/verify pair). Report-only Ф0 stage superseded: the manifest moved from scripts/v7next_domains.toml, the report stays the witness-level companion on the shared scripts/domain_graph.py core | retain | done | F5 | scripts/check_domains.py (gate + --write regenerator) + tests/test_domain_manifest.py (verify half incl. synthetic red-branch pins) + docs/DOMAIN_MAP.md |
|
||||
| CPL-2 | plan-item | gen/verify pairs shipped for all three inventories: frozen-contracts table (ARCHITECTURE §11.1 machine extraction, owner/anchor path resolution, contracts-package coverage gap pinned), data-layout tree (ARCHITECTURE §1 Data-layout tree — the factual carrier here; the reference PERSISTENCE_OWNERS.md doc does not exist in this tree — probed entry-by-entry against tracked paths and runtime-source literals), facade inventory (AST noqa:F401 re-export scan over the manifest population); staleness = red CI | retain | done | F5 | scripts/regenerate_inventories.py (--check) + tests/test_generated_inventories.py + docs/v7next/FROZEN_CONTRACTS_INVENTORY.md + docs/v7next/DATA_LAYOUT_INVENTORY.md + docs/v7next/FACADE_INVENTORY.md |
|
||||
| CPL-3 | plan-item | code_intelligence architecture facts: owner_of(path\|symbol), domain_dependencies(d), facade_consumers(sym), persistence_entities_written_by(sym), protected_contracts_affected(diff); consumer №1 = Ouroboros self-evolution (Q12=B: completeness ships whole in v7.0) | retain | pending | F5 | code_intelligence fact suite (new) |
|
||||
| CPL-3 | plan-item | code_intelligence architecture facts: owner_of(path\|symbol), domain_dependencies(d), facade_consumers(sym), persistence_entities_written_by(sym), protected_contracts_affected(diff); consumer №1 = Ouroboros self-evolution (Q12=B: completeness ships whole in v7.0). LANDED by the F5 lane C: five pure queries over the pinned carriers (domain manifest, facade/persistence/frozen-contract inventories, runtime_mode_policy protected sets) in `ouroboros/code_intelligence_architecture.py` — a D05 leaf beside code_intelligence; model seam = the EXISTING query_code tool, new `op=architecture` (no new registry tool — lane decision, ledger F5 lane C section) | retain | done | F5 | tests/test_architecture_facts.py (real-example pins + carrier completeness both ways + the query_code op seam) |
|
||||
| CPL-4 | plan-item | Persistence: schema_version/migration/retention/reset decision per durable entity, local (no generic framework); close §16 findings (undocumented planes, unbounded ledgers, mismatched temp — the §16 source is unrecoverable; inventory rebuilt by factual writer scan, disclosed in the F5 lane B ledger section with the candidate-fix table) | retain | done | F5 | docs/PERSISTENCE.md + tests/test_persistence_inventory.py (AST writer scan, count-anchored both ways) |
|
||||
| CPL-5 | plan-item | Runtime invariant model-visible⟺logged, narrowed per F15: sealed model_send records at the last host-controlled pre-transport seam, typed exclusions (provider-native queries/transforms/secrets), reverse-⟺ for model_send only; canonicalization design note BEFORE code (batch-1 Q8=A confirmed) | retain | in-progress | F5 | design note docs/v7next/DESIGN_MODEL_VISIBLE_LOGGED.md (landed, F5 lane B) → invariant reconstruction suite (next lane) |
|
||||
| CPL-6 | plan-item | Conformance contracts for multi-provider seams: LLM providers and the executor axis native\|harness — one normative shared suite every new provider must pass | retain | pending | F5 | shared conformance suite (new) |
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -11,24 +11,24 @@ The manifest is the SSOT of the module→domain assignment (1:1, complete over t
|
|||
| D01 | Agent core & main loop | 29 | 2 |
|
||||
| D02 | LLM client, routing & providers | 33 | 13 |
|
||||
| D03 | Context assembly, fit & compaction | 11 | 1 |
|
||||
| D04 | Tool execution: registry, access & typed results | 20 | 0 |
|
||||
| D04 | Tool execution: registry, access & typed results | 19 | 0 |
|
||||
| D05 | Tool surfaces: files, code, shell, media, external | 24 | 1 |
|
||||
| D06 | Review stack | 62 | 17 |
|
||||
| D07 | Delegation, subagents & Claudexor | 47 | 17 |
|
||||
| D08 | Supervisor: queue, workers, events & runtime control | 41 | 4 |
|
||||
| D09 | Cancellation, owner control & process custody | 12 | 0 |
|
||||
| D07 | Delegation, subagents & Claudexor | 45 | 17 |
|
||||
| D08 | Supervisor: queue, workers, events & runtime control | 40 | 4 |
|
||||
| D09 | Cancellation, owner control & process custody | 11 | 0 |
|
||||
| D10 | Git, update & release machinery | 28 | 1 |
|
||||
| D11 | Gateway, server & Web UI | 45 | 2 |
|
||||
| D11 | Gateway, server & Web UI | 42 | 2 |
|
||||
| D12 | Settings & configuration | 14 | 1 |
|
||||
| D13 | Safety, guards & runtime mode | 8 | 2 |
|
||||
| D14 | Skills & extensions | 51 | 8 |
|
||||
| D15 | Memory, knowledge, consciousness & self-evolution | 16 | 1 |
|
||||
| D16 | Observability, usage accounting & cost | 8 | 0 |
|
||||
| D17 | Projects, workspaces & task results | 20 | 1 |
|
||||
| D18 | Launcher, packaging, platform & shared substrate | 11 | 0 |
|
||||
| D17 | Projects, workspaces & task results | 19 | 1 |
|
||||
| D18 | Launcher, packaging, platform & shared substrate | 10 | 0 |
|
||||
| D19 | Frozen contracts (ABI) | 10 | 0 |
|
||||
| D20 | Presence | 9 | 9 |
|
||||
| **total** | | **499** | **80** |
|
||||
| **total** | | **489** | **80** |
|
||||
|
||||
## Dependency direction matrix (strict, pinned)
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ Rows may import columns (`[graph].allowed`). `·` = forbidden direction.
|
|||
|
||||
## Hidden coupling (classified out of the strict graph)
|
||||
|
||||
- lazy-only cross-domain pairs: **93**
|
||||
- lazy-only cross-domain pairs: **92**
|
||||
- D01->D08
|
||||
- D01->D10
|
||||
- D01->D11
|
||||
|
|
@ -127,7 +127,6 @@ Rows may import columns (`[graph].allowed`). `·` = forbidden direction.
|
|||
- D13->D15
|
||||
- D14->D08
|
||||
- D14->D13
|
||||
- D14->D17
|
||||
- D14->D20
|
||||
- D15->D06
|
||||
- D15->D07
|
||||
|
|
@ -263,7 +262,6 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
|
|||
- `ouroboros/tool_policy.py`
|
||||
- `ouroboros/tools/__init__.py`
|
||||
- `ouroboros/tools/extension_dispatch.py`
|
||||
- `ouroboros/tools/process_facts.py`
|
||||
- `ouroboros/tools/registry.py`
|
||||
- `ouroboros/tools/registry_core.py`
|
||||
- `ouroboros/tools/registry_guard_process.py`
|
||||
|
|
@ -278,9 +276,10 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
|
|||
|
||||
- `ouroboros/artifacts.py`
|
||||
- `ouroboros/code_intelligence.py`
|
||||
- `ouroboros/code_intelligence_architecture.py`
|
||||
- `ouroboros/code_search_rg.py`
|
||||
- `ouroboros/mcp_client.py`
|
||||
- `ouroboros/process_interpreters.py`
|
||||
- `ouroboros/python_interpreter.py`
|
||||
- `ouroboros/tools/browser.py`
|
||||
- `ouroboros/tools/core.py`
|
||||
- `ouroboros/tools/core_artifacts.py`
|
||||
|
|
@ -288,7 +287,6 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
|
|||
- `ouroboros/tools/edit_ops.py`
|
||||
- `ouroboros/tools/health.py`
|
||||
- `ouroboros/tools/media.py`
|
||||
- `ouroboros/tools/owner_delivery.py`
|
||||
- `ouroboros/tools/query_code.py`
|
||||
- `ouroboros/tools/recent_tasks.py`
|
||||
- `ouroboros/tools/search.py`
|
||||
|
|
@ -382,7 +380,6 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
|
|||
- `ouroboros/delegate_pending.py` *
|
||||
- `ouroboros/delegate_progress.py`
|
||||
- `ouroboros/delegate_recovery.py` *
|
||||
- `ouroboros/delegate_registration_policy.py`
|
||||
- `ouroboros/delegate_shared.py`
|
||||
- `ouroboros/delegate_source_coverage.py` *
|
||||
- `ouroboros/delegate_start_claims.py` *
|
||||
|
|
@ -411,7 +408,6 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
|
|||
- `ouroboros/tools/delegate_payload_patch.py`
|
||||
- `ouroboros/tools/delegate_terminal_evidence.py`
|
||||
- `ouroboros/tools/join_ledger.py`
|
||||
- `ouroboros/tools/patch_verdict.py`
|
||||
- `ouroboros/tools/subagent_integration.py`
|
||||
- `ouroboros/tools/subagent_integration_delegated.py`
|
||||
- `ouroboros/tools/task_tree.py`
|
||||
|
|
@ -451,7 +447,6 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
|
|||
- `supervisor/subagent_task_truth.py` *
|
||||
- `supervisor/task_admission.py`
|
||||
- `supervisor/task_dispatch.py` *
|
||||
- `supervisor/telemetry_events.py`
|
||||
- `supervisor/worker_assignment.py`
|
||||
- `supervisor/worker_chat_lane.py`
|
||||
- `supervisor/worker_health.py`
|
||||
|
|
@ -464,7 +459,6 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
|
|||
|
||||
- `ouroboros/cancel_intents.py`
|
||||
- `ouroboros/owner_hurry.py`
|
||||
- `ouroboros/owner_quiz.py`
|
||||
- `ouroboros/process_containment.py`
|
||||
- `ouroboros/process_custody.py`
|
||||
- `ouroboros/server_control.py`
|
||||
|
|
@ -514,7 +508,6 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
|
|||
- `ouroboros/gateway/claudexor_accounts.py`
|
||||
- `ouroboros/gateway/contracts.py`
|
||||
- `ouroboros/gateway/control.py`
|
||||
- `ouroboros/gateway/endpoint_index.py`
|
||||
- `ouroboros/gateway/extensions.py`
|
||||
- `ouroboros/gateway/files.py`
|
||||
- `ouroboros/gateway/history.py`
|
||||
|
|
@ -529,13 +522,11 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
|
|||
- `ouroboros/gateway/presence_settings.py` *
|
||||
- `ouroboros/gateway/projects.py`
|
||||
- `ouroboros/gateway/router.py`
|
||||
- `ouroboros/gateway/routing_decision.py`
|
||||
- `ouroboros/gateway/schedules.py`
|
||||
- `ouroboros/gateway/schema.py`
|
||||
- `ouroboros/gateway/settings.py`
|
||||
- `ouroboros/gateway/skill_publish.py` *
|
||||
- `ouroboros/gateway/state.py`
|
||||
- `ouroboros/gateway/task_decision.py`
|
||||
- `ouroboros/gateway/task_events.py`
|
||||
- `ouroboros/gateway/task_hurry.py`
|
||||
- `ouroboros/gateway/task_list_scan.py`
|
||||
|
|
@ -678,7 +669,6 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
|
|||
- `ouroboros/project_sources.py`
|
||||
- `ouroboros/projects_registry.py`
|
||||
- `ouroboros/retention.py`
|
||||
- `ouroboros/routing_wait.py`
|
||||
- `ouroboros/task_result_schema.py`
|
||||
- `ouroboros/task_results.py`
|
||||
- `ouroboros/task_status.py`
|
||||
|
|
@ -697,7 +687,6 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
|
|||
- `ouroboros/launcher_bootstrap.py`
|
||||
- `ouroboros/launcher_server_reaper.py`
|
||||
- `ouroboros/launcher_windows_runtime.py`
|
||||
- `ouroboros/node_runtime.py`
|
||||
- `ouroboros/packaged_cli.py`
|
||||
- `ouroboros/packaged_cli_install.py`
|
||||
- `ouroboros/platform_layer.py`
|
||||
|
|
|
|||
628
ouroboros/code_intelligence_architecture.py
Normal file
628
ouroboros/code_intelligence_architecture.py
Normal file
|
|
@ -0,0 +1,628 @@
|
|||
"""Architecture facts over the pinned domain/contract/persistence carriers (CPL-3).
|
||||
|
||||
Five pure queries whose consumer #1 is Ouroboros itself (self-evolution):
|
||||
|
||||
- ``owner_of(path|symbol)`` — the domain owner per ``ouroboros/domains.toml``;
|
||||
- ``domain_dependencies(d)`` — the manifest-baseline direction edges of one domain;
|
||||
- ``facade_consumers(sym)`` — who imports through a compatibility facade (the
|
||||
``noqa: F401`` re-export convention the facade inventory documents);
|
||||
- ``persistence_entities_written_by(sym)`` — the durable entities a writer owns
|
||||
per ``docs/PERSISTENCE.md``;
|
||||
- ``protected_contracts_affected(diff)`` — the protected surfaces
|
||||
(``runtime_mode_policy`` inventories) and frozen-contract rows
|
||||
(``docs/v7next/FROZEN_CONTRACTS_INVENTORY.md``) a change set touches.
|
||||
|
||||
Everything here is a pure function over data the repository already pins as
|
||||
SSOT — the domain manifest, the generated inventories, and the protected-path
|
||||
inventories. No LLM, no caches, no ledgers: every reader takes an explicit
|
||||
``repo_root``, reads the carrier files fresh, and raises a teaching
|
||||
``ValueError`` when a carrier is missing or an argument is malformed. The
|
||||
model consumes these through the existing ``query_code`` tool
|
||||
(``op=architecture``) — the seam decision is recorded in the campaign ledger
|
||||
(``docs/v7next/LEDGER_CORRECTIONS.md``, F5 lane C section).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import pathlib
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Iterable, List, Tuple
|
||||
|
||||
from ouroboros.code_intelligence import CodeInventory, _resolve_relative_import
|
||||
|
||||
DOMAIN_MANIFEST_RELPATH = "ouroboros/domains.toml"
|
||||
PERSISTENCE_DOC_RELPATH = "docs/PERSISTENCE.md"
|
||||
FROZEN_INVENTORY_RELPATH = "docs/v7next/FROZEN_CONTRACTS_INVENTORY.md"
|
||||
|
||||
ARCHITECTURE_FACTS = (
|
||||
"owner_of",
|
||||
"domain_dependencies",
|
||||
"facade_consumers",
|
||||
"persistence_entities_written_by",
|
||||
"protected_contracts_affected",
|
||||
)
|
||||
|
||||
# The facade convention the generated facade inventory documents: a top-level
|
||||
# ``from <module> import name`` carrying the F401-noqa marker is a re-export.
|
||||
_NOQA_F401 = re.compile(r"#\s*noqa(?::[^#]*\bF401\b|\s*$|:\s*$)")
|
||||
_BACKTICK_SPAN = re.compile(r"`([^`]+)`")
|
||||
_WORD = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
||||
_DIFF_GIT = re.compile(r"^diff --git a/(\S+) b/(\S+)$", re.MULTILINE)
|
||||
_DIFF_FILE = re.compile(r"^(?:\+\+\+|---) (?:[ab]/)?(\S+)", re.MULTILINE)
|
||||
_DIFF_RENAME = re.compile(r"^rename (?:from|to) (\S+)$", re.MULTILINE)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Carrier loading
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DomainManifest:
|
||||
"""The parsed domain manifest: vocabulary, assignment, baseline edges."""
|
||||
|
||||
domains: Dict[str, str] # "D02" -> title
|
||||
modules: Dict[str, str] # repo-relative path -> domain id
|
||||
graph_allowed: Tuple[str, ...] # strict baseline "D01->D02" pairs
|
||||
lazy_only: Tuple[str, ...] # lazy-only baseline pairs
|
||||
|
||||
|
||||
def _read_carrier(repo_root: pathlib.Path, relpath: str) -> str:
|
||||
path = pathlib.Path(repo_root) / relpath
|
||||
try:
|
||||
return path.read_text(encoding="utf-8")
|
||||
except OSError as exc:
|
||||
raise ValueError(
|
||||
f"architecture carrier {relpath} is unreadable under {repo_root} "
|
||||
f"({exc}); architecture facts are defined over an Ouroboros "
|
||||
"checkout that carries its pinned inventories"
|
||||
) from exc
|
||||
|
||||
|
||||
def load_domain_manifest(repo_root: pathlib.Path) -> DomainManifest:
|
||||
try: # Python 3.11+
|
||||
import tomllib
|
||||
except ImportError: # pragma: no cover - 3.10 venvs ship tomli
|
||||
import tomli as tomllib
|
||||
|
||||
raw = _read_carrier(repo_root, DOMAIN_MANIFEST_RELPATH)
|
||||
try:
|
||||
data = tomllib.loads(raw)
|
||||
except Exception as exc:
|
||||
raise ValueError(f"{DOMAIN_MANIFEST_RELPATH} does not parse as TOML: {exc}") from exc
|
||||
graph = data.get("graph") or {}
|
||||
return DomainManifest(
|
||||
domains={str(k): str(v) for k, v in (data.get("domains") or {}).items()},
|
||||
modules={str(k): str(v) for k, v in (data.get("modules") or {}).items()},
|
||||
graph_allowed=tuple(str(p) for p in graph.get("allowed") or ()),
|
||||
lazy_only=tuple(str(p) for p in graph.get("lazy_only") or ()),
|
||||
)
|
||||
|
||||
|
||||
def _module_dotted(path: str) -> str:
|
||||
parts = path[:-3].split("/") if path.endswith(".py") else path.split("/")
|
||||
if parts and parts[-1] == "__init__":
|
||||
parts = parts[:-1]
|
||||
return ".".join(parts)
|
||||
|
||||
|
||||
def _split_md_row(line: str) -> List[str]:
|
||||
"""Split one markdown table row on unescaped pipes (inventory convention)."""
|
||||
body = line.strip().strip("|")
|
||||
cells: List[str] = []
|
||||
cur: List[str] = []
|
||||
escaped = False
|
||||
for ch in body:
|
||||
if escaped:
|
||||
cur.append(ch)
|
||||
escaped = False
|
||||
elif ch == "\\":
|
||||
cur.append(ch)
|
||||
escaped = True
|
||||
elif ch == "|":
|
||||
cells.append("".join(cur).strip())
|
||||
cur = []
|
||||
else:
|
||||
cur.append(ch)
|
||||
cells.append("".join(cur).strip())
|
||||
return cells
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# owner_of
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DomainOwner:
|
||||
query: str
|
||||
module: str # the manifest module the query resolved to
|
||||
domain: str # "D02"
|
||||
domain_title: str
|
||||
via: str # module_path | dotted_module | symbol_definition
|
||||
|
||||
|
||||
def owner_of(
|
||||
repo_root: pathlib.Path,
|
||||
query: str,
|
||||
*,
|
||||
inventory: CodeInventory | None = None,
|
||||
) -> Tuple[DomainOwner, ...]:
|
||||
"""Domain owner(s) of a module path, dotted module, or defined symbol.
|
||||
|
||||
A path or dotted module resolves directly against the manifest; a bare
|
||||
symbol resolves through the code inventory to its defining module(s) and
|
||||
each definition inside the manifest population reports its owner. A target
|
||||
outside the runtime module population returns ``()`` — no domain owns it.
|
||||
"""
|
||||
manifest = load_domain_manifest(repo_root)
|
||||
text = str(query or "").strip().replace("\\", "/")
|
||||
if not text:
|
||||
raise ValueError("owner_of requires a module path, dotted module, or symbol name")
|
||||
norm = text[2:] if text.startswith("./") else text
|
||||
|
||||
def _owner(path: str, via: str) -> DomainOwner:
|
||||
domain = manifest.modules[path]
|
||||
return DomainOwner(text, path, domain, manifest.domains.get(domain, ""), via)
|
||||
|
||||
if norm in manifest.modules:
|
||||
return (_owner(norm, "module_path"),)
|
||||
if "/" in norm or norm.endswith(".py"):
|
||||
return () # a real path outside the runtime module population
|
||||
dotted_map = {_module_dotted(path): path for path in manifest.modules}
|
||||
if norm in dotted_map:
|
||||
return (_owner(dotted_map[norm], "dotted_module"),)
|
||||
if "." in norm:
|
||||
return () # dotted, but not a population module
|
||||
if inventory is None:
|
||||
from ouroboros.code_intelligence import build_code_inventory
|
||||
|
||||
inventory = build_code_inventory(pathlib.Path(repo_root), persist=False)
|
||||
from ouroboros.code_intelligence import symbol_definitions
|
||||
|
||||
owners = {
|
||||
file.path
|
||||
for file, _symbol in symbol_definitions(inventory, norm)
|
||||
if file.path in manifest.modules
|
||||
}
|
||||
return tuple(_owner(path, "symbol_definition") for path in sorted(owners))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# domain_dependencies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DomainDependencies:
|
||||
domain: str
|
||||
title: str
|
||||
outgoing: Tuple[str, ...] # strict manifest-baseline edges out of the domain
|
||||
incoming: Tuple[str, ...] # strict edges into the domain
|
||||
lazy_outgoing: Tuple[str, ...] # lazy-only baseline edges out
|
||||
lazy_incoming: Tuple[str, ...] # lazy-only baseline edges in
|
||||
|
||||
|
||||
def domain_dependencies(repo_root: pathlib.Path, domain: str) -> DomainDependencies:
|
||||
"""The manifest-baseline dependency edges of one domain (both directions)."""
|
||||
manifest = load_domain_manifest(repo_root)
|
||||
dom = str(domain or "").strip().upper()
|
||||
if dom not in manifest.domains:
|
||||
known = ", ".join(sorted(manifest.domains))
|
||||
raise ValueError(f"unknown domain {domain!r}; the manifest domains are: {known}")
|
||||
|
||||
def _ends(pairs: Iterable[str]) -> Tuple[Tuple[str, ...], Tuple[str, ...]]:
|
||||
out: List[str] = []
|
||||
inc: List[str] = []
|
||||
for pair in pairs:
|
||||
src, _, dst = pair.partition("->")
|
||||
if src == dom:
|
||||
out.append(dst)
|
||||
if dst == dom:
|
||||
inc.append(src)
|
||||
return tuple(sorted(out)), tuple(sorted(inc))
|
||||
|
||||
outgoing, incoming = _ends(manifest.graph_allowed)
|
||||
lazy_out, lazy_in = _ends(manifest.lazy_only)
|
||||
return DomainDependencies(
|
||||
dom, manifest.domains[dom], outgoing, incoming, lazy_out, lazy_in,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# facade_consumers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FacadeConsumer:
|
||||
facade: str # the facade module path
|
||||
consumer: str # the module importing through the facade
|
||||
name: str # the imported name ("" = the facade module itself, "*" = star)
|
||||
line: int
|
||||
|
||||
|
||||
def _population_resolver(modules: Dict[str, str]):
|
||||
dotted_map = {_module_dotted(path): path for path in modules}
|
||||
|
||||
def resolve(dotted: str) -> str | None:
|
||||
parts = dotted.split(".")
|
||||
for i in range(len(parts), 0, -1):
|
||||
candidate = ".".join(parts[:i])
|
||||
if candidate in dotted_map:
|
||||
return dotted_map[candidate]
|
||||
return None
|
||||
|
||||
return resolve
|
||||
|
||||
|
||||
def facade_reexports(
|
||||
repo_root: pathlib.Path,
|
||||
manifest: DomainManifest | None = None,
|
||||
) -> Dict[str, Dict[str, str]]:
|
||||
"""``facade path -> {exported name: leaf path}`` over the manifest population.
|
||||
|
||||
Same convention the generated facade inventory pins: a top-level
|
||||
``from <population module> import ...`` statement carrying the
|
||||
``noqa: F401`` marker declares re-export bindings.
|
||||
"""
|
||||
root = pathlib.Path(repo_root)
|
||||
manifest = manifest or load_domain_manifest(root)
|
||||
resolve = _population_resolver(manifest.modules)
|
||||
facades: Dict[str, Dict[str, str]] = {}
|
||||
for path in sorted(manifest.modules):
|
||||
source_path = root / path
|
||||
if not source_path.is_file():
|
||||
continue
|
||||
source = source_path.read_text(encoding="utf-8")
|
||||
lines = source.splitlines()
|
||||
try:
|
||||
tree = ast.parse(source, filename=path)
|
||||
except SyntaxError:
|
||||
continue
|
||||
for node in tree.body:
|
||||
if not isinstance(node, ast.ImportFrom):
|
||||
continue
|
||||
end = getattr(node, "end_lineno", node.lineno) or node.lineno
|
||||
if not any(
|
||||
_NOQA_F401.search(lines[lineno - 1])
|
||||
for lineno in range(node.lineno, min(end, len(lines)) + 1)
|
||||
):
|
||||
continue
|
||||
base = _resolve_relative_import(
|
||||
pathlib.PurePosixPath(path), node.module or "", int(node.level or 0),
|
||||
)
|
||||
for alias in node.names:
|
||||
if alias.name == "*":
|
||||
leaf = resolve(base)
|
||||
else:
|
||||
leaf = resolve(f"{base}.{alias.name}") or resolve(base)
|
||||
if leaf is None or leaf == path:
|
||||
continue
|
||||
exported = alias.asname or alias.name
|
||||
facades.setdefault(path, {})[exported] = leaf
|
||||
return facades
|
||||
|
||||
|
||||
def facade_consumers(repo_root: pathlib.Path, sym: str) -> Tuple[FacadeConsumer, ...]:
|
||||
"""Who imports through a facade — for a facade module, or one re-exported name.
|
||||
|
||||
``sym`` may be a facade module (path or dotted) — every import of that
|
||||
facade across the population is a consumer row — or a bare re-exported
|
||||
name — the rows narrow to ``from <facade> import <name>`` sites of the
|
||||
facades that re-export it. Attribute access on a plain module import
|
||||
(``import ouroboros.llm`` then ``llm.chat``) is deliberately out of scope:
|
||||
only import statements are counted.
|
||||
"""
|
||||
root = pathlib.Path(repo_root)
|
||||
manifest = load_domain_manifest(root)
|
||||
reexports = facade_reexports(root, manifest)
|
||||
text = str(sym or "").strip().replace("\\", "/")
|
||||
if not text:
|
||||
raise ValueError("facade_consumers requires a facade module or a re-exported name")
|
||||
|
||||
name_filter = ""
|
||||
if text in manifest.modules or "/" in text or text.endswith(".py"):
|
||||
targets = {text} if text in reexports else set()
|
||||
if not targets:
|
||||
raise ValueError(
|
||||
f"{text} is not a facade module (no top-level noqa:F401 re-exports); "
|
||||
"see docs/v7next/FACADE_INVENTORY.md for the facade list"
|
||||
)
|
||||
else:
|
||||
dotted_map = {_module_dotted(path): path for path in reexports}
|
||||
if text in dotted_map:
|
||||
targets = {dotted_map[text]}
|
||||
else:
|
||||
targets = {facade for facade, exports in reexports.items() if text in exports}
|
||||
name_filter = text
|
||||
if not targets:
|
||||
raise ValueError(
|
||||
f"no facade re-exports a name or matches a module {text!r}; "
|
||||
"facade_consumers answers about noqa:F401 facade bindings"
|
||||
)
|
||||
|
||||
resolve = _population_resolver(manifest.modules)
|
||||
rows: List[FacadeConsumer] = []
|
||||
for path in sorted(manifest.modules):
|
||||
source_path = root / path
|
||||
if path in targets or not source_path.is_file():
|
||||
continue
|
||||
try:
|
||||
tree = ast.parse(source_path.read_text(encoding="utf-8"), filename=path)
|
||||
except SyntaxError:
|
||||
continue
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
resolved = resolve(alias.name)
|
||||
if resolved in targets and not name_filter:
|
||||
rows.append(FacadeConsumer(resolved, path, "", node.lineno))
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
base = _resolve_relative_import(
|
||||
pathlib.PurePosixPath(path), node.module or "", int(node.level or 0),
|
||||
)
|
||||
for alias in node.names:
|
||||
if alias.name == "*":
|
||||
resolved = resolve(base)
|
||||
if resolved in targets and not name_filter:
|
||||
rows.append(FacadeConsumer(resolved, path, "*", node.lineno))
|
||||
continue
|
||||
base_module = resolve(base)
|
||||
named_module = resolve(f"{base}.{alias.name}")
|
||||
if named_module in targets and named_module != base_module:
|
||||
# ``from ouroboros import llm`` — the facade module itself.
|
||||
if not name_filter:
|
||||
rows.append(FacadeConsumer(named_module, path, "", node.lineno))
|
||||
continue
|
||||
if base_module in targets:
|
||||
if name_filter and alias.name != name_filter:
|
||||
continue
|
||||
rows.append(FacadeConsumer(base_module, path, alias.name, node.lineno))
|
||||
return tuple(sorted(rows, key=lambda r: (r.facade, r.consumer, r.line, r.name)))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# persistence_entities_written_by
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PersistenceWrite:
|
||||
entity: str # the row's Path cell (entity label, backticks kept)
|
||||
section: str # the "## ..." section the row lives in
|
||||
writer_cell: str # the row's raw Writer cell
|
||||
matched: str # the token of ``sym`` that matched the writer cell
|
||||
|
||||
|
||||
def _persistence_rows(repo_root: pathlib.Path) -> List[Tuple[str, str, str]]:
|
||||
"""Every ``(section, entity_cell, writer_cell)`` row of docs/PERSISTENCE.md."""
|
||||
text = _read_carrier(repo_root, PERSISTENCE_DOC_RELPATH)
|
||||
rows: List[Tuple[str, str, str]] = []
|
||||
section = ""
|
||||
header: List[str] = []
|
||||
for line in text.splitlines():
|
||||
if line.startswith("## "):
|
||||
section = line[3:].strip()
|
||||
header = []
|
||||
continue
|
||||
if not line.startswith("|"):
|
||||
header = []
|
||||
continue
|
||||
cells = _split_md_row(line)
|
||||
if [c.lower() for c in cells[:2]] == ["path", "writer"]:
|
||||
header = cells
|
||||
continue
|
||||
if header and set(line.replace("|", "").strip()) <= {"-", " ", ":"}:
|
||||
continue
|
||||
if header and len(cells) >= 2:
|
||||
rows.append((section, cells[0], cells[1]))
|
||||
if not rows:
|
||||
raise ValueError(
|
||||
f"{PERSISTENCE_DOC_RELPATH} carries no Path|Writer table rows; "
|
||||
"the persistence inventory is the carrier this query reads"
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def persistence_entities_written_by(
|
||||
repo_root: pathlib.Path, sym: str,
|
||||
) -> Tuple[PersistenceWrite, ...]:
|
||||
"""Durable entities whose PERSISTENCE.md writer cell names ``sym``.
|
||||
|
||||
``sym`` may be a writer module (path or dotted — matched against the
|
||||
backticked module spans of the Writer column) or a bare function name
|
||||
(word-matched inside the Writer cell prose, e.g. ``save_settings``).
|
||||
"""
|
||||
text = str(sym or "").strip().replace("\\", "/")
|
||||
if not text:
|
||||
raise ValueError("persistence_entities_written_by requires a writer module or name")
|
||||
if "." in text and "/" not in text and not text.endswith(".py"):
|
||||
candidate = text.replace(".", "/") + ".py"
|
||||
manifest = load_domain_manifest(repo_root)
|
||||
if candidate in manifest.modules:
|
||||
text = candidate
|
||||
is_path = "/" in text or text.endswith(".py")
|
||||
rows: List[PersistenceWrite] = []
|
||||
for section, entity, writer in _persistence_rows(repo_root):
|
||||
if is_path:
|
||||
spans = _BACKTICK_SPAN.findall(writer)
|
||||
if not any(span == text or span.endswith("/" + text) for span in spans):
|
||||
continue
|
||||
elif text not in _WORD.findall(writer):
|
||||
continue
|
||||
rows.append(PersistenceWrite(entity, section, writer, text))
|
||||
return tuple(rows)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# protected_contracts_affected
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ContractImpact:
|
||||
contract: str # the frozen-contract row label
|
||||
path: str # the changed path that hits the row
|
||||
role: str # owner | anchor
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProtectedImpact:
|
||||
paths: Tuple[str, ...] # normalized changed paths considered
|
||||
protected: Tuple[Any, ...] # runtime_mode_policy.ProtectedPath rows
|
||||
contracts: Tuple[ContractImpact, ...] # frozen-contract rows affected
|
||||
|
||||
|
||||
def _frozen_contract_rows(repo_root: pathlib.Path) -> List[Tuple[str, str, str]]:
|
||||
"""``(label, file, role)`` triples of the generated frozen-contracts inventory."""
|
||||
text = _read_carrier(repo_root, FROZEN_INVENTORY_RELPATH)
|
||||
triples: List[Tuple[str, str, str]] = []
|
||||
for line in text.splitlines():
|
||||
if line.startswith("- browser-envelope prose owners:"):
|
||||
for span in _BACKTICK_SPAN.findall(line):
|
||||
triples.append(("browser-envelope ABI", span, "owner"))
|
||||
continue
|
||||
if not line.startswith("|"):
|
||||
continue
|
||||
cells = _split_md_row(line)
|
||||
if len(cells) < 4 or not cells[0].isdigit():
|
||||
continue
|
||||
label_match = _BACKTICK_SPAN.search(cells[1])
|
||||
label = (label_match.group(1) if label_match else cells[1]).replace("\\|", "|")
|
||||
for cell, role in ((cells[2], "owner"), (cells[3], "anchor")):
|
||||
for span in _BACKTICK_SPAN.findall(cell):
|
||||
file_part = span.replace("\\|", "|").split("::", 1)[0].split(" ", 1)[0]
|
||||
if file_part:
|
||||
triples.append((label, file_part, role))
|
||||
if not triples:
|
||||
raise ValueError(
|
||||
f"{FROZEN_INVENTORY_RELPATH} carries no contract rows; regenerate the "
|
||||
"inventory (scripts/regenerate_inventories.py) before asking for impact"
|
||||
)
|
||||
return triples
|
||||
|
||||
|
||||
def paths_from_diff(diff: str | Iterable[str]) -> Tuple[str, ...]:
|
||||
"""Changed paths from a unified diff text, or a pass-through path iterable."""
|
||||
from ouroboros.runtime_mode_policy import normalize_repo_path
|
||||
|
||||
if isinstance(diff, str):
|
||||
raw: List[str] = []
|
||||
if _DIFF_GIT.search(diff) or _DIFF_FILE.search(diff):
|
||||
for a_path, b_path in _DIFF_GIT.findall(diff):
|
||||
raw.extend((a_path, b_path))
|
||||
raw.extend(_DIFF_FILE.findall(diff))
|
||||
raw.extend(_DIFF_RENAME.findall(diff))
|
||||
else:
|
||||
raw.extend(part for part in re.split(r"[,\s]+", diff) if part)
|
||||
candidates = raw
|
||||
else:
|
||||
candidates = [str(p) for p in diff]
|
||||
normalized = {
|
||||
normalize_repo_path(p)
|
||||
for p in candidates
|
||||
if p and p != "/dev/null" and not p.startswith("/dev/")
|
||||
}
|
||||
return tuple(sorted(p for p in normalized if p and p != "."))
|
||||
|
||||
|
||||
def protected_contracts_affected(
|
||||
repo_root: pathlib.Path, diff: str | Iterable[str],
|
||||
) -> ProtectedImpact:
|
||||
"""Protected surfaces and frozen contracts a change set touches.
|
||||
|
||||
``diff`` is a unified diff text or an iterable of changed repo-relative
|
||||
paths. Protected categories come from the ``runtime_mode_policy``
|
||||
inventories (safety-critical / frozen-contract / release-invariant); the
|
||||
contract rows come from the generated frozen-contracts inventory — a
|
||||
changed path that is an owner or anchor file of a row names that contract.
|
||||
"""
|
||||
from ouroboros.runtime_mode_policy import protected_paths_in
|
||||
|
||||
paths = paths_from_diff(diff)
|
||||
if not paths:
|
||||
raise ValueError(
|
||||
"protected_contracts_affected received no changed paths; pass a "
|
||||
"unified diff or a comma/space-separated repo-relative path list"
|
||||
)
|
||||
protected = tuple(protected_paths_in(paths))
|
||||
contracts: List[ContractImpact] = []
|
||||
triples = _frozen_contract_rows(repo_root)
|
||||
for path in paths:
|
||||
for label, file_part, role in triples:
|
||||
if file_part == path:
|
||||
contracts.append(ContractImpact(label, path, role))
|
||||
deduped = tuple(sorted(set(contracts), key=lambda c: (c.contract, c.path, c.role)))
|
||||
return ProtectedImpact(paths=paths, protected=protected, contracts=deduped)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The query_code op=architecture renderer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def architecture_fact_rows(
|
||||
repo_root: pathlib.Path,
|
||||
query: str,
|
||||
*,
|
||||
inventory: CodeInventory | None = None,
|
||||
) -> List[str]:
|
||||
"""Render one architecture fact as compact tool rows.
|
||||
|
||||
``query`` is ``"<fact> <argument>"`` where fact is one of
|
||||
``ARCHITECTURE_FACTS``; for ``protected_contracts_affected`` the argument
|
||||
is a comma/space-separated changed-path list (or a pasted unified diff).
|
||||
"""
|
||||
text = str(query or "").strip()
|
||||
fact, _, arg = text.partition(" ")
|
||||
fact = fact.strip().lower()
|
||||
arg = arg.strip()
|
||||
if fact not in ARCHITECTURE_FACTS:
|
||||
raise ValueError(
|
||||
"op=architecture takes query='<fact> <argument>' with fact one of: "
|
||||
+ ", ".join(ARCHITECTURE_FACTS)
|
||||
)
|
||||
if not arg:
|
||||
raise ValueError(f"architecture fact {fact} requires an argument after the fact name")
|
||||
root = pathlib.Path(repo_root)
|
||||
if fact == "owner_of":
|
||||
owners = owner_of(root, arg, inventory=inventory)
|
||||
if not owners:
|
||||
return [f"{arg}: no domain owner — not in the runtime module population "
|
||||
f"({DOMAIN_MANIFEST_RELPATH})"]
|
||||
return [
|
||||
f"{row.module} -> {row.domain} ({row.domain_title}) [{row.via}]"
|
||||
for row in owners
|
||||
]
|
||||
if fact == "domain_dependencies":
|
||||
deps = domain_dependencies(root, arg)
|
||||
return [
|
||||
f"{deps.domain} ({deps.title}) — manifest-baseline strict edges",
|
||||
f"imports: {', '.join(deps.outgoing) or '—'}",
|
||||
f"imported by: {', '.join(deps.incoming) or '—'}",
|
||||
f"lazy-only imports: {', '.join(deps.lazy_outgoing) or '—'}",
|
||||
f"lazy-only imported by: {', '.join(deps.lazy_incoming) or '—'}",
|
||||
]
|
||||
if fact == "facade_consumers":
|
||||
rows = facade_consumers(root, arg)
|
||||
if not rows:
|
||||
return [f"{arg}: facade has no import-statement consumers in the population"]
|
||||
return [
|
||||
f"{row.consumer}:{row.line} imports "
|
||||
+ (f"{row.name} from {row.facade}" if row.name else f"{row.facade}")
|
||||
for row in rows
|
||||
]
|
||||
if fact == "persistence_entities_written_by":
|
||||
writes = persistence_entities_written_by(root, arg)
|
||||
if not writes:
|
||||
return [f"{arg}: no durable entity in {PERSISTENCE_DOC_RELPATH} names this writer"]
|
||||
return [
|
||||
f"{row.entity} [{row.section}] writer: {row.writer_cell}"
|
||||
for row in writes
|
||||
]
|
||||
impact = protected_contracts_affected(root, arg)
|
||||
rows = [
|
||||
f"{len(impact.paths)} changed path(s): {len(impact.protected)} protected, "
|
||||
f"{len(impact.contracts)} frozen-contract row(s) affected"
|
||||
]
|
||||
rows.extend(f"{p.path} -> {p.category}" for p in impact.protected)
|
||||
rows.extend(
|
||||
f"frozen contract `{c.contract}` ({c.role}: {c.path})" for c in impact.contracts
|
||||
)
|
||||
return rows
|
||||
|
|
@ -75,6 +75,7 @@ D20 = "Presence"
|
|||
"ouroboros/cli.py" = "D18"
|
||||
"ouroboros/client_surface.py" = "D11"
|
||||
"ouroboros/code_intelligence.py" = "D05"
|
||||
"ouroboros/code_intelligence_architecture.py" = "D05"
|
||||
"ouroboros/code_search_rg.py" = "D05"
|
||||
"ouroboros/colab_bootstrap.py" = "D12"
|
||||
"ouroboros/commit_admission.py" = "D06"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ _OPS = (
|
|||
"impact",
|
||||
"structural",
|
||||
"digest",
|
||||
"architecture",
|
||||
)
|
||||
_MAX_LIMIT = 200
|
||||
# Structural walks read every candidate file, so bound them for an arbitrary
|
||||
|
|
@ -371,7 +372,22 @@ def _query_code(
|
|||
offset = max(0, int(offset or 0))
|
||||
|
||||
try:
|
||||
if op == "structural":
|
||||
if op == "architecture":
|
||||
# Architecture facts (CPL-3) are defined over the Ouroboros repo's
|
||||
# pinned inventories — the manifest/inventory carriers live in the
|
||||
# code roots, never in a skill payload or an external target.
|
||||
if normalized_root not in ("active_workspace", "system_repo"):
|
||||
return (
|
||||
"⚠️ TOOL_ARG_ERROR (query_code): op architecture requires "
|
||||
"root=active_workspace or system_repo."
|
||||
)
|
||||
from ouroboros.code_intelligence_architecture import architecture_fact_rows
|
||||
|
||||
try:
|
||||
rows = architecture_fact_rows(repo_root, query)
|
||||
except ValueError as exc:
|
||||
return f"⚠️ TOOL_ARG_ERROR (query_code): {exc}"
|
||||
elif op == "structural":
|
||||
rows = _structural(
|
||||
ctx, repo_root, query, scoped_path, str(lang or "any"), limit, binding
|
||||
)
|
||||
|
|
@ -470,11 +486,16 @@ def get_tools() -> List[ToolEntry]:
|
|||
"...); op=structural (node-type queries) is polyglot too — tree-sitter for every supported "
|
||||
"language (Python/JS/TS/Go/Rust/Java/Ruby/C/C++/C#/PHP/Kotlin/Swift/Scala/Lua/Bash), with a "
|
||||
"visible structural_unavailable:<lang> marker when a grammar is missing (Python also has a "
|
||||
"stdlib-ast fallback). Returns compact file:line anchors and signatures/snippets, never full bodies."
|
||||
"stdlib-ast fallback). op=architecture answers over the Ouroboros repo's pinned inventories "
|
||||
"(domain manifest, facade/persistence/frozen-contract carriers): query='<fact> <argument>' with "
|
||||
"fact one of owner_of, domain_dependencies, facade_consumers, persistence_entities_written_by, "
|
||||
"protected_contracts_affected (argument = path/symbol, domain id, facade/name, writer, or a "
|
||||
"comma-separated changed-path list). Returns compact file:line anchors and signatures/snippets, "
|
||||
"never full bodies."
|
||||
),
|
||||
"parameters": {"type": "object", "properties": {
|
||||
"op": {"type": "string", "enum": list(_OPS), "description": "Operation: relevant_files (where to look), digest (whole-repo map), symbols, definition, references, callers, callees, impact, structural."},
|
||||
"query": {"type": "string", "default": "", "description": "Exact symbol name (definition/references/callers/...), AST node type (structural), or task text (relevant_files). Empty for digest."},
|
||||
"op": {"type": "string", "enum": list(_OPS), "description": "Operation: relevant_files (where to look), digest (whole-repo map), symbols, definition, references, callers, callees, impact, structural, architecture (domain/facade/persistence/protected facts)."},
|
||||
"query": {"type": "string", "default": "", "description": "Exact symbol name (definition/references/callers/...), AST node type (structural), task text (relevant_files), or '<fact> <argument>' (architecture). Empty for digest."},
|
||||
"path": {"type": "string", "default": "", "description": "Optional file/dir scope or definition disambiguator. REQUIRED for root=user_files (the explicit target dir/file, e.g. '/app' or '/app/src'); it is never the whole home."},
|
||||
"lang": {"type": "string", "enum": ["python", "javascript", "typescript", "go", "rust", "java", "ruby", "c", "cpp", "csharp", "php", "kotlin", "swift", "scala", "lua", "bash", "any"], "default": "any"},
|
||||
"kind": {"type": "string", "enum": ["function", "async_function", "class", "constant", "any"], "default": "any"},
|
||||
|
|
|
|||
333
tests/test_architecture_facts.py
Normal file
333
tests/test_architecture_facts.py
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
"""CPL-3 architecture-fact suite (plan §7.3).
|
||||
|
||||
Pins the five pure queries of ``ouroboros/code_intelligence_architecture.py``
|
||||
on the REAL repository carriers — the domain manifest, the generated facade /
|
||||
frozen-contract inventories, ``docs/PERSISTENCE.md`` and the
|
||||
``runtime_mode_policy`` protected inventories — plus their completeness
|
||||
against those carriers (a manifest row, facade row, persistence row or
|
||||
frozen-contract row the queries cannot reach = red), and the model-facing
|
||||
seam: the existing ``query_code`` tool's ``op=architecture``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from ouroboros.code_intelligence_architecture import (
|
||||
ARCHITECTURE_FACTS,
|
||||
FROZEN_INVENTORY_RELPATH,
|
||||
PERSISTENCE_DOC_RELPATH,
|
||||
_frozen_contract_rows,
|
||||
_persistence_rows,
|
||||
architecture_fact_rows,
|
||||
domain_dependencies,
|
||||
facade_consumers,
|
||||
facade_reexports,
|
||||
load_domain_manifest,
|
||||
owner_of,
|
||||
paths_from_diff,
|
||||
persistence_entities_written_by,
|
||||
protected_contracts_affected,
|
||||
)
|
||||
|
||||
REPO = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def manifest():
|
||||
return load_domain_manifest(REPO)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def reexports(manifest):
|
||||
return facade_reexports(REPO, manifest)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# owner_of
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_owner_of_resolves_a_module_path_a_dotted_module_and_a_symbol():
|
||||
(llm,) = owner_of(REPO, "ouroboros/llm.py")
|
||||
assert (llm.domain, llm.via) == ("D02", "module_path")
|
||||
assert llm.domain_title # the human vocabulary rides along
|
||||
|
||||
(state,) = owner_of(REPO, "supervisor.state")
|
||||
assert (state.module, state.domain, state.via) == (
|
||||
"supervisor/state.py", "D08", "dotted_module",
|
||||
)
|
||||
|
||||
owners = owner_of(REPO, "protected_path_category")
|
||||
assert any(
|
||||
(o.module, o.domain, o.via) == ("ouroboros/runtime_mode_policy.py", "D13", "symbol_definition")
|
||||
for o in owners
|
||||
), owners
|
||||
|
||||
|
||||
def test_owner_of_outside_the_population_is_ownerless_not_an_error():
|
||||
assert owner_of(REPO, "web/modules/chat.js") == ()
|
||||
assert owner_of(REPO, "docs/ARCHITECTURE.md") == ()
|
||||
|
||||
|
||||
def test_owner_of_covers_every_manifest_module(manifest):
|
||||
"""Completeness against the manifest: every population row answers with
|
||||
exactly its pinned domain."""
|
||||
assert len(manifest.modules) >= 400 # the whole runtime population, not a sample
|
||||
for path, domain in manifest.modules.items():
|
||||
rows = owner_of(REPO, path)
|
||||
assert [(r.module, r.domain) for r in rows] == [(path, domain)], path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# domain_dependencies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_domain_dependencies_reports_the_manifest_baseline_edges(manifest):
|
||||
deps = domain_dependencies(REPO, "d02") # case-normalized
|
||||
assert deps.domain == "D02" and deps.title == manifest.domains["D02"]
|
||||
assert set(deps.outgoing) == {
|
||||
d.split("->")[1] for d in manifest.graph_allowed if d.startswith("D02->")
|
||||
}
|
||||
assert set(deps.incoming) == {
|
||||
d.split("->")[0] for d in manifest.graph_allowed if d.endswith("->D02")
|
||||
}
|
||||
|
||||
|
||||
def test_domain_dependencies_cover_the_whole_direction_matrix(manifest):
|
||||
"""Completeness: the union of per-domain answers reproduces [graph].allowed
|
||||
and [graph].lazy_only exactly — both directions."""
|
||||
strict, lazy = set(), set()
|
||||
for domain in manifest.domains:
|
||||
deps = domain_dependencies(REPO, domain)
|
||||
strict.update(f"{domain}->{dst}" for dst in deps.outgoing)
|
||||
strict.update(f"{src}->{domain}" for src in deps.incoming)
|
||||
lazy.update(f"{domain}->{dst}" for dst in deps.lazy_outgoing)
|
||||
lazy.update(f"{src}->{domain}" for src in deps.lazy_incoming)
|
||||
assert strict == set(manifest.graph_allowed)
|
||||
assert lazy == set(manifest.lazy_only)
|
||||
|
||||
|
||||
def test_domain_dependencies_unknown_domain_teaches_the_vocabulary():
|
||||
with pytest.raises(ValueError, match="unknown domain.*D01"):
|
||||
domain_dependencies(REPO, "D99")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# facade_consumers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_facade_scan_matches_the_generated_facade_inventory(reexports):
|
||||
"""Completeness against the gen/verify-pinned carrier: the runtime scan
|
||||
finds exactly the facade modules docs/v7next/FACADE_INVENTORY.md pins."""
|
||||
inventory_text = (REPO / "docs/v7next/FACADE_INVENTORY.md").read_text(encoding="utf-8")
|
||||
pinned = set()
|
||||
for line in inventory_text.splitlines():
|
||||
if line.startswith("| `") and line.count("|") >= 4:
|
||||
first = line.split("|")[1].strip()
|
||||
if first.startswith("`") and first.endswith("`"):
|
||||
pinned.add(first.strip("`"))
|
||||
assert pinned, "the facade inventory carrier parsed empty"
|
||||
assert set(reexports) == pinned
|
||||
|
||||
|
||||
def test_facade_module_query_lists_its_import_consumers():
|
||||
rows = facade_consumers(REPO, "ouroboros/llm.py")
|
||||
consumers = {r.consumer for r in rows}
|
||||
assert "ouroboros/agent.py" in consumers # from ouroboros.llm import LLMClient
|
||||
assert all(r.facade == "ouroboros/llm.py" and r.line > 0 for r in rows)
|
||||
|
||||
|
||||
def test_facade_symbol_query_narrows_to_the_reexported_name(reexports):
|
||||
rows = facade_consumers(REPO, "add_usage")
|
||||
assert rows, "add_usage is a re-exported facade binding with real consumers"
|
||||
for row in rows:
|
||||
assert row.name == "add_usage"
|
||||
assert "add_usage" in reexports[row.facade]
|
||||
assert "ouroboros/loop_llm_call.py" in {r.consumer for r in rows}
|
||||
|
||||
|
||||
def test_facade_query_on_a_non_facade_is_a_teaching_refusal():
|
||||
with pytest.raises(ValueError, match="not a facade module"):
|
||||
facade_consumers(REPO, "ouroboros/runtime_mode_policy.py")
|
||||
with pytest.raises(ValueError, match="no facade re-exports"):
|
||||
facade_consumers(REPO, "definitely_not_an_exported_name")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# persistence_entities_written_by
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_writer_module_query_names_its_entities():
|
||||
entities = " | ".join(
|
||||
r.entity for r in persistence_entities_written_by(REPO, "supervisor/state.py")
|
||||
)
|
||||
assert "state/state.json" in entities
|
||||
ledger = persistence_entities_written_by(REPO, "ouroboros/usage_ledger.py")
|
||||
assert any("usage_attempts.jsonl" in r.entity for r in ledger)
|
||||
# A dotted spelling of the same writer answers identically.
|
||||
assert persistence_entities_written_by(REPO, "supervisor.state") == \
|
||||
persistence_entities_written_by(REPO, "supervisor/state.py")
|
||||
|
||||
|
||||
def test_writer_function_name_query_matches_writer_prose():
|
||||
rows = persistence_entities_written_by(REPO, "save_settings")
|
||||
assert [r.entity for r in rows] == ["`settings.json`"]
|
||||
|
||||
|
||||
def test_persistence_parser_reaches_every_table_row(manifest):
|
||||
"""Completeness against the carrier: the parser yields one row per
|
||||
Path|Writer table line of docs/PERSISTENCE.md (nothing silently dropped),
|
||||
and every exact .py writer span resolves against the tree."""
|
||||
rows = _persistence_rows(REPO)
|
||||
text = (REPO / PERSISTENCE_DOC_RELPATH).read_text(encoding="utf-8")
|
||||
raw_rows = 0
|
||||
in_table = False
|
||||
for line in text.splitlines():
|
||||
if line.startswith("| Path | Writer |"):
|
||||
in_table = True
|
||||
continue
|
||||
if not line.startswith("|"):
|
||||
in_table = False
|
||||
continue
|
||||
if in_table and not set(line.replace("|", "").strip()) <= {"-", " ", ":"}:
|
||||
raw_rows += 1
|
||||
assert raw_rows == len(rows) and raw_rows >= 50, (raw_rows, len(rows))
|
||||
|
||||
span_re = re.compile(r"`([^`]+\.py)`")
|
||||
for _section, _entity, writer in rows:
|
||||
for span in span_re.findall(writer):
|
||||
if any(ch in span for ch in "*<>"):
|
||||
continue # glob/placeholder spans are labels, not paths
|
||||
resolved = (REPO / span).is_file() or any(
|
||||
module.endswith("/" + span) for module in manifest.modules
|
||||
) # the doc shortens sibling writers to bare filenames in listings
|
||||
assert resolved, f"PERSISTENCE.md names a missing writer: {span}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# protected_contracts_affected
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_protected_diff_names_categories_and_contracts():
|
||||
diff = (
|
||||
"diff --git a/ouroboros/gateway/contracts.py b/ouroboros/gateway/contracts.py\n"
|
||||
"--- a/ouroboros/gateway/contracts.py\n"
|
||||
"+++ b/ouroboros/gateway/contracts.py\n"
|
||||
"@@ -1 +1 @@\n-1\n+2\n"
|
||||
"diff --git a/BIBLE.md b/BIBLE.md\n"
|
||||
"--- a/BIBLE.md\n+++ b/BIBLE.md\n@@ -1 +1 @@\n-1\n+2\n"
|
||||
"diff --git a/README.md b/README.md\n"
|
||||
"--- a/README.md\n+++ b/README.md\n@@ -1 +1 @@\n-1\n+2\n"
|
||||
)
|
||||
impact = protected_contracts_affected(REPO, diff)
|
||||
assert set(impact.paths) == {"ouroboros/gateway/contracts.py", "BIBLE.md", "README.md"}
|
||||
categories = {(p.path, p.category) for p in impact.protected}
|
||||
assert ("BIBLE.md", "safety-critical") in categories
|
||||
assert ("ouroboros/gateway/contracts.py", "frozen-contract") in categories
|
||||
assert not any(p.path == "README.md" for p in impact.protected)
|
||||
labels = {c.contract for c in impact.contracts}
|
||||
assert "gateway/contracts.py" in labels # the §11.1 row itself
|
||||
assert "ProviderTestRequest" in labels # a contract owned by the touched file
|
||||
|
||||
|
||||
def test_protected_path_list_form_and_release_invariants():
|
||||
impact = protected_contracts_affected(
|
||||
REPO, ["supervisor/git_ops_reset.py", "ouroboros/safety.py"],
|
||||
)
|
||||
categories = dict((p.path, p.category) for p in impact.protected)
|
||||
assert categories == {
|
||||
"supervisor/git_ops_reset.py": "release-invariant",
|
||||
"ouroboros/safety.py": "safety-critical",
|
||||
}
|
||||
|
||||
|
||||
def test_every_frozen_contract_row_is_reachable_from_its_owner_file():
|
||||
"""Completeness against the generated inventory: feeding each row's own
|
||||
owner file back into the query must name that row."""
|
||||
triples = _frozen_contract_rows(REPO)
|
||||
assert len({label for label, _f, _r in triples}) >= 20
|
||||
for label, file_part, role in triples:
|
||||
impact = protected_contracts_affected(REPO, [file_part])
|
||||
assert (label, role) in {(c.contract, c.role) for c in impact.contracts}, (
|
||||
label, file_part, role,
|
||||
)
|
||||
|
||||
|
||||
def test_protected_inventories_are_fully_categorized():
|
||||
from ouroboros.runtime_mode_policy import (
|
||||
FROZEN_CONTRACT_PATHS,
|
||||
RELEASE_INVARIANT_PATHS,
|
||||
SAFETY_CRITICAL_PATHS,
|
||||
)
|
||||
|
||||
every = sorted(SAFETY_CRITICAL_PATHS | FROZEN_CONTRACT_PATHS | RELEASE_INVARIANT_PATHS)
|
||||
impact = protected_contracts_affected(REPO, every)
|
||||
assert {p.path for p in impact.protected} == set(every)
|
||||
|
||||
|
||||
def test_paths_from_diff_accepts_lists_diffs_and_separated_strings():
|
||||
assert paths_from_diff("a.py, b.py c.py") == ("a.py", "b.py", "c.py")
|
||||
assert paths_from_diff(["./x/y.py", "x/y.py"]) == ("x/y.py",)
|
||||
with pytest.raises(ValueError, match="no changed paths"):
|
||||
protected_contracts_affected(REPO, " ")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The model-facing seam: query_code op=architecture
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _ctx(tmp_path):
|
||||
from ouroboros.tools.registry import ToolContext
|
||||
|
||||
return ToolContext(repo_dir=REPO, drive_root=tmp_path)
|
||||
|
||||
|
||||
def test_query_code_architecture_op_serves_the_five_facts(tmp_path):
|
||||
from ouroboros.tools.query_code import _query_code
|
||||
|
||||
out = _query_code(_ctx(tmp_path), op="architecture", query="owner_of ouroboros/llm.py")
|
||||
assert "D02" in out and "module_path" in out
|
||||
|
||||
out = _query_code(_ctx(tmp_path), op="architecture", query="domain_dependencies D02")
|
||||
assert "imports:" in out and "imported by:" in out
|
||||
|
||||
out = _query_code(
|
||||
_ctx(tmp_path), op="architecture",
|
||||
query="protected_contracts_affected BIBLE.md, ouroboros/gateway/contracts.py",
|
||||
)
|
||||
assert "safety-critical" in out and "frozen contract" in out
|
||||
|
||||
|
||||
def test_query_code_architecture_op_refuses_bad_facts_and_foreign_roots(tmp_path):
|
||||
from ouroboros.tools.query_code import _query_code
|
||||
|
||||
out = _query_code(_ctx(tmp_path), op="architecture", query="who_owns ouroboros/llm.py")
|
||||
assert "TOOL_ARG_ERROR" in out and "owner_of" in out # the refusal teaches the facts
|
||||
|
||||
out = _query_code(_ctx(tmp_path), op="architecture", query="owner_of")
|
||||
assert "TOOL_ARG_ERROR" in out and "argument" in out
|
||||
|
||||
out = _query_code(
|
||||
_ctx(tmp_path), op="architecture", query="owner_of ouroboros/llm.py",
|
||||
root="user_files", path="/tmp",
|
||||
)
|
||||
# A non-code root never serves architecture facts: either the binding layer
|
||||
# or the op's own root guard refuses, typed.
|
||||
assert ("TOOL_ARG_ERROR" in out or "TOOL_ACCESS_BLOCKED" in out)
|
||||
assert "D02" not in out
|
||||
|
||||
|
||||
def test_architecture_fact_vocabulary_is_closed():
|
||||
assert ARCHITECTURE_FACTS == (
|
||||
"owner_of",
|
||||
"domain_dependencies",
|
||||
"facade_consumers",
|
||||
"persistence_entities_written_by",
|
||||
"protected_contracts_affected",
|
||||
)
|
||||
with pytest.raises(ValueError, match="owner_of"):
|
||||
architecture_fact_rows(REPO, "unknown_fact x")
|
||||
assert FROZEN_INVENTORY_RELPATH.endswith("FROZEN_CONTRACTS_INVENTORY.md")
|
||||
Loading…
Add table
Add a link
Reference in a new issue