orchestrata_ultimate.knowledge_graph.architecture_writer
Reference › orchestrata_ultimate › knowledge_graph › architecture_writer
ArchitectureWriter is the single, converged writer for the architecture ontology (Tier J, ratified in the ontology phase 1 wave). It supersedes the ad-hoc global-name writes of the sync_architecture_to_graph script and writes to the operational graph (orchkg on orchgraph) through a synchronous graph repository, using the label and relationship constants of canonical_schema. Scripts such as check_adr_graph_sync and kg_schema import it; the architecture narrative is in the architecture ontology page.
Project-scoped labels merge on an id of the form project, colon, natural id; the genuinely global reference labels (WAFPillar, CloudService, and the global Pattern) merge on their bare natural id. The private helpers _merge_node and _edge implement every write as a MERGE, so repeated runs are idempotent. The public surface is a family of upsert_* methods, one per node kind: architecture, component, API contract, data entity, quality attribute, constraint, capacity model, cost model and line item, environment, deployment, trade-off, risk, failure mode, technology choice, SLO, alert, pattern, artifact (limited to the types in ARTIFACT_TYPES), WAF pillar and cloud service. A matching set of link_* methods creates the edges, for example link_has_component, link_exposes, link_data_flows_to, link_depends_on, link_persists_to, link_runs_on and link_satisfies. seed_reference_data idempotently loads the WAF_PILLARS and CLOUD_SERVICES tables and returns counts.
ADR immutability is enforced here. upsert_architecture_decision computes a body hash over the four body fields in ADR_BODY_FIELDS and, once the stored decision has status Accepted, refuses to change them, raising AdrImmutabilityError (REQ-106-006). Evolution is a new ADR joined by link_supersedes; set_architecture_decision_status may always change status. rebaseline_architecture_decision and rebaseline_first_capture are the audited exceptions, gated by a table of authorising ADRs. Records arrive from adr_parser.
At a glance
| Source | src/orchestrata_ultimate/knowledge_graph/architecture_writer.py |
| Lines of code | 1853 |
| Classes | 2 |
| Functions | 6 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.core.graph, orchestrata_ultimate.knowledge_graph, orchestrata_ultimate.knowledge_graph.canonical_schema |
| Imported by | scripts.check_adr_graph_sync, scripts.kg_schema, scripts.seed_component_dag, scripts.sync_architecture_to_graph |
| Tests that import it | tests/knowledge_graph/test_architecture_writer.py, tests/knowledge_graph/test_architecture_writer_status_line.py, tests/scripts/test_check_adr_graph_sync.py, tests/scripts/test_check_adr_graph_sync_bodies.py, tests/scripts/test_seed_component_dag.py, tests/scripts/test_sync_adrs_pointer_entries.py, tests/scripts/test_sync_architecture_to_graph.py, tests/scripts/test_sync_first_capture.py, tests/scripts/test_sync_rebaseline_log_line.py, tests/scripts/test_sync_rebaseline_under_authority.py |
Class diagram
Dependency diagram
API
Constants
| Name | Type | Value |
|---|---|---|
_REBASELINE_AUTHORITIES |
dict[str, frozenset[str]] |
{'ADR-0096': frozenset({'ADR-0073'})} |
ADR_BODY_FIELDS |
tuple[str, ...] |
('context', 'decision', 'alternatives', 'consequences') |
ARTIFACT_TYPES |
frozenset[str] |
frozenset({'design-doc', 'adr', 'iac-module', 'helm-chart', 'image', 'diagram'}) |
WAF_PILLARS |
tuple[dict[str, str], ...] |
({'name': 'Reliability', 'concern': 'Resiliency, availability, recovery'}, {'na… |
CLOUD_SERVICES |
tuple[dict[str, str], ...] |
({'name': 'Azure Service Bus', 'vendor': 'azure', 'neutral_kind': 'message-brok… |
Classes
AdrImmutabilityError
Bases: Exception · Decorators: traces(spec='106', reqs=['REQ-106-006']) · Line 60
Raised when a write would mutate an Accepted ArchitectureDecision's body.
ArchitectureWriter
Line 390
The converged writer over the RATIFIED architecture ontology (Tier J).
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(repo: SyncGraphRepository | None = None) -> None |
|
_get_repo |
_get_repo() -> SyncGraphRepository |
|
close |
close() -> None |
Close the writer's repository (no-op for an injected/shared one). |
_write |
_write(query: str, params: Mapping[str, Any] | None = None) -> list[dict[str, Any]] |
|
_read |
_read(query: str, params: Mapping[str, Any] | None = None) -> list[dict[str, Any]] |
|
_merge_node |
_merge_node(label: str, key_prop: str, key_value: str, properties: Mapping[str, Any]) -> str |
MERGE one node on (label, key_prop=key_value) and SET the rest. |
_edge |
_edge(from_label: str, from_key_prop: str, from_key_value: str, rel_type: str, to_label: str, to_key_prop: str, to_key_value: str, rel_properties: Mapping[str, Any] | None = None) -> None |
MERGE one edge between two already-MERGEd nodes. |
_key_prop staticmethod |
_key_prop(label: str) -> str |
|
upsert_architecture |
upsert_architecture(project: str, name: str, *, version: str = '', description: str = '', status: str = 'draft') -> str |
MERGE an Architecture root and its owning Project (HAS_ARCHITECTURE). |
upsert_component |
upsert_component(project: str, name: str, *, responsibility: str = '', compute_platform: str = '', protocol: str = '', required: bool = True, boundary: str = '') -> str |
|
upsert_api_contract |
upsert_api_contract(project: str, component_natural: str, name: str, *, kind: str = '', shape: str = '', sync: bool | None = None, version: str = '') -> str |
|
upsert_data_entity |
upsert_data_entity(project: str, name: str, *, access_patterns: Sequence[str] | None = None, consistency: str = '', store_kind: str = '', pii: bool | None = None) -> str |
|
upsert_quality_attribute |
upsert_quality_attribute(project: str, name: str, *, metric: str = '', target: str = '', measured_as: str = '', pillar: str = '') -> str |
|
upsert_constraint |
upsert_constraint(project: str, name: str, *, kind: str = '', severity: str = '', description: str = '') -> str |
|
upsert_capacity_model |
upsert_capacity_model(project: str, name: str, *, peak_rps: int | None = None, avg_rps: int | None = None, read_write_ratio: str = '', storage_gb_yr: float | None = None, working_set_fits_memory: bool | None = None, binding_constraint: str = '') -> str |
|
upsert_cost_line_item |
upsert_cost_line_item(project: str, cost_model_natural: str, index: int, *, component: str = '', meter: str = '', quantity: float | None = None, unit_price: float | None = None, monthly: float | None = None, billing_model: str = '') -> str |
|
upsert_cost_model |
upsert_cost_model(project: str, name: str, *, run_rate_monthly: float | None = None, currency: str = '', at_10x_dominant_meter: str = '', top_drivers: Sequence[str] | None = None, priced_at: str | None = None, region: str = '', line_items: Sequence[Mapping[str, Any]] | None = None) -> str |
MERGE a CostModel, and (per REQ) its CostLineItem children. |
upsert_environment |
upsert_environment(project: str, name: str, *, tier: str = '') -> str |
|
upsert_deployment |
upsert_deployment(project: str, component_natural: str, environment_natural: str, *, delivery_model: str = '', resiliency: str = '', replicas: int | None = None, network_placement: str = '') -> str |
MERGE a Deployment and wire DEPLOYED_AS/IN_ENVIRONMENT. |
upsert_architecture_decision |
upsert_architecture_decision(project: str, adr_number: int, title: str, *, padded_id: str, register: str, status: str = 'Proposed', date: str | None = None, deciders: Sequence[str] | None = None, context: str = '', decision: str = '', alternatives: str = '', consequences: str = '', one_way_door: bo… |
MERGE an ArchitectureDecision, enforcing ruling-1 immutability. |
rebaseline_architecture_decision |
rebaseline_architecture_decision(project: str, padded_id: str, *, authority: str, title: str, status: str, date: str | None, context: str, decision: str, alternatives: str, consequences: str, named_ids: frozenset[str] | None = None, authority_status: str | None = None) -> str |
Single-id, ruling-authorised body re-baseline of an existing node. |
read_architecture_decision_hashes |
read_architecture_decision_hashes(project: str) -> dict[str, str] |
{padded_id: body_sha} for every factory-register decision node of project (read-only; a node with no stored hash maps to ""). |
rebaseline_first_capture |
rebaseline_first_capture(project: str, padded_id: str, *, authority: str, wiki_body_sha: str | None, title: str, status: str, date: str | None, context: str, decision: str, alternatives: str, consequences: str) -> tuple[str, str] |
First docs capture of a node whose body never came from docs. |
set_architecture_decision_status |
set_architecture_decision_status(adr_id: str, status: str) -> None |
Status-only mutation -- always allowed, even on an Accepted ADR. |
upsert_tradeoff |
upsert_tradeoff(project: str, name: str, *, improves: str = '', at_cost_of: str = '', rationale: str = '') -> str |
|
upsert_risk |
upsert_risk(project: str, name: str, *, description: str = '', likelihood: str = '', impact: str = '', mitigation: str = '') -> str |
|
upsert_failure_mode |
upsert_failure_mode(project: str, name: str, *, trigger: str = '', blast_radius: str = '', degradation: str = '') -> str |
|
upsert_technology_choice |
upsert_technology_choice(project: str, name: str, *, category: str = '', rationale: str = '', reversibility: str = '') -> str |
|
upsert_slo |
upsert_slo(project: str, component_natural: str, name: str, *, sli: str = '', target: str = '', window: str = '', error_budget: str = '', burn_alert_windows: Sequence[str] | None = None) -> str |
|
upsert_alert |
upsert_alert(project: str, name: str, *, alert_on: str = '', tier: str = '', burn_rate: str = '', runbook_url: str = '') -> str |
|
upsert_pattern |
upsert_pattern(name: str, *, category: str = '', problem_area: str = '', pillars: Sequence[str] | None = None, description: str = '', source: str = '') -> str |
MERGE a (global) Pattern -- REUSE+EXT of the existing canon label. |
upsert_artifact |
upsert_artifact(project: str, name: str, *, artifact_type: str, path: str = '') -> str |
MERGE an Artifact -- a deliverable file (design doc, ADR page, IaC module, Helm chart, golden image, diagram) -- project-qualified per ruling 2, exactly like every other Tier-J label. |
upsert_waf_pillar |
upsert_waf_pillar(name: str, *, concern: str = '') -> str |
|
upsert_cloud_service |
upsert_cloud_service(name: str, *, vendor: str = '', neutral_kind: str = '', category: str = '', docs_url: str = '') -> str |
|
seed_reference_data |
seed_reference_data(project: str, *, waf_pillars: Sequence[Mapping[str, str]] | None = None, cloud_services: Sequence[Mapping[str, str]] | None = None) -> dict[str, int] |
Idempotent MERGE of the 5 WAFPillar nodes + the curated CloudService seed (ruling 6). |
link_has_component |
link_has_component(architecture_id: str, component_id: str) -> None |
|
link_exposes |
link_exposes(component_id: str, api_contract_id: str) -> None |
|
link_data_flows_to |
link_data_flows_to(from_component_id: str, to_component_id: str, *, protocol: str = '', sync: bool | None = None, critical_path: bool | None = None, ordering: str = '', via: str = '') -> None |
|
link_depends_on |
link_depends_on(from_component_id: str, to_component_id: str, *, kind: str = '') -> None |
|
link_persists_to |
link_persists_to(component_id: str, data_entity_id: str, *, mode: str = '') -> None |
|
link_relates_to |
link_relates_to(from_data_entity_id: str, to_data_entity_id: str, *, cardinality: str = '', via: str = '') -> None |
|
link_runs_on |
link_runs_on(component_id: str, technology_choice_id: str) -> None |
|
link_has_scenario |
link_has_scenario(requirement_item_id: str, scenario_id: str) -> None |
|
link_satisfies |
link_satisfies(component_id: str, requirement_item_id: str) -> None |
|
link_has_quality_attribute |
link_has_quality_attribute(requirement_item_id: str, quality_attribute_id: str) -> None |
|
link_measured_on |
link_measured_on(quality_attribute_id: str, component_id: str) -> None |
|
link_addresses_pillar |
link_addresses_pillar(quality_attribute_id: str, waf_pillar_name: str) -> None |
|
link_must_follow |
link_must_follow(component_id: str, constraint_id: str) -> None |
|
link_constrains |
link_constrains(constraint_id: str, architecture_id: str) -> None |
|
link_modeled_by |
link_modeled_by(architecture_id: str, capacity_model_id: str) -> None |
|
link_informs |
link_informs(capacity_model_id: str, cost_model_id: str) -> None |
|
link_has_line_item |
link_has_line_item(cost_model_id: str, cost_line_item_id: str) -> None |
|
link_meters |
link_meters(cost_line_item_id: str, cloud_service_name: str) -> None |
|
link_decides |
link_decides(adr_id: str, target_label: str, target_id: str) -> None |
|
link_documents |
link_documents(artifact_id: str, architecture_id: str) -> None |
(Artifact)-[:DOCUMENTS]->(Architecture) -- same pattern as :meth:link_decides. |
link_chose |
link_chose(adr_id: str, technology_choice_id: str) -> None |
|
link_rejected |
link_rejected(adr_id: str, target_label: str, target_id: str, *, reason: str = '') -> None |
|
link_justified_by |
link_justified_by(adr_id: str, requirement_item_id: str) -> None |
|
link_weighs |
link_weighs(adr_id: str, tradeoff_id: str) -> None |
|
link_supersedes |
link_supersedes(new_adr_id: str, old_adr_id: str, *, date: str | None = None) -> None |
The evolution path for an Accepted ADR (ruling 1): a NEW node supersedes the old one, rather than editing the old one's body. |
link_selects |
link_selects(technology_choice_id: str, cloud_service_name: str) -> None |
|
link_forced_by |
link_forced_by(technology_choice_id: str, requirement_item_id: str) -> None |
The "name the requirement that forces it" gate (§2 rule 6): a TechnologyChoice with no FORCED_BY edge is unjustified. |
link_applies |
link_applies(source_label: str, source_id: str, pattern_name: str) -> None |
(ArchitectureDecision|Component)-[:APPLIES]->(Pattern). |
link_implements |
link_implements(component_id: str, pattern_name: str) -> None |
|
link_threatens |
link_threatens(risk_id: str, target_label: str, target_id: str) -> None |
|
link_affects |
link_affects(failure_mode_id: str, component_id: str) -> None |
|
link_mitigated_by |
link_mitigated_by(source_label: str, source_id: str, target_label: str, target_id: str) -> None |
(Risk|FailureMode)-[:MITIGATED_BY]->(Pattern|Constraint). |
link_has_slo |
link_has_slo(component_id: str, slo_id: str) -> None |
|
link_measures |
link_measures(slo_id: str, quality_attribute_id: str) -> None |
|
link_alerts_on |
link_alerts_on(alert_id: str, slo_id: str) -> None |
|
link_deployed_as |
link_deployed_as(component_id: str, deployment_id: str) -> None |
|
link_in_environment |
link_in_environment(deployment_id: str, environment_id: str) -> None |
Functions
| Function | Signature | Summary |
|---|---|---|
_require_project_slug traces |
_require_project_slug(project: str, caller: str) -> None |
Guard every project-scoped write against an empty/whitespace/qualified slug -- same fail-fast doctrine as kg_design.py's _require_project_slug (2026-07 audit: an anonymous/throwaway slug… |
_pid |
_pid(project: str, natural: str, caller: str) -> str |
Project-qualified id builder: "{project}:{natural}" (ruling 2). |
slugify |
slugify(text: str) -> str |
Deterministic slug for the id = "{project}:{slug}" labels (Constraint/QualityAttribute/TradeOff/Risk/FailureMode/TechnologyChoice/ Alert) -- lowercase, non-alphanumeric runs collapsed to a… |
_adr_body_sha traces |
_adr_body_sha(context: str, decision: str, alternatives: str, consequences: str) -> str |
Content hash over the four immutable ADR body fields (ruling 1). |
_now_iso |
_now_iso() -> str |
|
check_adr_integrity traces |
check_adr_integrity(repo: SyncGraphRepository) -> list[str] |
Read-only conformance gate (ruling 1, gate half). |
Source
"""The converged architecture-ontology writer (RATIFIED, wave ontology-phase1).
Supersedes ``scripts/sync_architecture_to_graph.py``'s ad-hoc global-name
writes (``plans/architecture-ontology-draft.md`` §7, ruling 8): the single
writer for every Tier J node/relationship family the ratified ontology
defines (``canonical_schema.py`` LABEL_*/REL_* constants), MERGEing on the
canonical identity for each label:
* project-scoped labels key on ``id = "{project}:{natural}"`` (ruling 2, the
same File/Directory/Package precedent as every other project-scoped tier);
* the genuinely global reference-data labels — ``WAFPillar``, ``CloudService``
(``Region`` deferred, ruling 6) — key on their bare natural id instead.
**ADR immutability (ruling 1) is enforced at BOTH layers**:
1. Writer-side guard — :meth:`ArchitectureWriter.upsert_architecture_decision`
computes a ``body_sha`` over the four immutable body fields (context/
decision/alternatives/consequences) and refuses to change them once the
matched node's ``status`` is ``"Accepted"`` (raises
:class:`AdrImmutabilityError`). Evolution is a **new** ADR linked by
:meth:`ArchitectureWriter.link_supersedes` — never an in-place body edit.
``status`` alone (and non-body metadata) may still change on an Accepted
ADR — :meth:`ArchitectureWriter.set_architecture_decision_status` is the
dedicated seam for that.
2. Conformance gate — :func:`check_adr_integrity` is the read-only half: it
recomputes ``body_sha`` for every live ``Accepted`` ADR and flags any node
whose stored hash no longer matches (a bypassed writer, hand-tampering, or
an out-of-band Cypher write). ``scripts/kg_schema.py check`` wires this in
alongside :func:`canonical_schema.check`.
Follows the driver-ownership + ``execute_read``/``execute_write`` convention
already live in ``knowledge_graph/kg_design.py`` (``SyncGraphRepository``),
but resolves the connection via ``core.graph.repository.create_graph_repository``
+ ``GraphRepositoryConfig.from_env()`` rather than ``kg_design.py``'s direct
``NEO4J_URI``-constant construction — the newer, call-time env-resolution
seam (``core/graph/config.py`` docstring; project doctrine "read env at call
time", not at import time).
"""
from __future__ import annotations
import hashlib
import re
from collections.abc import Mapping, Sequence
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.core.graph import (
GraphRepositoryConfig,
create_graph_repository,
)
from orchestrata_ultimate.knowledge_graph import canonical_schema as cs
if TYPE_CHECKING:
from orchestrata_ultimate.core.graph import SyncGraphRepository
@traces(spec="106", reqs=["REQ-106-006"])
class AdrImmutabilityError(Exception):
"""Raised when a write would mutate an Accepted ArchitectureDecision's body.
An ADR's body (context/decision/alternatives/consequences) is frozen once
``status = "Accepted"`` (ruling 1, ``plans/architecture-ontology-draft.md``
§2 rule 5). Evolve via a new ADR + :meth:`ArchitectureWriter.link_supersedes`
instead of editing this one in place.
"""
#: Accepted ruling ADR -> the exact ADR ids it authorises
#: ``rebaseline_architecture_decision`` to overwrite (ADR-0096: ADR-0073's graph
#: body was captured from the wiki summary while Proposed and is stale).
_REBASELINE_AUTHORITIES: dict[str, frozenset[str]] = {
"ADR-0096": frozenset({"ADR-0073"}),
}
#: Accepted ruling ADR(s) that authorise ``rebaseline_first_capture`` (ADR-0098:
#: a node whose body never came from docs is baselined once from docs).
_FIRST_CAPTURE_AUTHORITIES = frozenset({"ADR-0098"})
@traces(spec="106", reqs=["REQ-106-003"])
def _require_project_slug(project: str, caller: str) -> None:
"""Guard every project-scoped write against an empty/whitespace/qualified
slug -- same fail-fast doctrine as ``kg_design.py``'s
``_require_project_slug`` (2026-07 audit: an anonymous/throwaway slug
MERGEs its own junk ``:Project`` hub every project-scoped label anchors
onto)."""
if not project or not project.strip():
raise ValueError(
f"ArchitectureWriter.{caller}: project must be a non-empty, "
"non-whitespace string -- refusing to write an anonymous "
"Project hub node."
)
if ":" in project:
raise ValueError(
f"ArchitectureWriter.{caller}: project must not contain ':': {project!r}"
)
def _pid(project: str, natural: str, caller: str) -> str:
"""Project-qualified id builder: ``"{project}:{natural}"`` (ruling 2)."""
_require_project_slug(project, caller)
if not natural or not str(natural).strip():
raise ValueError(f"ArchitectureWriter.{caller}: natural id must be non-empty")
return f"{project}:{natural}"
_SLUG_RE = re.compile(r"[^a-z0-9]+")
def slugify(text: str) -> str:
"""Deterministic slug for the ``id = "{project}:{slug}"`` labels
(Constraint/QualityAttribute/TradeOff/Risk/FailureMode/TechnologyChoice/
Alert) -- lowercase, non-alphanumeric runs collapsed to a single hyphen,
leading/trailing hyphens trimmed."""
slug = _SLUG_RE.sub("-", text.strip().lower()).strip("-")
return slug or "unnamed"
ADR_BODY_FIELDS: tuple[str, ...] = (
"context",
"decision",
"alternatives",
"consequences",
)
# Ratified Artifact.artifact_type enum (plans/architecture-ontology-draft.md
# §3 Phase-6/18/24 rows: design-doc/adr Phase 6, iac-module Phase 24,
# helm-chart/image Phase 18/24, diagram Phase 4/6).
ARTIFACT_TYPES: frozenset[str] = frozenset(
{"design-doc", "adr", "iac-module", "helm-chart", "image", "diagram"}
)
@traces(spec="106", reqs=["REQ-106-006"])
def _adr_body_sha(
context: str, decision: str, alternatives: str, consequences: str
) -> str:
"""Content hash over the four immutable ADR body fields (ruling 1)."""
payload = "\x1f".join(
(context or "", decision or "", alternatives or "", consequences or "")
)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def _now_iso() -> str:
return datetime.now(UTC).isoformat()
@traces(spec="106", reqs=["REQ-106-006"])
def check_adr_integrity(repo: SyncGraphRepository) -> list[str]:
"""Read-only conformance gate (ruling 1, gate half).
Recomputes ``body_sha`` for every live ``ArchitectureDecision`` whose
``status = "Accepted"`` and reports the ids of any whose current body no
longer matches its stamped hash -- a bypassed writer, hand-tampering, or
an out-of-band Cypher write. Never mutates the graph.
Args:
repo: A ``SyncGraphRepository`` connected to the graph to check.
Returns:
A sorted list of offending ``ArchitectureDecision.id`` values (empty
when every Accepted ADR's body is intact).
"""
rows = repo.execute_read(
f"MATCH (a:{cs.LABEL_ARCHITECTURE_DECISION} {{status: 'Accepted'}}) "
"RETURN a.id AS id, a.body_sha AS body_sha, a.context AS context, "
"a.decision AS decision, a.alternatives AS alternatives, "
"a.consequences AS consequences"
)
findings: list[str] = []
for row in rows:
recomputed = _adr_body_sha(
row.get("context") or "",
row.get("decision") or "",
row.get("alternatives") or "",
row.get("consequences") or "",
)
if row.get("body_sha") != recomputed:
findings.append(str(row["id"]))
return sorted(findings)
# ---------------------------------------------------------------------------
# Ruling 6 -- curated CloudService seed + the 5 fixed WAFPillar nodes.
#
# The ~30 services actually named across the system-design skill's 26
# reference files (messaging-servicebus-eventhubs.md, databases-postgres-
# mysql-redis.md, databricks-fabric-analytics.md, azure-storage.md,
# kubernetes-aks-helm.md, api-management-apim.md, key-vault-secrets.md,
# azure-networking.md, cicd-github-actions-argocd.md, observability.md) --
# NOT the full ~714-icon Azure catalog (full-catalog ingestion was explicitly
# rejected, ruling 6). Region seeding is lazy, deferred (not this wave).
# ---------------------------------------------------------------------------
WAF_PILLARS: tuple[dict[str, str], ...] = (
{"name": "Reliability", "concern": "Resiliency, availability, recovery"},
{"name": "Security", "concern": "Confidentiality, integrity, threat mitigation"},
{"name": "CostOptimization", "concern": "ROI, reduce waste"},
{"name": "OperationalExcellence", "concern": "Observability, DevOps, safe deploys"},
{"name": "PerformanceEfficiency", "concern": "Scale to demand efficiently"},
)
CLOUD_SERVICES: tuple[dict[str, str], ...] = (
{
"name": "Azure Service Bus",
"vendor": "azure",
"neutral_kind": "message-broker",
"category": "messaging",
},
{
"name": "Azure Event Hubs",
"vendor": "azure",
"neutral_kind": "event-stream",
"category": "messaging",
},
{
"name": "Azure Event Grid",
"vendor": "azure",
"neutral_kind": "event-router",
"category": "messaging",
},
{
"name": "Azure Database for PostgreSQL - Flexible Server",
"vendor": "azure",
"neutral_kind": "relational-db",
"category": "database",
},
{
"name": "Azure Database for MySQL - Flexible Server",
"vendor": "azure",
"neutral_kind": "relational-db",
"category": "database",
},
{
"name": "Azure Cache for Redis",
"vendor": "azure",
"neutral_kind": "cache",
"category": "database",
},
{
"name": "Azure Managed Redis",
"vendor": "azure",
"neutral_kind": "cache",
"category": "database",
},
{
"name": "Azure Databricks",
"vendor": "azure",
"neutral_kind": "lakehouse-platform",
"category": "analytics",
},
{
"name": "Microsoft Fabric",
"vendor": "azure",
"neutral_kind": "lakehouse-platform",
"category": "analytics",
},
{
"name": "Azure SQL Database",
"vendor": "azure",
"neutral_kind": "relational-db",
"category": "database",
},
{
"name": "Azure Blob Storage",
"vendor": "azure",
"neutral_kind": "object-storage",
"category": "storage",
},
{
"name": "Azure Data Lake Storage Gen2",
"vendor": "azure",
"neutral_kind": "data-lake",
"category": "storage",
},
{
"name": "Azure Files",
"vendor": "azure",
"neutral_kind": "file-storage",
"category": "storage",
},
{
"name": "Azure NetApp Files",
"vendor": "azure",
"neutral_kind": "file-storage",
"category": "storage",
},
{
"name": "Azure Managed Lustre",
"vendor": "azure",
"neutral_kind": "hpc-file-storage",
"category": "storage",
},
{
"name": "Azure Managed Disks",
"vendor": "azure",
"neutral_kind": "block-storage",
"category": "storage",
},
{
"name": "Azure Kubernetes Service",
"vendor": "azure",
"neutral_kind": "container-orchestration",
"category": "compute",
},
{
"name": "Azure Container Apps",
"vendor": "azure",
"neutral_kind": "serverless-containers",
"category": "compute",
},
{
"name": "Azure Batch",
"vendor": "azure",
"neutral_kind": "batch-scheduler",
"category": "compute",
},
{
"name": "Azure API Management",
"vendor": "azure",
"neutral_kind": "api-gateway",
"category": "integration",
},
{
"name": "Azure Key Vault",
"vendor": "azure",
"neutral_kind": "secrets-manager",
"category": "security",
},
{
"name": "Azure Virtual Network",
"vendor": "azure",
"neutral_kind": "virtual-network",
"category": "networking",
},
{
"name": "Azure Firewall",
"vendor": "azure",
"neutral_kind": "network-firewall",
"category": "networking",
},
{
"name": "Azure Bastion",
"vendor": "azure",
"neutral_kind": "bastion-host",
"category": "networking",
},
{
"name": "Azure Load Balancer",
"vendor": "azure",
"neutral_kind": "load-balancer",
"category": "networking",
},
{
"name": "Azure Container Registry",
"vendor": "azure",
"neutral_kind": "container-registry",
"category": "devops",
},
{
"name": "GitHub Actions",
"vendor": "github",
"neutral_kind": "ci-pipeline",
"category": "devops",
},
{
"name": "Argo CD",
"vendor": "argoproj",
"neutral_kind": "gitops-cd",
"category": "devops",
},
{
"name": "Azure DevOps",
"vendor": "azure",
"neutral_kind": "alm-platform",
"category": "devops",
},
{
"name": "Azure Monitor",
"vendor": "azure",
"neutral_kind": "monitoring",
"category": "observability",
},
)
class ArchitectureWriter:
"""The converged writer over the RATIFIED architecture ontology (Tier J).
Ownership: when ``repo`` is None, the writer lazily builds and owns its
own repository (via ``create_graph_repository`` / ``GraphRepositoryConfig
.from_env()``, resolved at first write, not at import time) --
``close()`` on it really closes the underlying driver. When a ``repo`` is
injected, ownership belongs to whoever built it (``BoltSyncRepository
.close()`` is already a no-op for a shared/injected driver).
"""
def __init__(self, repo: SyncGraphRepository | None = None) -> None:
self._repo: SyncGraphRepository | None = repo
@traces(spec="106", reqs=["REQ-106-008"])
def _get_repo(self) -> SyncGraphRepository:
if self._repo is None:
self._repo = create_graph_repository(GraphRepositoryConfig.from_env())
return self._repo
def close(self) -> None:
"""Close the writer's repository (no-op for an injected/shared one)."""
if self._repo is not None:
self._repo.close()
self._repo = None
@traces(spec="106", reqs=["REQ-106-008"])
def _write(
self, query: str, params: Mapping[str, Any] | None = None
) -> list[dict[str, Any]]:
return self._get_repo().execute_write(query, dict(params or {}))
@traces(spec="106", reqs=["REQ-106-008"])
def _read(
self, query: str, params: Mapping[str, Any] | None = None
) -> list[dict[str, Any]]:
return self._get_repo().execute_read(query, dict(params or {}))
# -- generic node/edge primitives -----------------------------------
@traces(spec="106", reqs=["REQ-106-008"])
def _merge_node(
self, label: str, key_prop: str, key_value: str, properties: Mapping[str, Any]
) -> str:
"""MERGE one node on ``(label, key_prop=key_value)`` and SET the rest.
``label`` is always one of this module's own ``cs.LABEL_*`` constants
(never external input), so f-string label interpolation is safe --
the same trusted-fixed-set stance as the writer this module
supersedes.
"""
props = {k: v for k, v in properties.items() if k != key_prop}
set_clause = ", ".join(f"n.{k} = ${k}" for k in props)
query = f"MERGE (n:{label} {{{key_prop}: $__key}})"
if set_clause:
query += f" SET {set_clause}"
params: dict[str, Any] = dict(props)
params["__key"] = key_value
self._write(query, params)
return key_value
@traces(spec="106", reqs=["REQ-106-008"])
def _edge(
self,
from_label: str,
from_key_prop: str,
from_key_value: str,
rel_type: str,
to_label: str,
to_key_prop: str,
to_key_value: str,
rel_properties: Mapping[str, Any] | None = None,
) -> None:
"""MERGE one edge between two already-MERGEd nodes.
A MATCH on a not-yet-created endpoint yields no rows and silently
drops the edge (same ordering caveat the superseded writer's own
docstring called out) -- callers create both endpoints first.
"""
rel_properties = dict(rel_properties or {})
set_clause = ""
if rel_properties:
set_clause = " SET " + ", ".join(f"r.{k} = ${k}" for k in rel_properties)
query = (
f"MATCH (a:{from_label} {{{from_key_prop}: $__from}}) "
f"MATCH (b:{to_label} {{{to_key_prop}: $__to}}) "
f"MERGE (a)-[r:{rel_type}]->(b)" + set_clause
)
params: dict[str, Any] = dict(rel_properties)
params["__from"] = from_key_value
params["__to"] = to_key_value
self._write(query, params)
@staticmethod
def _key_prop(label: str) -> str:
return cs.CANONICAL_MERGE_KEYS[label]
# -- Architecture -----------------------------------------------------
def upsert_architecture(
self,
project: str,
name: str,
*,
version: str = "",
description: str = "",
status: str = "draft",
) -> str:
"""MERGE an ``Architecture`` root and its owning ``Project`` (HAS_ARCHITECTURE)."""
arch_id = _pid(project, name, "upsert_architecture")
self._merge_node(
cs.LABEL_ARCHITECTURE,
"id",
arch_id,
{
"name": name,
"version": version,
"description": description,
"status": status,
"project": project,
},
)
self._merge_node(cs.LABEL_PROJECT, "slug", project, {"slug": project})
self._edge(
cs.LABEL_PROJECT,
"slug",
project,
cs.REL_HAS_ARCHITECTURE,
cs.LABEL_ARCHITECTURE,
"id",
arch_id,
)
return arch_id
def upsert_component(
self,
project: str,
name: str,
*,
responsibility: str = "",
compute_platform: str = "",
protocol: str = "",
required: bool = True,
boundary: str = "",
) -> str:
comp_id = _pid(project, name, "upsert_component")
self._merge_node(
cs.LABEL_COMPONENT,
"id",
comp_id,
{
"name": name,
"responsibility": responsibility,
"compute_platform": compute_platform,
"protocol": protocol,
"required": required,
"boundary": boundary,
"project": project,
},
)
return comp_id
def upsert_api_contract(
self,
project: str,
component_natural: str,
name: str,
*,
kind: str = "",
shape: str = "",
sync: bool | None = None,
version: str = "",
) -> str:
_require_project_slug(project, "upsert_api_contract")
api_id = f"{project}:{component_natural}:{name}"
self._merge_node(
cs.LABEL_API_CONTRACT,
"id",
api_id,
{
"name": name,
"kind": kind,
"shape": shape,
"sync": sync,
"version": version,
"project": project,
},
)
self.link_exposes(
_pid(project, component_natural, "upsert_api_contract"), api_id
)
return api_id
def upsert_data_entity(
self,
project: str,
name: str,
*,
access_patterns: Sequence[str] | None = None,
consistency: str = "",
store_kind: str = "",
pii: bool | None = None,
) -> str:
de_id = _pid(project, name, "upsert_data_entity")
self._merge_node(
cs.LABEL_DATA_ENTITY,
"id",
de_id,
{
"name": name,
"access_patterns": list(access_patterns or []),
"consistency": consistency,
"store_kind": store_kind,
"pii": pii,
"project": project,
},
)
return de_id
def upsert_quality_attribute(
self,
project: str,
name: str,
*,
metric: str = "",
target: str = "",
measured_as: str = "",
pillar: str = "",
) -> str:
qa_id = _pid(project, slugify(name), "upsert_quality_attribute")
self._merge_node(
cs.LABEL_QUALITY_ATTRIBUTE,
"id",
qa_id,
{
"name": name,
"metric": metric,
"target": target,
"measured_as": measured_as,
"pillar": pillar,
"project": project,
},
)
return qa_id
def upsert_constraint(
self,
project: str,
name: str,
*,
kind: str = "",
severity: str = "",
description: str = "",
) -> str:
con_id = _pid(project, slugify(name), "upsert_constraint")
self._merge_node(
cs.LABEL_CONSTRAINT,
"id",
con_id,
{
"name": name,
"kind": kind,
"severity": severity,
"description": description,
"project": project,
},
)
return con_id
def upsert_capacity_model(
self,
project: str,
name: str,
*,
peak_rps: int | None = None,
avg_rps: int | None = None,
read_write_ratio: str = "",
storage_gb_yr: float | None = None,
working_set_fits_memory: bool | None = None,
binding_constraint: str = "",
) -> str:
cap_id = _pid(project, name, "upsert_capacity_model")
self._merge_node(
cs.LABEL_CAPACITY_MODEL,
"id",
cap_id,
{
"name": name,
"peak_rps": peak_rps,
"avg_rps": avg_rps,
"read_write_ratio": read_write_ratio,
"storage_gb_yr": storage_gb_yr,
"working_set_fits_memory": working_set_fits_memory,
"binding_constraint": binding_constraint,
"project": project,
},
)
return cap_id
def upsert_cost_line_item(
self,
project: str,
cost_model_natural: str,
index: int,
*,
component: str = "",
meter: str = "",
quantity: float | None = None,
unit_price: float | None = None,
monthly: float | None = None,
billing_model: str = "",
) -> str:
_require_project_slug(project, "upsert_cost_line_item")
li_id = f"{project}:{cost_model_natural}:{index}"
self._merge_node(
cs.LABEL_COST_LINE_ITEM,
"id",
li_id,
{
"component": component,
"meter": meter,
"quantity": quantity,
"unit_price": unit_price,
"monthly": monthly,
"billing_model": billing_model,
"project": project,
},
)
return li_id
def upsert_cost_model(
self,
project: str,
name: str,
*,
run_rate_monthly: float | None = None,
currency: str = "",
at_10x_dominant_meter: str = "",
top_drivers: Sequence[str] | None = None,
priced_at: str | None = None,
region: str = "",
line_items: Sequence[Mapping[str, Any]] | None = None,
) -> str:
"""MERGE a ``CostModel``, and (per REQ) its ``CostLineItem`` children.
``line_items`` is a list of kwargs for :meth:`upsert_cost_line_item`
(``component``/``meter``/``quantity``/``unit_price``/``monthly``/
``billing_model``); each is created and wired via ``HAS_LINE_ITEM``
in the order given (the id's ``{n}`` is the list index).
"""
cm_id = _pid(project, name, "upsert_cost_model")
self._merge_node(
cs.LABEL_COST_MODEL,
"id",
cm_id,
{
"name": name,
"run_rate_monthly": run_rate_monthly,
"currency": currency,
"at_10x_dominant_meter": at_10x_dominant_meter,
"top_drivers": list(top_drivers or []),
"priced_at": priced_at,
"region": region,
"project": project,
},
)
for i, item in enumerate(line_items or []):
li_id = self.upsert_cost_line_item(project, name, i, **item)
self.link_has_line_item(cm_id, li_id)
return cm_id
def upsert_environment(self, project: str, name: str, *, tier: str = "") -> str:
env_id = _pid(project, name, "upsert_environment")
self._merge_node(
cs.LABEL_ENVIRONMENT,
"id",
env_id,
{"name": name, "tier": tier, "project": project},
)
return env_id
def upsert_deployment(
self,
project: str,
component_natural: str,
environment_natural: str,
*,
delivery_model: str = "",
resiliency: str = "",
replicas: int | None = None,
network_placement: str = "",
) -> str:
"""MERGE a ``Deployment`` and wire ``DEPLOYED_AS``/``IN_ENVIRONMENT``.
Requires the ``Component`` and ``Environment`` nodes to already exist
(a MATCH on either not-yet-created endpoint silently drops the edge --
create them first, same caveat as the superseded writer).
"""
_require_project_slug(project, "upsert_deployment")
dep_id = f"{project}:{component_natural}:{environment_natural}"
self._merge_node(
cs.LABEL_DEPLOYMENT,
"id",
dep_id,
{
"delivery_model": delivery_model,
"resiliency": resiliency,
"replicas": replicas,
"network_placement": network_placement,
"project": project,
},
)
comp_id = f"{project}:{component_natural}"
env_id = f"{project}:{environment_natural}"
self.link_deployed_as(comp_id, dep_id)
self.link_in_environment(dep_id, env_id)
return dep_id
# -- ArchitectureDecision (ADR) — RULING 1 immutability guard ----------
@traces(spec="106", reqs=["REQ-106-002", "REQ-106-003"])
def upsert_architecture_decision(
self,
project: str,
adr_number: int,
title: str,
*,
padded_id: str,
register: str,
status: str = "Proposed",
date: str | None = None,
deciders: Sequence[str] | None = None,
context: str = "",
decision: str = "",
alternatives: str = "",
consequences: str = "",
one_way_door: bool | None = None,
) -> str:
"""MERGE an ``ArchitectureDecision``, enforcing ruling-1 immutability.
ADR-0047: this project has TWO independent ADR registers -- the
factory register (``docs/adr/NNNN-*.md``, always 4-digit) and the
project register (the wiki's own 1-3 digit ``ADR-NNN`` sections) --
and they are NOT the same id space: ``ADR-001`` (project) and
``ADR-0001`` (factory) are different decisions that happen to share
an integer. The node id is therefore keyed on the caller-supplied
``padded_id`` verbatim (``f"{project}:ADR-{padded_id}"``), never on
a re-padded ``adr_number`` -- that re-padding is exactly what used
to collapse the two registers onto one node. ``register`` and
``padded_id`` are stamped as node properties alongside ``adr_number``
(which stays, for callers that only need the integer).
The body (``context``/``decision``/``alternatives``/``consequences``)
is hashed into ``body_sha`` and frozen once the matched node's
``status`` is ``"Accepted"``: a call that would change the hash
raises :class:`AdrImmutabilityError` instead of writing. Everything
else (``title``/``status``/``deciders``/``date``/``one_way_door``)
remains freely mutable, including on an Accepted node -- evolve the
body via a *new* ADR + :meth:`link_supersedes` instead.
Args:
padded_id: the heading's digit string verbatim (e.g. ``"0001"``
or ``"001"``) -- forms the node id together with ``project``.
register: ``"factory"`` or ``"project"`` (ADR-0047) -- which
register this decision belongs to.
Raises:
AdrImmutabilityError: the matched node is Accepted and this call
would change its body.
"""
_require_project_slug(project, "upsert_architecture_decision")
adr_id = f"{project}:ADR-{padded_id}"
body_sha = _adr_body_sha(context, decision, alternatives, consequences)
existing = self._read(
f"MATCH (a:{cs.LABEL_ARCHITECTURE_DECISION} {{id: $id}}) "
"RETURN a.status AS status, a.body_sha AS body_sha",
{"id": adr_id},
)
if (
existing
and existing[0]["status"] == "Accepted"
and existing[0]["body_sha"] != body_sha
):
raise AdrImmutabilityError(
f"ArchitectureDecision {adr_id!r} is Accepted; its body "
"(context/decision/alternatives/consequences) is immutable. "
"Evolve via a new ADR + link_supersedes() instead of editing "
"this one in place."
)
self._merge_node(
cs.LABEL_ARCHITECTURE_DECISION,
"id",
adr_id,
{
"adr_number": int(adr_number),
"register": register,
"padded_id": padded_id,
"title": title,
"status": status,
"date": date,
"deciders": list(deciders or []),
"context": context,
"decision": decision,
"alternatives": alternatives,
"consequences": consequences,
"one_way_door": one_way_door,
"body_sha": body_sha,
"project": project,
},
)
return adr_id
@traces(spec="106", reqs=["REQ-106-006"])
def rebaseline_architecture_decision(
self,
project: str,
padded_id: str,
*,
authority: str,
title: str,
status: str,
date: str | None,
context: str,
decision: str,
alternatives: str,
consequences: str,
named_ids: frozenset[str] | None = None,
authority_status: str | None = None,
) -> str:
"""Single-id, ruling-authorised body re-baseline of an existing node.
The ONE sanctioned bypass of the ruling-1 immutability guard: an
Accepted ADR ruling (``authority``) names the exact ADR ids whose
graph body is stale and may be overwritten once with the docs (or
wiki-register) body. Two authority-verification modes:
* ``named_ids`` given (ADR-0102's generic mechanism) -- the caller
parsed the authority ADR's own docs record; ``named_ids`` is the
set of ``ADR-NNNN`` ids its Decision section names and
``authority_status`` that record's status. Refused unless the
authority record is Accepted and the target id is named.
* ``named_ids=None`` -- the authority's docs record could not be
parsed, so the hardcoded ``_REBASELINE_AUTHORITIES`` map is the
fallback (ADR-0096's original path).
Any other ``(authority, id)`` pair is refused with ``ValueError``
before a read or write. The node must already exist (a re-baseline
is never a create). Writes the body, its ``body_sha``, ``status``
and the audit properties ``rebaselined_by`` / ``rebaselined_at``.
``upsert_architecture_decision`` and its guard are untouched.
"""
_require_project_slug(project, "rebaseline_architecture_decision")
label = f"ADR-{padded_id}"
if named_ids is not None:
if authority_status != "Accepted":
raise ValueError(
f"authority {authority} is {authority_status!r} in docs, "
"not Accepted"
)
if label not in named_ids:
raise ValueError(f"{authority} does not name {label}")
elif label not in _REBASELINE_AUTHORITIES.get(authority, frozenset()):
raise ValueError(
f"{authority!r} does not authorise re-baselining {label!r}; "
f"it names {sorted(_REBASELINE_AUTHORITIES.get(authority, ()))}"
)
adr_id = f"{project}:{label}"
existing = self._read(
f"MATCH (a:{cs.LABEL_ARCHITECTURE_DECISION} {{id: $id}}) RETURN a.id AS id",
{"id": adr_id},
)
if not existing:
raise ValueError(f"cannot re-baseline {adr_id!r}: no such node")
return self._merge_node(
cs.LABEL_ARCHITECTURE_DECISION,
"id",
adr_id,
{
"title": title,
"status": status,
"date": date,
"context": context,
"decision": decision,
"alternatives": alternatives,
"consequences": consequences,
"body_sha": _adr_body_sha(
context, decision, alternatives, consequences
),
"rebaselined_by": authority,
"rebaselined_at": datetime.now(UTC).isoformat(),
},
)
@traces(spec="106", reqs=["REQ-106-006"])
def read_architecture_decision_hashes(self, project: str) -> dict[str, str]:
"""``{padded_id: body_sha}`` for every factory-register decision node
of ``project`` (read-only; a node with no stored hash maps to ``""``)."""
_require_project_slug(project, "read_architecture_decision_hashes")
rows = self._read(
f"MATCH (a:{cs.LABEL_ARCHITECTURE_DECISION} "
"{project: $project, register: 'factory'}) "
"RETURN a.padded_id AS padded_id, a.body_sha AS body_sha",
{"project": project},
)
return {str(r["padded_id"]): str(r.get("body_sha") or "") for r in rows}
@traces(spec="106", reqs=["REQ-106-006"])
def rebaseline_first_capture(
self,
project: str,
padded_id: str,
*,
authority: str,
wiki_body_sha: str | None,
title: str,
status: str,
date: str | None,
context: str,
decision: str,
alternatives: str,
consequences: str,
) -> tuple[str, str]:
"""First docs capture of a node whose body never came from docs.
ADR-0098 rules 1-3: the ONLY bodies immutability protects are docs
bodies, so a node holding the empty body or the wiki entry's parsed
body (``wiki_body_sha``) is baselined once from docs. Refused with
``ValueError`` unless ``authority`` is in ``_FIRST_CAPTURE_AUTHORITIES``,
the node exists, and its stored ``body_sha`` is the empty-body hash or
``wiki_body_sha`` -- a node whose body already differs from both is real
drift and is never touched. Writes the body, ``body_sha``, status, the
audit pair ``rebaselined_by``/``rebaselined_at`` and the docs-capture
marker ``docs_captured``.
Returns:
``(before_sha, after_sha)`` of the node's body hash.
"""
_require_project_slug(project, "rebaseline_first_capture")
if authority not in _FIRST_CAPTURE_AUTHORITIES:
raise ValueError(
f"{authority!r} does not authorise a first-capture re-baseline; "
f"authorities: {sorted(_FIRST_CAPTURE_AUTHORITIES)}"
)
adr_id = f"{project}:ADR-{padded_id}"
existing = self._read(
f"MATCH (a:{cs.LABEL_ARCHITECTURE_DECISION} {{id: $id}}) "
"RETURN a.body_sha AS body_sha",
{"id": adr_id},
)
if not existing:
raise ValueError(f"cannot re-baseline {adr_id!r}: no such node")
before = str(existing[0].get("body_sha") or "")
first_capture = {_adr_body_sha("", "", "", "")}
if wiki_body_sha:
first_capture.add(wiki_body_sha)
if before not in first_capture:
raise ValueError(
f"{adr_id!r} body {before[:10]!r} is neither empty nor the wiki "
"body: not a first capture (real drift needs its own ADR)"
)
after = _adr_body_sha(context, decision, alternatives, consequences)
self._merge_node(
cs.LABEL_ARCHITECTURE_DECISION,
"id",
adr_id,
{
"title": title,
"status": status,
"date": date,
"context": context,
"decision": decision,
"alternatives": alternatives,
"consequences": consequences,
"body_sha": after,
"docs_captured": True,
"rebaselined_by": authority,
"rebaselined_at": datetime.now(UTC).isoformat(),
},
)
return before, after
def set_architecture_decision_status(self, adr_id: str, status: str) -> None:
"""Status-only mutation -- always allowed, even on an Accepted ADR.
The dedicated seam for a status transition (``Proposed``
-> ``Accepted``, ``Accepted`` -> ``Superseded``/``Deprecated``) that
never touches the immutable body fields, so it never raises
:class:`AdrImmutabilityError`.
"""
self._write(
f"MATCH (a:{cs.LABEL_ARCHITECTURE_DECISION} {{id: $id}}) "
"SET a.status = $status, a.updated_at = $ts",
{"id": adr_id, "status": status, "ts": _now_iso()},
)
def upsert_tradeoff(
self,
project: str,
name: str,
*,
improves: str = "",
at_cost_of: str = "",
rationale: str = "",
) -> str:
to_id = _pid(project, slugify(name), "upsert_tradeoff")
self._merge_node(
cs.LABEL_TRADE_OFF,
"id",
to_id,
{
"name": name,
"improves": improves,
"at_cost_of": at_cost_of,
"rationale": rationale,
"project": project,
},
)
return to_id
def upsert_risk(
self,
project: str,
name: str,
*,
description: str = "",
likelihood: str = "",
impact: str = "",
mitigation: str = "",
) -> str:
risk_id = _pid(project, slugify(name), "upsert_risk")
self._merge_node(
cs.LABEL_RISK,
"id",
risk_id,
{
"name": name,
"description": description,
"likelihood": likelihood,
"impact": impact,
"mitigation": mitigation,
"project": project,
},
)
return risk_id
def upsert_failure_mode(
self,
project: str,
name: str,
*,
trigger: str = "",
blast_radius: str = "",
degradation: str = "",
) -> str:
fm_id = _pid(project, slugify(name), "upsert_failure_mode")
self._merge_node(
cs.LABEL_FAILURE_MODE,
"id",
fm_id,
{
"name": name,
"trigger": trigger,
"blast_radius": blast_radius,
"degradation": degradation,
"project": project,
},
)
return fm_id
def upsert_technology_choice(
self,
project: str,
name: str,
*,
category: str = "",
rationale: str = "",
reversibility: str = "",
) -> str:
tc_id = _pid(project, slugify(name), "upsert_technology_choice")
self._merge_node(
cs.LABEL_TECHNOLOGY_CHOICE,
"id",
tc_id,
{
"name": name,
"category": category,
"rationale": rationale,
"reversibility": reversibility,
"project": project,
},
)
return tc_id
def upsert_slo(
self,
project: str,
component_natural: str,
name: str,
*,
sli: str = "",
target: str = "",
window: str = "",
error_budget: str = "",
burn_alert_windows: Sequence[str] | None = None,
) -> str:
_require_project_slug(project, "upsert_slo")
slo_id = f"{project}:{component_natural}:{name}"
self._merge_node(
cs.LABEL_SERVICE_LEVEL_OBJECTIVE,
"id",
slo_id,
{
"name": name,
"sli": sli,
"target": target,
"window": window,
"error_budget": error_budget,
"burn_alert_windows": list(burn_alert_windows or []),
"project": project,
},
)
self.link_has_slo(_pid(project, component_natural, "upsert_slo"), slo_id)
return slo_id
def upsert_alert(
self,
project: str,
name: str,
*,
alert_on: str = "",
tier: str = "",
burn_rate: str = "",
runbook_url: str = "",
) -> str:
alert_id = _pid(project, slugify(name), "upsert_alert")
self._merge_node(
cs.LABEL_ALERT,
"id",
alert_id,
{
"name": name,
"alert_on": alert_on,
"tier": tier,
"burn_rate": burn_rate,
"runbook_url": runbook_url,
"project": project,
},
)
return alert_id
def upsert_pattern(
self,
name: str,
*,
category: str = "",
problem_area: str = "",
pillars: Sequence[str] | None = None,
description: str = "",
source: str = "",
) -> str:
"""MERGE a (global) ``Pattern`` -- REUSE+EXT of the existing canon label."""
self._merge_node(
cs.LABEL_PATTERN,
"name",
name,
{
"category": category,
"problem_area": problem_area,
"pillars": list(pillars or []),
"description": description,
"source": source,
},
)
return name
def upsert_artifact(
self,
project: str,
name: str,
*,
artifact_type: str,
path: str = "",
) -> str:
"""MERGE an ``Artifact`` -- a deliverable file (design doc, ADR page,
IaC module, Helm chart, golden image, diagram) -- project-qualified
per ruling 2, exactly like every other Tier-J label.
Raises:
ValueError: ``artifact_type`` is not one of the ratified enum
values (:data:`ARTIFACT_TYPES`).
"""
if artifact_type not in ARTIFACT_TYPES:
raise ValueError(
"ArchitectureWriter.upsert_artifact: artifact_type must be "
f"one of {sorted(ARTIFACT_TYPES)}, got {artifact_type!r}"
)
art_id = _pid(project, name, "upsert_artifact")
self._merge_node(
cs.LABEL_ARTIFACT,
"id",
art_id,
{
"name": name,
"artifact_type": artifact_type,
"path": path,
"project": project,
},
)
return art_id
# -- Ruling 6 reference data (global) ----------------------------------
@traces(spec="106", reqs=["REQ-106-008"])
def upsert_waf_pillar(self, name: str, *, concern: str = "") -> str:
self._merge_node(cs.LABEL_WAF_PILLAR, "name", name, {"concern": concern})
return name
@traces(spec="106", reqs=["REQ-106-008"])
def upsert_cloud_service(
self,
name: str,
*,
vendor: str = "",
neutral_kind: str = "",
category: str = "",
docs_url: str = "",
) -> str:
self._merge_node(
cs.LABEL_CLOUD_SERVICE,
"name",
name,
{
"vendor": vendor,
"neutral_kind": neutral_kind,
"category": category,
"docs_url": docs_url,
},
)
return name
@traces(spec="106", reqs=["REQ-106-005"])
def seed_reference_data(
self,
project: str,
*,
waf_pillars: Sequence[Mapping[str, str]] | None = None,
cloud_services: Sequence[Mapping[str, str]] | None = None,
) -> dict[str, int]:
"""Idempotent MERGE of the 5 WAFPillar nodes + the curated CloudService
seed (ruling 6). Defaults to the real, ratified :data:`WAF_PILLARS` /
:data:`CLOUD_SERVICES` catalogs; ``waf_pillars``/``cloud_services``
override for callers (e.g. tests) that want a smaller/different set
without touching the module constants.
``project`` is REQUIRED: the session-254 root-connectivity invariant
says every node must reach the root ``Project``, so each seeded
reference node is anchored via ``(:Project)-[:REFERENCES]->(node)``.
The nodes themselves stay global (natural ``name`` identity, shared
across projects); only the hub edge is per-project.
Returns:
``{"waf_pillars": N, "cloud_services": M}`` counts written.
"""
_require_project_slug(project, "seed_reference_data")
pillars = list(waf_pillars if waf_pillars is not None else WAF_PILLARS)
services = list(
cloud_services if cloud_services is not None else CLOUD_SERVICES
)
self._merge_node(cs.LABEL_PROJECT, "slug", project, {"slug": project})
for pillar in pillars:
self.upsert_waf_pillar(pillar["name"], concern=pillar.get("concern", ""))
self._edge(
cs.LABEL_PROJECT,
"slug",
project,
cs.REL_REFERENCES,
cs.LABEL_WAF_PILLAR,
"name",
pillar["name"],
)
for service in services:
self.upsert_cloud_service(
service["name"],
vendor=service.get("vendor", ""),
neutral_kind=service.get("neutral_kind", ""),
category=service.get("category", ""),
docs_url=service.get("docs_url", ""),
)
self._edge(
cs.LABEL_PROJECT,
"slug",
project,
cs.REL_REFERENCES,
cs.LABEL_CLOUD_SERVICE,
"name",
service["name"],
)
return {"waf_pillars": len(pillars), "cloud_services": len(services)}
# -- §4 edge helpers ----------------------------------------------------
def link_has_component(self, architecture_id: str, component_id: str) -> None:
self._edge(
cs.LABEL_ARCHITECTURE,
"id",
architecture_id,
cs.REL_HAS_COMPONENT,
cs.LABEL_COMPONENT,
"id",
component_id,
)
def link_exposes(self, component_id: str, api_contract_id: str) -> None:
self._edge(
cs.LABEL_COMPONENT,
"id",
component_id,
cs.REL_EXPOSES,
cs.LABEL_API_CONTRACT,
"id",
api_contract_id,
)
def link_data_flows_to(
self,
from_component_id: str,
to_component_id: str,
*,
protocol: str = "",
sync: bool | None = None,
critical_path: bool | None = None,
ordering: str = "",
via: str = "",
) -> None:
self._edge(
cs.LABEL_COMPONENT,
"id",
from_component_id,
cs.REL_DATA_FLOWS_TO,
cs.LABEL_COMPONENT,
"id",
to_component_id,
{
"protocol": protocol,
"sync": sync,
"critical_path": critical_path,
"ordering": ordering,
"via": via,
},
)
def link_depends_on(
self, from_component_id: str, to_component_id: str, *, kind: str = ""
) -> None:
self._edge(
cs.LABEL_COMPONENT,
"id",
from_component_id,
cs.REL_DEPENDS_ON,
cs.LABEL_COMPONENT,
"id",
to_component_id,
{"kind": kind},
)
def link_persists_to(
self, component_id: str, data_entity_id: str, *, mode: str = ""
) -> None:
self._edge(
cs.LABEL_COMPONENT,
"id",
component_id,
cs.REL_PERSISTS_TO,
cs.LABEL_DATA_ENTITY,
"id",
data_entity_id,
{"mode": mode},
)
def link_relates_to(
self,
from_data_entity_id: str,
to_data_entity_id: str,
*,
cardinality: str = "",
via: str = "",
) -> None:
self._edge(
cs.LABEL_DATA_ENTITY,
"id",
from_data_entity_id,
cs.REL_RELATES_TO,
cs.LABEL_DATA_ENTITY,
"id",
to_data_entity_id,
{"cardinality": cardinality, "via": via},
)
def link_runs_on(self, component_id: str, technology_choice_id: str) -> None:
self._edge(
cs.LABEL_COMPONENT,
"id",
component_id,
cs.REL_RUNS_ON,
cs.LABEL_TECHNOLOGY_CHOICE,
"id",
technology_choice_id,
)
def link_has_scenario(self, requirement_item_id: str, scenario_id: str) -> None:
self._edge(
cs.LABEL_REQUIREMENT_ITEM,
"id",
requirement_item_id,
cs.REL_HAS_SCENARIO,
cs.LABEL_SCENARIO,
"id",
scenario_id,
)
def link_satisfies(self, component_id: str, requirement_item_id: str) -> None:
self._edge(
cs.LABEL_COMPONENT,
"id",
component_id,
cs.REL_SATISFIES,
cs.LABEL_REQUIREMENT_ITEM,
"id",
requirement_item_id,
)
def link_has_quality_attribute(
self, requirement_item_id: str, quality_attribute_id: str
) -> None:
self._edge(
cs.LABEL_REQUIREMENT_ITEM,
"id",
requirement_item_id,
cs.REL_HAS_QUALITY_ATTRIBUTE,
cs.LABEL_QUALITY_ATTRIBUTE,
"id",
quality_attribute_id,
)
def link_measured_on(self, quality_attribute_id: str, component_id: str) -> None:
self._edge(
cs.LABEL_QUALITY_ATTRIBUTE,
"id",
quality_attribute_id,
cs.REL_MEASURED_ON,
cs.LABEL_COMPONENT,
"id",
component_id,
)
def link_addresses_pillar(
self, quality_attribute_id: str, waf_pillar_name: str
) -> None:
self._edge(
cs.LABEL_QUALITY_ATTRIBUTE,
"id",
quality_attribute_id,
cs.REL_ADDRESSES_PILLAR,
cs.LABEL_WAF_PILLAR,
"name",
waf_pillar_name,
)
def link_must_follow(self, component_id: str, constraint_id: str) -> None:
self._edge(
cs.LABEL_COMPONENT,
"id",
component_id,
cs.REL_MUST_FOLLOW,
cs.LABEL_CONSTRAINT,
"id",
constraint_id,
)
def link_constrains(self, constraint_id: str, architecture_id: str) -> None:
self._edge(
cs.LABEL_CONSTRAINT,
"id",
constraint_id,
cs.REL_CONSTRAINS,
cs.LABEL_ARCHITECTURE,
"id",
architecture_id,
)
def link_modeled_by(self, architecture_id: str, capacity_model_id: str) -> None:
self._edge(
cs.LABEL_ARCHITECTURE,
"id",
architecture_id,
cs.REL_MODELED_BY,
cs.LABEL_CAPACITY_MODEL,
"id",
capacity_model_id,
)
def link_informs(self, capacity_model_id: str, cost_model_id: str) -> None:
self._edge(
cs.LABEL_CAPACITY_MODEL,
"id",
capacity_model_id,
cs.REL_INFORMS,
cs.LABEL_COST_MODEL,
"id",
cost_model_id,
)
def link_has_line_item(self, cost_model_id: str, cost_line_item_id: str) -> None:
self._edge(
cs.LABEL_COST_MODEL,
"id",
cost_model_id,
cs.REL_HAS_LINE_ITEM,
cs.LABEL_COST_LINE_ITEM,
"id",
cost_line_item_id,
)
def link_meters(self, cost_line_item_id: str, cloud_service_name: str) -> None:
self._edge(
cs.LABEL_COST_LINE_ITEM,
"id",
cost_line_item_id,
cs.REL_METERS,
cs.LABEL_CLOUD_SERVICE,
"name",
cloud_service_name,
)
def link_decides(self, adr_id: str, target_label: str, target_id: str) -> None:
self._edge(
cs.LABEL_ARCHITECTURE_DECISION,
"id",
adr_id,
cs.REL_DECIDES,
target_label,
self._key_prop(target_label),
target_id,
)
def link_documents(self, artifact_id: str, architecture_id: str) -> None:
"""``(Artifact)-[:DOCUMENTS]->(Architecture)`` -- same pattern as
:meth:`link_decides`."""
self._edge(
cs.LABEL_ARTIFACT,
"id",
artifact_id,
cs.REL_DOCUMENTS,
cs.LABEL_ARCHITECTURE,
"id",
architecture_id,
)
def link_chose(self, adr_id: str, technology_choice_id: str) -> None:
self._edge(
cs.LABEL_ARCHITECTURE_DECISION,
"id",
adr_id,
cs.REL_CHOSE,
cs.LABEL_TECHNOLOGY_CHOICE,
"id",
technology_choice_id,
)
def link_rejected(
self, adr_id: str, target_label: str, target_id: str, *, reason: str = ""
) -> None:
self._edge(
cs.LABEL_ARCHITECTURE_DECISION,
"id",
adr_id,
cs.REL_REJECTED,
target_label,
self._key_prop(target_label),
target_id,
{"reason": reason},
)
def link_justified_by(self, adr_id: str, requirement_item_id: str) -> None:
self._edge(
cs.LABEL_ARCHITECTURE_DECISION,
"id",
adr_id,
cs.REL_JUSTIFIED_BY,
cs.LABEL_REQUIREMENT_ITEM,
"id",
requirement_item_id,
)
def link_weighs(self, adr_id: str, tradeoff_id: str) -> None:
self._edge(
cs.LABEL_ARCHITECTURE_DECISION,
"id",
adr_id,
cs.REL_WEIGHS,
cs.LABEL_TRADE_OFF,
"id",
tradeoff_id,
)
def link_supersedes(
self, new_adr_id: str, old_adr_id: str, *, date: str | None = None
) -> None:
"""The evolution path for an Accepted ADR (ruling 1): a NEW node
supersedes the old one, rather than editing the old one's body."""
self._edge(
cs.LABEL_ARCHITECTURE_DECISION,
"id",
new_adr_id,
cs.REL_SUPERSEDES,
cs.LABEL_ARCHITECTURE_DECISION,
"id",
old_adr_id,
{"date": date or _now_iso()},
)
def link_selects(self, technology_choice_id: str, cloud_service_name: str) -> None:
self._edge(
cs.LABEL_TECHNOLOGY_CHOICE,
"id",
technology_choice_id,
cs.REL_SELECTS,
cs.LABEL_CLOUD_SERVICE,
"name",
cloud_service_name,
)
def link_forced_by(
self, technology_choice_id: str, requirement_item_id: str
) -> None:
"""The "name the requirement that forces it" gate (§2 rule 6):
a TechnologyChoice with no FORCED_BY edge is unjustified."""
self._edge(
cs.LABEL_TECHNOLOGY_CHOICE,
"id",
technology_choice_id,
cs.REL_FORCED_BY,
cs.LABEL_REQUIREMENT_ITEM,
"id",
requirement_item_id,
)
def link_applies(
self, source_label: str, source_id: str, pattern_name: str
) -> None:
"""``(ArchitectureDecision|Component)-[:APPLIES]->(Pattern)``."""
self._edge(
source_label,
self._key_prop(source_label),
source_id,
cs.REL_APPLIES,
cs.LABEL_PATTERN,
"name",
pattern_name,
)
def link_implements(self, component_id: str, pattern_name: str) -> None:
self._edge(
cs.LABEL_COMPONENT,
"id",
component_id,
cs.REL_IMPLEMENTS,
cs.LABEL_PATTERN,
"name",
pattern_name,
)
def link_threatens(self, risk_id: str, target_label: str, target_id: str) -> None:
self._edge(
cs.LABEL_RISK,
"id",
risk_id,
cs.REL_THREATENS,
target_label,
self._key_prop(target_label),
target_id,
)
def link_affects(self, failure_mode_id: str, component_id: str) -> None:
self._edge(
cs.LABEL_FAILURE_MODE,
"id",
failure_mode_id,
cs.REL_AFFECTS,
cs.LABEL_COMPONENT,
"id",
component_id,
)
def link_mitigated_by(
self, source_label: str, source_id: str, target_label: str, target_id: str
) -> None:
"""``(Risk|FailureMode)-[:MITIGATED_BY]->(Pattern|Constraint)``."""
self._edge(
source_label,
self._key_prop(source_label),
source_id,
cs.REL_MITIGATED_BY,
target_label,
self._key_prop(target_label),
target_id,
)
def link_has_slo(self, component_id: str, slo_id: str) -> None:
self._edge(
cs.LABEL_COMPONENT,
"id",
component_id,
cs.REL_HAS_SLO,
cs.LABEL_SERVICE_LEVEL_OBJECTIVE,
"id",
slo_id,
)
def link_measures(self, slo_id: str, quality_attribute_id: str) -> None:
self._edge(
cs.LABEL_SERVICE_LEVEL_OBJECTIVE,
"id",
slo_id,
cs.REL_MEASURES,
cs.LABEL_QUALITY_ATTRIBUTE,
"id",
quality_attribute_id,
)
def link_alerts_on(self, alert_id: str, slo_id: str) -> None:
self._edge(
cs.LABEL_ALERT,
"id",
alert_id,
cs.REL_ALERTS_ON,
cs.LABEL_SERVICE_LEVEL_OBJECTIVE,
"id",
slo_id,
)
def link_deployed_as(self, component_id: str, deployment_id: str) -> None:
self._edge(
cs.LABEL_COMPONENT,
"id",
component_id,
cs.REL_DEPLOYED_AS,
cs.LABEL_DEPLOYMENT,
"id",
deployment_id,
)
def link_in_environment(self, deployment_id: str, environment_id: str) -> None:
self._edge(
cs.LABEL_DEPLOYMENT,
"id",
deployment_id,
cs.REL_IN_ENVIRONMENT,
cs.LABEL_ENVIRONMENT,
"id",
environment_id,
)