scripts.sync_architecture_to_graph
Reference › scripts › sync_architecture_to_graph
scripts/sync_architecture_to_graph.py syncs orchestrata-ultimate's own architecture, the ratified architecture ontology, into the knowledge graph. The architecture is declared as tables in the module (COMPONENTS, CONSTRAINTS, PATTERNS, plus the reference data WAF_PILLARS and CLOUD_SERVICES) and written with ArchitectureWriter; the ADR texts come from orchestrata_ultimate.knowledge_graph.adr_parser. A human runs it, and bootstrap_graphs invokes it with --repo-adrs. Every write is an idempotent MERGE. See orchestrata_ultimate.knowledge_graph.architecture_writer, check_adr_graph_sync (the read-only drift detector for the same ADRs) and the decisions page.
Options: --dry-run (print what would sync without connecting), --seed (also seed the reference-data catalogue first), --adrs [PATH] (also sync the knowledge-wiki ADR corpus, decisions.md, into ArchitectureDecision nodes with Artifact and DOCUMENTS edges; the default path is resolved at call time, and a missing file is a loud skip, never a failure) and --repo-adrs [DIR] (also sync the repository's one-ADR-per-file corpus under docs/adr, DECIDES-linked to the project architecture). sync_adrs and sync_repo_adrs do the two ADR syncs; is_pointer_entry and _is_docs_owned keep a wiki entry that points at a docs/adr record from being written twice; rebaseline_adr re-baselines one ADR's recorded body. A foreign register is refused up front with REFUSED: and exit 2. Body drift alone never fails a run; only errors do (_adr_exit_code).
At a glance
| Source | scripts/sync_architecture_to_graph.py |
| Lines of code | 1072 |
| Classes | 2 |
| Functions | 19 |
| Imports (internal) | orchestrata_ultimate.core.env_bootstrap, orchestrata_ultimate.knowledge_graph.adr_parser, orchestrata_ultimate.knowledge_graph.architecture_writer, orchestrata_ultimate.knowledge_graph.canonical_schema |
| Imported by | none |
| Tests that import it | none |
Class diagram
Dependency diagram
API
Constants
| Name | Type | Value |
|---|---|---|
COMPONENTS |
list[dict[str, Any]] |
[{'name': 'VizInt UI', 'protocol': 'HTTP', 'compute_platform': 'python -m http.… |
Classes
AdrSyncOutcome (dataclass)
Decorators: dataclass(frozen=True) · Line 116
One ADR sync_adrs() could not write cleanly.
| Attribute | Type | Default |
|---|---|---|
adr_id |
str |
|
state |
str |
|
detail |
str |
| Method | Signature | Summary |
|---|---|---|
to_dict |
to_dict() -> dict[str, str] |
AdrSyncReport (dataclass)
Decorators: dataclass(frozen=True) · Line 132
Outcome of one sync_adrs() run.
| Attribute | Type | Default |
|---|---|---|
synced |
int |
|
created |
int |
|
body_drift |
int |
|
errors |
int |
|
outcomes |
list[AdrSyncOutcome] |
field(default_factory=list) |
pointer_skipped |
int |
0 |
| Method | Signature | Summary |
|---|---|---|
summary_line |
summary_line() -> str |
|
to_dict |
to_dict() -> dict[str, Any] |
Functions
| Function | Signature | Summary |
|---|---|---|
_default_adrs_path |
_default_adrs_path() -> str |
Resolve the knowledge-wiki decisions.md path AT CALL TIME (never at import time, per project doctrine) from WIKI_ROOT -- falls back to ~/workspace/knowledge-wiki when unset. |
_adr_exit_code |
_adr_exit_code(report: AdrSyncReport) -> int |
Body drift alone never fails the run -- only errors does. |
_write_parsed_adrs |
_write_parsed_adrs(writer: ArchitectureWriter, parsed: list[ParsedAdr], arch_id: str) -> tuple[int, list[AdrSyncOutcome]] |
MERGE each parsed ADR and link it -[:DECIDES]-> arch_id. |
is_pointer_entry |
is_pointer_entry(adr: ParsedAdr) -> bool |
True when the wiki entry is a pointer stub to its docs/adr record. |
_is_docs_owned |
_is_docs_owned(adr: ParsedAdr, docs_dir: Path) -> bool |
A factory-register wiki entry whose docs/adr file exists, or that is a pointer stub, is owned by docs/adr (ADR-0096): the wiki pass never hashes or writes its body. |
sync_adrs |
sync_adrs(writer: ArchitectureWriter, adrs_path: Path, arch_id: str, adr_dir: Path | None = None) -> AdrSyncReport |
Graduate the ADR corpus at adrs_path into ArchitectureDecision nodes -[:DECIDES]-> arch_id, and record the decisions page itself plus the ontology design doc as Artifact nodes… |
_default_repo_adr_dir |
_default_repo_adr_dir() -> str |
The repo's docs/adr directory (the factory register). |
sync_repo_adrs |
sync_repo_adrs(writer: ArchitectureWriter, adr_dir: Path, arch_id: str) -> AdrSyncReport |
Graduate every adr_dir/NNNN-*.md into ArchitectureDecision nodes -[:DECIDES]-> arch_id (README.md excluded). |
_parse_authority |
_parse_authority(adr_dir: Path, authority: str) -> tuple[frozenset[str], str] | None |
Parse the authority ADR's own docs record in adr_dir (ADR-0102). |
rebaseline_adr |
rebaseline_adr(writer: ArchitectureWriter, adr_dir: Path, adr_label: str, authority: str, wiki_path: Path | None = None) -> str |
Re-baseline ONE ADR node to its record's body under authority. |
_rebaseline_from_wiki |
_rebaseline_from_wiki(writer: ArchitectureWriter, wiki_path: Path | None, adr_label: str, authority: str, *, named_ids: frozenset[str], authority_status: str | None) -> str |
Re-baseline a wiki-only id from its register entry (ADR-0102 rule 1). |
compute_first_capture_set |
compute_first_capture_set(writer: ArchitectureWriter, wiki_path: Path, adr_dir: Path) -> tuple[list[tuple[ParsedAdr, str, str | None]], list[tuple[str, str]]] |
The ADR-0098 first-capture set -- computed, never listed. |
rebaseline_first_capture_run |
rebaseline_first_capture_run(writer: ArchitectureWriter, wiki_path: Path, adr_dir: Path, authority: str) -> tuple[int, int] |
Baseline every computed first-capture id from docs, once, logged. |
_allowed_register_roots |
_allowed_register_roots() -> tuple[Path, Path] |
ADR-0102 rule 3's two register roots: this repository's root (from the script's own location) and the wiki's projects/orchestrata-ultimate/ directory (WIKI_ROOT read at call time, exactly as… |
_foreign_register_refusal |
_foreign_register_refusal(args: argparse.Namespace) -> str | None |
ADR-0102 rule 3: refuse a register that is not this repository's. |
_load_dotenv |
_load_dotenv() -> None |
Load the RUL-019 two-phase environment contract. |
sync |
sync(writer: ArchitectureWriter) -> dict[str, int] |
Run the full idempotent MERGE sequence through writer. |
_build_parser |
_build_parser() -> argparse.ArgumentParser |
|
main |
main(argv: list[str] | None = None) -> int |
Sync architecture metadata to the graph. |
Source
"""Sync orchestrata-ultimate's own architecture into the knowledge graph.
Thin CLI over ``ArchitectureWriter`` (``knowledge_graph/architecture_writer.py``)
-- the converged writer for the RATIFIED architecture ontology
(``plans/architecture-ontology-draft.md``). Re-expresses this project's own
self-documented architecture (5 infra components, 5 invariants, the
operational-protocol pattern catalog) in ontology terms:
* ``Architecture`` id ``"orchestrata-ultimate:orchestrata"`` (project-qualified,
ruling 2) -- superseding this script's old global ``Architecture{name}`` node.
* Each ``Component`` project-qualified (``"orchestrata-ultimate:{name}"``).
* The old writer's architecture "rules" become ``Constraint{kind:"invariant"}``
nodes linked ``Component -[:MUST_FOLLOW]-> Constraint -[:CONSTRAINS]->
Architecture`` -- the ``Rule`` label is retired here in favour of the
canonical Tier-A Prolog-rules-only meaning (ruling 3).
* Operational protocols (PID file, health check, ...) become ``Pattern``
nodes (``category="operational"``) linked ``Component -[:APPLIES]->
Pattern`` -- superseding the old ``USES_PATTERN`` synonym (§7).
This SUPERSEDES the script's pre-ontology node shapes (global Architecture/
Component/Rule/Pattern, HAS_RULE/USES_PATTERN edges) per §7 of the draft; the
old shapes are not preserved or migrated by this script (that migration is
``canonical_conformance.py``'s job, out of scope here).
Idempotent -- every write is a MERGE through ``ArchitectureWriter``.
``--adrs`` (wave ontology-phase2) additionally graduates the knowledge-wiki's
``decisions.md`` ADR corpus into first-class ``ArchitectureDecision`` nodes
(via ``adr_parser.parse_adrs`` + ``ArchitectureWriter.upsert_architecture_decision``),
links each ``-[:DECIDES]->`` this project's ``Architecture`` node, and records
the decisions page itself plus ``plans/architecture-ontology-draft.md`` as
``Artifact`` nodes ``-[:DOCUMENTS]->`` the same ``Architecture`` node. The
wiki checkout is optional infra (session-start hook machines may lack it) --
a missing ``decisions.md`` is a loud SKIP, never a failure.
``--repo-adrs`` (D5) graduates the OTHER register -- the one-ADR-per-file
factory register under ``docs/adr/NNNN-*.md`` -- into the same
``ArchitectureDecision`` nodes (``parse_adr_file`` + the identical writer
path), each ``-[:DECIDES]->`` the project ``Architecture`` which hangs off the
``Project`` hub via ``HAS_ARCHITECTURE`` (so the node is reachable from the
hub per ``check_kg_connectivity``, which walks relationships undirected).
This is the step ``scripts/bootstrap_graphs.py`` runs, so a full bootstrap
leaves one node per repo ADR. Ids are ``{project}:ADR-{padded_id}`` (ADR-0047:
the 4-digit factory ids never collide with the wiki's 1-3 digit project
register), MERGEd, so a re-run is a no-op. A malformed ADR file aborts the
run BEFORE any repo-ADR write (all files are parsed first).
An ADR that was Accepted in the graph and whose BODY later changed in the
document is NOT rewritten: the writer's immutability guard raises
``AdrImmutabilityError``, which is recorded as ``BODY_DRIFT`` (informational,
exit 0) and the body stays frozen -- evolve a decision via a new ADR +
``link_supersedes``. Title/status/date edits are not body edits and DO flow
through on every re-run.
Per-ADR ``AdrImmutabilityError`` (ruling 1's guard on
``upsert_architecture_decision``) is caught here, not left to abort the run:
the wiki's one-paragraph summary of an Accepted ADR is allowed to fall out of
sync with the graph's frozen body -- that body is owned by ``docs/adr``, not
the wiki page (ruling 1) -- so one drifted ADR must never stop every other
ADR in the corpus from syncing. Each drifted id is recorded as ``BODY_DRIFT``
in the returned :class:`AdrSyncReport` and the loop continues; the body
itself is never overwritten. See ``sync_adrs()`` for the full contract.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
# Ensure repo src is on path when run directly (mirrors kg_schema.py / run_ast_pipeline.py).
_repo_src = Path(__file__).resolve().parent.parent / "src"
if str(_repo_src) not in sys.path:
sys.path.insert(0, str(_repo_src))
from orchestrata_ultimate.core.env_bootstrap import load_env_contract # noqa: E402
from orchestrata_ultimate.knowledge_graph.adr_parser import ( # noqa: E402
ParsedAdr,
parse_adr_file,
parse_adrs,
)
from orchestrata_ultimate.knowledge_graph.architecture_writer import ( # noqa: E402
CLOUD_SERVICES,
WAF_PILLARS,
AdrImmutabilityError,
ArchitectureWriter,
_adr_body_sha,
)
from orchestrata_ultimate.knowledge_graph.canonical_schema import ( # noqa: E402
CANONICAL_LABELS,
LABEL_ARCHITECTURE,
LABEL_COMPONENT,
)
# ── ADR corpus (wave ontology-phase2) ───────────────────────────────────────
# The decisions.md page + this project's own ontology design doc, recorded
# as Artifact nodes -[:DOCUMENTS]-> Architecture (§9 of the draft).
ADRS_ARTIFACT_PATH = "projects/orchestrata-ultimate/decisions.md"
DESIGN_DOC_ARTIFACT_PATH = "plans/architecture-ontology-draft.md"
def _default_adrs_path() -> str:
"""Resolve the knowledge-wiki ``decisions.md`` path AT CALL TIME (never
at import time, per project doctrine) from ``WIKI_ROOT`` -- falls back to
``~/workspace/knowledge-wiki`` when unset."""
root = os.environ.get("WIKI_ROOT", str(Path.home() / "workspace/knowledge-wiki"))
return str(Path(root) / "projects" / "orchestrata-ultimate" / "decisions.md")
@dataclass(frozen=True)
class AdrSyncOutcome:
"""One ADR ``sync_adrs()`` could not write cleanly.
``state`` is ``"BODY_DRIFT"`` for the only case this loop produces today
(an Accepted node whose frozen body the wiki summary no longer matches).
"""
adr_id: str
state: str
detail: str
def to_dict(self) -> dict[str, str]:
return {"id": self.adr_id, "state": self.state, "detail": self.detail}
@dataclass(frozen=True)
class AdrSyncReport:
"""Outcome of one ``sync_adrs()`` run.
``body_drift`` counts ``ArchitectureDecision`` nodes left untouched
because ``upsert_architecture_decision`` raised ``AdrImmutabilityError``
-- informational, never a failure (ruling 1: the body of an Accepted ADR
is owned by ``docs/adr``, not the wiki summary). ``errors`` is reserved
for anything else that interrupts a single ADR's write without aborting
the whole run; ``sync_adrs()`` never populates it today because its
per-ADR ``try/except`` catches ONLY ``AdrImmutabilityError`` -- every
other exception still propagates and fails the run loudly, unchanged.
"""
synced: int
created: int
body_drift: int
errors: int
outcomes: list[AdrSyncOutcome] = field(default_factory=list)
pointer_skipped: int = 0
def summary_line(self) -> str:
line = (
f"synced {self.synced} · created {self.created} · "
f"body_drift {self.body_drift} · errors {self.errors}"
)
if self.pointer_skipped:
line += f" · pointer_skipped {self.pointer_skipped}"
return line
def to_dict(self) -> dict[str, Any]:
return {
"synced": self.synced,
"created": self.created,
"body_drift": self.body_drift,
"errors": self.errors,
"pointer_skipped": self.pointer_skipped,
"outcomes": [o.to_dict() for o in self.outcomes],
}
def _adr_exit_code(report: AdrSyncReport) -> int:
"""Body drift alone never fails the run -- only ``errors`` does."""
return 1 if report.errors else 0
def _write_parsed_adrs(
writer: ArchitectureWriter, parsed: list[ParsedAdr], arch_id: str
) -> tuple[int, list[AdrSyncOutcome]]:
"""MERGE each parsed ADR and link it ``-[:DECIDES]->`` ``arch_id``.
Catches ONLY ``AdrImmutabilityError`` per ADR (recorded as
``BODY_DRIFT``); anything else propagates and aborts the run.
"""
synced = 0
outcomes: list[AdrSyncOutcome] = []
for adr in parsed:
node_id = f"{PROJECT}:ADR-{adr.padded_id}"
try:
adr_id = writer.upsert_architecture_decision(
PROJECT,
adr.number,
adr.title,
padded_id=adr.padded_id,
register=adr.register,
status=adr.status or "Proposed",
date=adr.date,
context=adr.context,
decision=adr.decision,
alternatives=adr.alternatives,
consequences=adr.consequences,
)
except AdrImmutabilityError as exc:
outcomes.append(AdrSyncOutcome(node_id, "BODY_DRIFT", str(exc)))
continue
writer.link_decides(adr_id, LABEL_ARCHITECTURE, arch_id)
synced += 1
return synced, outcomes
#: A wiki entry that points at the full record: ``Full record: docs/adr/<file>``
#: (with or without backticks / a trailing period). The parser folds a
#: section to one paragraph, so the marker can sit mid-body (ADR-0073's entry
#: carries an execution note after it).
_POINTER_RE = re.compile(r"Full record: `?docs/adr/[^\s`]+\.md`?\.?")
def is_pointer_entry(adr: ParsedAdr) -> bool:
"""True when the wiki entry is a pointer stub to its ``docs/adr`` record."""
body = " ".join((adr.context, adr.decision, adr.alternatives, adr.consequences))
return _POINTER_RE.search(body) is not None
def _is_docs_owned(adr: ParsedAdr, docs_dir: Path) -> bool:
"""A factory-register wiki entry whose ``docs/adr`` file exists, or that is
a pointer stub, is owned by ``docs/adr`` (ADR-0096): the wiki pass never
hashes or writes its body. Project-register entries are the wiki's own."""
if adr.register != "factory":
return False
return is_pointer_entry(adr) or any(docs_dir.glob(f"{adr.padded_id}-*.md"))
def sync_adrs(
writer: ArchitectureWriter,
adrs_path: Path,
arch_id: str,
adr_dir: Path | None = None,
) -> AdrSyncReport:
"""Graduate the ADR corpus at ``adrs_path`` into ``ArchitectureDecision``
nodes -[:DECIDES]-> ``arch_id``, and record the decisions page itself
plus the ontology design doc as ``Artifact`` nodes -[:DOCUMENTS]-> the
same architecture.
Each ADR's ``upsert_architecture_decision`` call is wrapped in its own
``try/except`` catching ONLY ``AdrImmutabilityError`` -- the matched node
is Accepted and this call would change its frozen body. That id is
recorded as ``BODY_DRIFT`` in the returned report and the loop moves on
to the next ADR; the body is never overwritten. Any other exception
still propagates uncaught and aborts the run -- that is ruling 1's guard
working, unchanged.
Returns:
An :class:`AdrSyncReport` (``synced``/``created``/``body_drift``/
``errors`` counts + the per-ADR ``outcomes`` for anything not
cleanly synced).
"""
all_parsed = parse_adrs(adrs_path.read_text(encoding="utf-8"))
docs_dir = Path(adr_dir) if adr_dir is not None else Path(_default_repo_adr_dir())
parsed = [a for a in all_parsed if not _is_docs_owned(a, docs_dir)]
pointer_skipped = len(all_parsed) - len(parsed)
synced, outcomes = _write_parsed_adrs(writer, parsed, arch_id)
decisions_artifact_id = writer.upsert_artifact(
PROJECT, "decisions.md", artifact_type="adr", path=ADRS_ARTIFACT_PATH
)
writer.link_documents(decisions_artifact_id, arch_id)
design_doc_artifact_id = writer.upsert_artifact(
PROJECT,
"architecture-ontology-draft.md",
artifact_type="design-doc",
path=DESIGN_DOC_ARTIFACT_PATH,
)
writer.link_documents(design_doc_artifact_id, arch_id)
body_drift = sum(1 for o in outcomes if o.state == "BODY_DRIFT")
errors = sum(1 for o in outcomes if o.state == "ERROR")
return AdrSyncReport(
synced=synced,
created=2,
body_drift=body_drift,
errors=errors,
outcomes=outcomes,
pointer_skipped=pointer_skipped,
)
def _default_repo_adr_dir() -> str:
"""The repo's ``docs/adr`` directory (the factory register)."""
return str(Path(__file__).resolve().parent.parent / "docs" / "adr")
def sync_repo_adrs(
writer: ArchitectureWriter, adr_dir: Path, arch_id: str
) -> AdrSyncReport:
"""Graduate every ``adr_dir/NNNN-*.md`` into ``ArchitectureDecision``
nodes -[:DECIDES]-> ``arch_id`` (``README.md`` excluded).
All files are parsed BEFORE the first write, so a malformed ADR raises
``ValueError`` (from ``parse_adr_file``) without a partial sync. Writes go
through the same per-ADR path as :func:`sync_adrs`, so the immutability
guard behaves identically (``BODY_DRIFT``, body never overwritten).
``created`` is 0: this register records no Artifact nodes.
"""
paths = sorted(p for p in adr_dir.glob("*.md") if p.name.lower() != "readme.md")
parsed = [parse_adr_file(p.read_text(encoding="utf-8")) for p in paths]
synced, outcomes = _write_parsed_adrs(writer, parsed, arch_id)
return AdrSyncReport(
synced=synced,
created=0,
body_drift=sum(1 for o in outcomes if o.state == "BODY_DRIFT"),
errors=sum(1 for o in outcomes if o.state == "ERROR"),
outcomes=outcomes,
)
#: Matches a literal ``ADR-NNN``/``ADR-NNNN`` token in an authority ADR's
#: Decision section (ADR-0102's naming mechanism).
_AUTHORITY_ID_RE = re.compile(r"ADR-(\d{1,4})")
def _parse_authority(
adr_dir: Path, authority: str
) -> tuple[frozenset[str], str] | None:
"""Parse the authority ADR's own docs record in ``adr_dir`` (ADR-0102).
Returns ``(named_ids, status)`` -- the padded ``ADR-NNNN`` ids the
authority's Decision section names, and the record's normalised status --
or ``None`` when the authority has no parseable docs file there (the
writer's hardcoded authority map is the fallback for those, ADR-0096's
original path).
"""
padded = authority.removeprefix("ADR-")
matches = sorted(adr_dir.glob(f"{padded}-*.md"))
if len(matches) != 1:
return None
try:
record = parse_adr_file(matches[0].read_text(encoding="utf-8"))
except ValueError:
return None
named = frozenset(
f"ADR-{int(digits):04d}" for digits in _AUTHORITY_ID_RE.findall(record.decision)
)
return named, record.status
def rebaseline_adr(
writer: ArchitectureWriter,
adr_dir: Path,
adr_label: str,
authority: str,
wiki_path: Path | None = None,
) -> str:
"""Re-baseline ONE ADR node to its record's body under ``authority``.
``adr_label`` is the exact ``ADR-NNNN`` id. When the authority ADR's own
docs record parses (ADR-0102's generic mechanism), the ids its Decision
section names govern: a named id re-baselines from its ``docs/adr``
record at WHATEVER status that record carries (a Proposed record stays
Proposed), or -- when the id has no docs record -- from its wiki register
entry (``wiki_path``: the ``## ADR-NNNN:`` heading, its ``**Status:**``
line and folded body). An unnamed id is refused with ``ValueError``
(``"<authority> does not name <id>"``); the writer independently
re-checks both the naming and the authority's Accepted status. When the
authority's docs record cannot be parsed, the legacy path is unchanged:
exactly one docs file, an Accepted record, and the writer's hardcoded
authority map (ADR-0096). Logs the act; returns the node id.
"""
named_ids: frozenset[str] | None = None
authority_status: str | None = None
parsed_authority = _parse_authority(adr_dir, authority)
if parsed_authority is not None:
named_ids, authority_status = parsed_authority
if adr_label not in named_ids:
raise ValueError(f"{authority} does not name {adr_label}")
padded = adr_label.removeprefix("ADR-")
matches = sorted(adr_dir.glob(f"{padded}-*.md"))
if not matches and named_ids is not None:
return _rebaseline_from_wiki(
writer,
wiki_path,
adr_label,
authority,
named_ids=named_ids,
authority_status=authority_status,
)
if len(matches) != 1:
raise ValueError(f"expected exactly one docs/adr file for {adr_label}")
adr = parse_adr_file(matches[0].read_text(encoding="utf-8"))
if named_ids is None and adr.status != "Accepted":
raise ValueError(f"{adr_label} is {adr.status!r} in docs, not Accepted")
before = writer.read_architecture_decision_hashes(PROJECT).get(adr.padded_id, "")
node_id = writer.rebaseline_architecture_decision(
PROJECT,
adr.padded_id,
authority=authority,
title=adr.title,
status=adr.status,
date=adr.date,
context=adr.context,
decision=adr.decision,
alternatives=adr.alternatives,
consequences=adr.consequences,
named_ids=named_ids,
authority_status=authority_status,
)
after = _adr_body_sha(adr.context, adr.decision, adr.alternatives, adr.consequences)
print(f"REBASELINED: {node_id} {before[:10]} -> {after[:10]} under {authority}")
return node_id
def _rebaseline_from_wiki(
writer: ArchitectureWriter,
wiki_path: Path | None,
adr_label: str,
authority: str,
*,
named_ids: frozenset[str],
authority_status: str | None,
) -> str:
"""Re-baseline a wiki-only id from its register entry (ADR-0102 rule 1).
The id has no ``docs/adr`` record; its ``## ADR-NNNN:`` heading's
``**Status:**`` line and folded body ARE the record. The writer still
enforces the authority (Accepted, names the id) and node existence.
"""
padded = adr_label.removeprefix("ADR-")
if wiki_path is None or not wiki_path.exists():
raise ValueError(
f"{adr_label} has no docs/adr record and no wiki register to "
f"re-baseline from (wiki path: {wiki_path})"
)
entries = {
entry.padded_id: entry
for entry in parse_adrs(wiki_path.read_text(encoding="utf-8"))
}
entry = entries.get(padded)
if entry is None:
raise ValueError(f"{adr_label} has no entry in the wiki register {wiki_path}")
before = writer.read_architecture_decision_hashes(PROJECT).get(entry.padded_id, "")
node_id = writer.rebaseline_architecture_decision(
PROJECT,
entry.padded_id,
authority=authority,
title=entry.title,
status=entry.status,
date=entry.date,
context=entry.context,
decision=entry.decision,
alternatives=entry.alternatives,
consequences=entry.consequences,
named_ids=named_ids,
authority_status=authority_status,
)
after = _adr_body_sha(
entry.context, entry.decision, entry.alternatives, entry.consequences
)
print(f"REBASELINED: {node_id} {before[:10]} -> {after[:10]} under {authority}")
return node_id
def compute_first_capture_set(
writer: ArchitectureWriter, wiki_path: Path, adr_dir: Path
) -> tuple[list[tuple[ParsedAdr, str, str | None]], list[tuple[str, str]]]:
"""The ADR-0098 first-capture set -- computed, never listed.
An id is in the set when a docs file exists for it, the graph node exists
with a body that differs from docs, and that graph body hash is the
empty-body hash or the hash of the wiki entry's parsed body (it was never
captured from docs). A differing body that is neither is real drift and is
returned as ``(padded_id, reason)`` in the refused list, never touched.
Returns:
``(eligible, refused)``; each eligible item is ``(docs_adr,
graph_sha, wiki_sha_or_None)``. Read-only.
"""
graph = writer.read_architecture_decision_hashes(PROJECT)
wiki_shas: dict[str, str] = {}
if wiki_path.exists():
for entry in parse_adrs(wiki_path.read_text(encoding="utf-8")):
if entry.register == "factory":
wiki_shas[entry.padded_id] = _adr_body_sha(
entry.context,
entry.decision,
entry.alternatives,
entry.consequences,
)
empty_sha = _adr_body_sha("", "", "", "")
eligible: list[tuple[ParsedAdr, str, str | None]] = []
refused: list[tuple[str, str]] = []
for path in sorted(
p for p in adr_dir.glob("*.md") if p.name.lower() != "readme.md"
):
adr = parse_adr_file(path.read_text(encoding="utf-8"))
graph_sha = graph.get(adr.padded_id)
docs_sha = _adr_body_sha(
adr.context, adr.decision, adr.alternatives, adr.consequences
)
if graph_sha is None or graph_sha == docs_sha:
continue
wiki_sha = wiki_shas.get(adr.padded_id)
if graph_sha == empty_sha or graph_sha == wiki_sha:
eligible.append((adr, graph_sha, wiki_sha))
else:
refused.append(
(adr.padded_id, f"graph body {graph_sha[:10]} is not empty or wiki")
)
return eligible, refused
def rebaseline_first_capture_run(
writer: ArchitectureWriter, wiki_path: Path, adr_dir: Path, authority: str
) -> tuple[int, int]:
"""Baseline every computed first-capture id from docs, once, logged.
Prints one ``REBASELINED:`` line per id (before/after hashes) and one
``REFUSED:`` line per differing id that is not a first capture. Returns
``(rebaselined, refused)``.
"""
if authority != "ADR-0098":
raise ValueError(f"{authority!r} does not authorise a first-capture run")
eligible, refused = compute_first_capture_set(writer, wiki_path, adr_dir)
for adr, _graph_sha, wiki_sha in eligible:
before, after = writer.rebaseline_first_capture(
PROJECT,
adr.padded_id,
authority=authority,
wiki_body_sha=wiki_sha,
title=adr.title,
status=adr.status or "Proposed",
date=adr.date,
context=adr.context,
decision=adr.decision,
alternatives=adr.alternatives,
consequences=adr.consequences,
)
print(
f"REBASELINED: {PROJECT}:ADR-{adr.padded_id} {before[:10]} -> "
f"{after[:10]} under {authority}"
)
for padded_id, reason in refused:
print(f"REFUSED: {PROJECT}:ADR-{padded_id}: {reason}")
return len(eligible), len(refused)
def _allowed_register_roots() -> tuple[Path, Path]:
"""ADR-0102 rule 3's two register roots: this repository's root (from
the script's own location) and the wiki's
``projects/orchestrata-ultimate/`` directory (``WIKI_ROOT`` read at call
time, exactly as ``_default_adrs_path`` reads it). Both sides are
symlink-resolved by the caller before comparison."""
repo_root = Path(__file__).resolve().parents[1]
wiki_root = (
Path(os.environ.get("WIKI_ROOT", str(Path.home() / "workspace/knowledge-wiki")))
/ "projects"
/ "orchestrata-ultimate"
)
return repo_root, wiki_root.resolve()
def _foreign_register_refusal(args: argparse.Namespace) -> str | None:
"""ADR-0102 rule 3: refuse a register that is not this repository's.
``--adrs`` and ``--repo-adrs`` must each resolve under one of the two
allowed roots until a project key makes a foreign path meaningful.
Returns the refusal message, or ``None`` when every given path is
admitted. Runs before any graph write, in every mode.
"""
roots = _allowed_register_roots()
for given in (args.adrs, args.repo_adrs):
if given is None:
continue
resolved = Path(given).resolve()
if not any(resolved.is_relative_to(root) for root in roots):
return (
f"foreign register {given}; ADR-0102 rule 3: no sync of "
"another project's ADRs until a project key exists"
)
return None
def _load_dotenv() -> None:
"""Load the RUL-019 two-phase environment contract.
Delegates to ``core.env_bootstrap.load_env_contract`` — THE canonical
loader — instead of parsing ``.env`` here. The hand-rolled copy this
replaced (byte-identical in four scripts) stopped after phase 1, and
``.env`` is the prod-BASE layer: it carries ``CXP_GRPC_HOST=localhost``
for the host user-units, so a script that loads it and stops resolves
PRODUCTION endpoints while believing it is on the test fleet. The
canonical loader then applies the ``ORCHESTRATA_ENV_PROFILE`` overlay,
which is the half that points the process at the right infrastructure,
and honours ``declare_owned`` so it cannot reset a test worker's
per-slot isolation.
"""
load_env_contract()
# ── Project + Architecture ──────────────────────────────────────────────────
PROJECT = "orchestrata-ultimate"
ARCHITECTURE_NAME = "orchestrata"
ARCHITECTURE = {
"version": "0.7.0",
"description": "Multi-agent software engineering system with browser UI (VizInt), WebSocket gateway, gRPC kernel, Redis queues, and knowledge graph.",
"status": "active",
}
# ── Components ───────────────────────────────────────────────────────────
# Must match the canonical PATTERNS names exactly (below), or the
# link_applies() call silently binds nothing (MATCH on an unknown Pattern.name).
COMPONENTS: list[dict[str, Any]] = [
{
"name": "VizInt UI",
"protocol": "HTTP",
"compute_platform": "python -m http.server (port 5000)",
"required": True,
"responsibility": "Browser-based command center with 31 gadgets. Served independently by python -m http.server. WebSocket bridge connects to Gateway.",
"invariants": [
"Gateway never serves HTML/JS/CSS",
"VizInt is independent of Gateway",
],
"protocols": [],
},
{
"name": "Gateway",
"protocol": "WebSocket (JSON-RPC 2.0)",
"compute_platform": "asyncio process (port 8765)",
"required": True,
"responsibility": "WebSocket JSON-RPC server. Authenticates clients, dispatches RPCs, manages pub/sub, broadcasts pipeline events. Connects to the out-of-process kernel via the cxpt direct transport (grpcio retired 2026-08-07).",
"invariants": [
"Gateway never serves HTML/JS/CSS",
"kernel is out-of-process (cxpt direct transport)",
"Never use except: pass",
"Never hardcode service IPs",
],
"protocols": [
"PID file",
"Health check",
"Readiness probe",
"Graceful shutdown",
"Idempotent start",
"Foreign process detection",
],
},
{
"name": "Kernel",
"protocol": "cxpt direct transport (port 50053)",
"compute_platform": "asyncio process",
"required": True,
"responsibility": "Out-of-process agent runtime. Boots full PipelineRunner with all 9 volks. Gateway connects via the cxpt direct transport (grpcio retired 2026-08-07); falls back to in-process PipelineKernelClient when unreachable.",
"invariants": [
"kernel is out-of-process (cxpt direct transport)",
"Never hardcode service IPs",
],
"protocols": [],
},
{
"name": "Redis",
"protocol": "TCP (port 6379)",
"compute_platform": "Incus container (redis-queue)",
"required": True,
"responsibility": "Message broker for CXP packets, session state cache, pipeline event stream (gw:ringbuf:pipeline.events). Runs in Incus container.",
"invariants": ["Never hardcode service IPs"],
"protocols": [],
},
{
"name": "orchgraph",
"protocol": "Bolt (port 7687)",
"compute_platform": "Incus HA cluster (orchgraph — Memgraph 3.11 fork: coordinators + main/replica)",
"required": True,
"responsibility": "Operational knowledge graph (orchkg database): code structure, specs/requirements, sessions, audit trail, architecture ontology. The Memgraph semantic twin lives in the 'memgraph' database on the same cluster. Neo4j was retired 2026-07-27; the neo4j:// scheme and the Python neo4j driver are Bolt-client naming only.",
"invariants": ["Never hardcode service IPs"],
"protocols": [],
},
]
# ── Constraints (formerly "Rules" -- ruling 3: Rule is Prolog-only now) ─────
CONSTRAINTS = [
{
"name": "Gateway never serves HTML/JS/CSS",
"severity": "CRITICAL",
"description": "The gateway is a WebSocket JSON-RPC server. Adding process_request for static files is an architectural violation. VizInt UI served independently on port 5000.",
},
{
"name": "VizInt is independent of Gateway",
"severity": "CRITICAL",
"description": "Browser loads UI from port 5000. WebSocketBridge connects to gateway at ws://localhost:8765. E2E tests use localStorage.setItem('vizint.gateway.url', ...) for dynamic port configuration.",
},
{
"name": "Never use except: pass",
"severity": "CRITICAL",
"description": "Every exception handler must log the actual error at the appropriate level (INFO/WARNING/ERROR). Silent failures are the hardest bugs.",
},
{
"name": "kernel is out-of-process (cxpt direct transport)",
"severity": "HIGH",
"description": "Gateway connects to kernel via the cxpt direct transport (grpcio retired 2026-08-07) on port 50053. Three-tier logging: unconfigured (INFO), unreachable (WARNING with host:port+error), connected (INFO). Falls back to PipelineKernelClient only when unreachable.",
},
{
"name": "Never hardcode service IPs",
"severity": "CRITICAL",
"description": "All infrastructure addresses come from .env. Both start_test_services.py and gateway CLI auto-load .env via _load_dotenv(). Never assume localhost for Redis/graph.",
},
{
"name": "Never dismiss a test failure",
"severity": "CRITICAL",
"description": "Every test failure must be traced to root cause. 'Pre-existing', 'flaky', 'race condition' are not valid dismissals. Fix it or report you cannot.",
},
]
# ── Patterns (operational protocols; category="operational" per §7) ────────
PATTERNS = [
{
"name": "PID file",
"description": "Write PID on start, validate liveness with os.kill(pid, 0), remove on graceful stop.",
},
{
"name": "Health check",
"description": "Application-level RPC (e.g. gateway.ping), not just port-open. Port can be open while process is hung.",
},
{
"name": "Readiness probe",
"description": "Wait for health check response, not just TCP accept. Service may accept TCP but still be loading dependencies.",
},
{
"name": "Graceful shutdown",
"description": "SIGTERM -> drain connections (10s) -> SIGKILL as last resort -> cleanup PID file.",
},
{
"name": "Idempotent start",
"description": "Check PID + port + health before spawning. Safe to run start_test_services.py N times.",
},
{
"name": "Foreign process detection",
"description": "If port is in use but no matching PID file, refuse to start. Prevents hijacking another process's port.",
},
{
"name": "Stale PID cleanup",
"description": "If PID file exists but process is dead, auto-remove it so restart is not blocked.",
},
{
"name": "Three-tier gRPC logging",
"description": "Unconfigured (no env vars) -> INFO. Unreachable (server down) -> WARNING with host:port + error. Connected -> INFO.",
},
{
"name": "Exponential backoff",
"description": "Reply consumer uses 2s->4s->8s->...->60s max backoff. Logs actual error on first failure only. Stops after 10 consecutive failures.",
},
]
def sync(writer: ArchitectureWriter) -> dict[str, int]:
"""Run the full idempotent MERGE sequence through ``writer``.
Order matters: Constraints and Patterns are created BEFORE Components,
same reasoning the pre-ontology writer documented -- a MATCH on a
not-yet-created node yields no rows and silently drops the edge, so on a
fresh graph the component->constraint/pattern links would never bind.
Returns:
A summary count dict (architecture/components/constraints/patterns).
"""
arch_id = writer.upsert_architecture(
PROJECT,
ARCHITECTURE_NAME,
version=ARCHITECTURE["version"],
description=ARCHITECTURE["description"],
status=ARCHITECTURE["status"],
)
constraint_ids: dict[str, str] = {}
for con in CONSTRAINTS:
cid = writer.upsert_constraint(
PROJECT,
con["name"],
kind="invariant",
severity=con["severity"],
description=con["description"],
)
writer.link_constrains(cid, arch_id)
constraint_ids[con["name"]] = cid
for pat in PATTERNS:
writer.upsert_pattern(
pat["name"], category="operational", description=pat["description"]
)
for comp in COMPONENTS:
comp_id = writer.upsert_component(
PROJECT,
comp["name"],
responsibility=comp["responsibility"],
compute_platform=comp["compute_platform"],
protocol=comp["protocol"],
required=comp["required"],
)
writer.link_has_component(arch_id, comp_id)
for invariant_name in comp.get("invariants", []):
linked_cid = constraint_ids.get(invariant_name)
if linked_cid is not None:
writer.link_must_follow(comp_id, linked_cid)
for pat_name in comp.get("protocols", []):
writer.link_applies(LABEL_COMPONENT, comp_id, pat_name)
return {
"architecture": 1,
"components": len(COMPONENTS),
"constraints": len(CONSTRAINTS),
"patterns": len(PATTERNS),
}
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Sync orchestrata-ultimate's own architecture into the "
"knowledge graph (RATIFIED architecture ontology)."
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print what would be synced without connecting to the graph at all.",
)
parser.add_argument(
"--seed",
action="store_true",
help="Also seed the reference-data catalog (WAFPillar + CloudService "
"nodes, via ArchitectureWriter.seed_reference_data()) before the sync.",
)
parser.add_argument(
"--adrs",
nargs="?",
const=_default_adrs_path(),
default=None,
help="Also sync the knowledge-wiki ADR corpus (decisions.md) into "
"ArchitectureDecision nodes + Artifact/DOCUMENTS edges. Optional PATH "
"overrides the default ($WIKI_ROOT/projects/orchestrata-ultimate/"
"decisions.md, resolved at call time). A missing file is a loud SKIP, "
"never a failure.",
)
parser.add_argument(
"--repo-adrs",
nargs="?",
const=_default_repo_adr_dir(),
default=None,
metavar="DIR",
help="Also sync the repo's one-ADR-per-file corpus (docs/adr/NNNN-*.md) "
"into ArchitectureDecision nodes DECIDES-linked to the project "
"Architecture. Optional DIR overrides docs/adr. A missing directory is "
"a loud SKIP, never a failure. scripts/bootstrap_graphs.py passes this.",
)
parser.add_argument(
"--report-json",
default=None,
metavar="PATH",
help="With --adrs and/or --repo-adrs, write the per-ADR sync report "
"(the synced/created/body_drift/errors counts plus every BODY_DRIFT row) "
"as JSON to PATH. With both, the repo report is nested under "
'"repo_adrs" in the --adrs report.',
)
parser.add_argument(
"--rebaseline-id",
default=None,
metavar="ADR-NNNN",
help="Re-baseline ONE ADR node's body to its docs/adr record (ADR-0096). "
"Requires --authority; refuses any id the authority does not name.",
)
parser.add_argument(
"--rebaseline-first-capture",
action="store_true",
help="ADR-0098: baseline once from docs every id whose graph body is "
"empty or the wiki entry's body (computed set; refuses real drift). "
"Requires --authority ADR-0098; uses --adrs / --repo-adrs paths.",
)
parser.add_argument(
"--authority",
default=None,
metavar="ADR-NNNN",
help="The Accepted ruling ADR that authorises --rebaseline-id.",
)
parser.add_argument(
"--retire-legacy",
action="append",
default=None,
metavar="LABEL:id",
help="DETACH DELETE the node with the given label and canonical id "
"(project-qualified: orchestrata-ultimate:...). Refuses ids that do "
"not start with 'orchestrata-ultimate:'. Refuses labels that are not "
"canonical schema labels. Prints the target and the pre-delete property "
"snapshot. Exits non-zero if the node does not exist. This is the ONLY "
"delete path; default runs NEVER delete anything. Coordinator-run, "
"one-shot, per authorized correction.",
)
return parser
def main(argv: list[str] | None = None) -> int:
"""Sync architecture metadata to the graph. Idempotent via MERGE."""
args = _build_parser().parse_args(argv)
_load_dotenv()
refusal = _foreign_register_refusal(args)
if refusal is not None:
print(f"REFUSED: {refusal}")
return 2
if args.dry_run:
if args.seed:
print(
f"Would seed reference data: {len(WAF_PILLARS)} WAFPillar(s), "
f"{len(CLOUD_SERVICES)} CloudService(s)."
)
print(
f"Would sync Architecture '{PROJECT}:{ARCHITECTURE_NAME}': "
f"{len(COMPONENTS)} component(s), {len(CONSTRAINTS)} constraint(s), "
f"{len(PATTERNS)} pattern(s)."
)
for comp in COMPONENTS:
print(f" - Component: {comp['name']}")
for con in CONSTRAINTS:
print(f" - Constraint (invariant): {con['name']}")
for pat in PATTERNS:
print(f" - Pattern (operational): {pat['name']}")
if args.adrs is not None:
adrs_path = Path(args.adrs)
if not adrs_path.exists():
print(f"SKIP adrs: {adrs_path} does not exist")
else:
parsed = parse_adrs(adrs_path.read_text(encoding="utf-8"))
print(f"Would sync {len(parsed)} ADRs + 2 artifacts")
if args.repo_adrs is not None:
repo_dir = Path(args.repo_adrs)
if not repo_dir.is_dir():
print(f"SKIP repo-adrs: {repo_dir} does not exist")
else:
n = sum(
1 for p in repo_dir.glob("*.md") if p.name.lower() != "readme.md"
)
print(f"Would sync {n} repo ADRs from {repo_dir}")
return 0
if args.rebaseline_id or args.authority or args.rebaseline_first_capture:
if bool(args.rebaseline_id) == bool(args.rebaseline_first_capture) or not (
args.authority
):
print(
"give --authority with exactly one of --rebaseline-id or "
"--rebaseline-first-capture"
)
return 2
writer = ArchitectureWriter()
try:
adr_dir = Path(args.repo_adrs or _default_repo_adr_dir())
if args.rebaseline_first_capture:
done, refused = rebaseline_first_capture_run(
writer,
Path(args.adrs or _default_adrs_path()),
adr_dir,
args.authority,
)
print(f"first-capture: rebaselined {done} · refused {refused}")
else:
rebaseline_adr(
writer,
adr_dir,
args.rebaseline_id,
args.authority,
Path(args.adrs or _default_adrs_path()),
)
except ValueError as exc:
print(f"REFUSED: {exc}")
return 2
finally:
writer.close()
return 0
# ── --retire-legacy ─────────────────────────────────────────────────────
if args.retire_legacy:
writer = ArchitectureWriter()
try:
for item in args.retire_legacy:
if ":" not in item:
print(
f"REFUSED: --retire-legacy expects LABEL:id format, got {item!r}"
)
return 2
label, node_id = item.split(":", 1)
if label not in CANONICAL_LABELS:
print(
f"REFUSED: --retire-legacy label must be a canonical schema label, got {label!r}"
)
return 2
if not node_id.startswith("orchestrata-ultimate:"):
print(
f"REFUSED: --retire-legacy ids must start with 'orchestrata-ultimate:', got {node_id!r}"
)
return 2
# Read the node first to print its snapshot
props = writer._read(
f"MATCH (n:{label} {{id: $id}}) RETURN properties(n) AS props",
{"id": node_id},
)
if not props:
print(f"REFUSED: node {item} does not exist")
return 2
snapshot = props[0]["props"]
print(f"RETIRE LEGACY: {item}")
print(
f" Pre-delete snapshot: {json.dumps(snapshot, indent=2, default=str)}"
)
writer._write(
f"MATCH (n:{label} {{id: $id}}) DETACH DELETE n",
{"id": node_id},
)
print(f" ✅ Deleted {item}")
finally:
writer.close()
return 0
writer = ArchitectureWriter()
exit_code = 0
try:
if args.seed:
seed_summary = writer.seed_reference_data(PROJECT)
print(f"✅ Reference data seeded to knowledge graph: {seed_summary}")
summary = sync(writer)
wiki_report: AdrSyncReport | None = None
if args.adrs is not None:
adrs_path = Path(args.adrs)
if not adrs_path.exists():
print(f"SKIP adrs: {adrs_path} does not exist")
else:
arch_id = f"{PROJECT}:{ARCHITECTURE_NAME}"
adr_report = sync_adrs(writer, adrs_path, arch_id)
summary.update(
{"adrs": adr_report.synced, "artifacts": adr_report.created}
)
print(adr_report.summary_line())
for outcome in adr_report.outcomes:
print(f" {outcome.state}: {outcome.adr_id}: {outcome.detail}")
wiki_report = adr_report
exit_code = _adr_exit_code(adr_report)
repo_report: AdrSyncReport | None = None
if args.repo_adrs is not None:
repo_dir = Path(args.repo_adrs)
if not repo_dir.is_dir():
print(f"SKIP repo-adrs: {repo_dir} does not exist")
else:
arch_id = f"{PROJECT}:{ARCHITECTURE_NAME}"
repo_report = sync_repo_adrs(writer, repo_dir, arch_id)
summary["repo_adrs"] = repo_report.synced
print("repo-adrs " + repo_report.summary_line())
for outcome in repo_report.outcomes:
print(f" {outcome.state}: {outcome.adr_id}: {outcome.detail}")
exit_code = max(exit_code, _adr_exit_code(repo_report))
primary_report = wiki_report or repo_report
if args.report_json and primary_report is not None:
payload = primary_report.to_dict()
if wiki_report and repo_report:
payload["repo_adrs"] = repo_report.to_dict()
Path(args.report_json).write_text(
json.dumps(payload, indent=2) + "\n", encoding="utf-8"
)
print(f"✅ Architecture nodes synced to knowledge graph: {summary}")
finally:
writer.close()
return exit_code
if __name__ == "__main__":
raise SystemExit(main())