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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ glossary/catalog row가 유지됩니다.
- `GET /plane/catalog-objects/{catalog_object_id}` — 단건 조회
- `POST /plane/catalog-objects/{catalog_object_id}/document-kg-links` — 문서 KG 참조 연결
- `POST /plane/catalog-objects/{catalog_object_id}/concept-bindings` — 온톨로지 개념 연결
- `POST /plane/catalog-objects/{catalog_object_id}/data-owner-assignments` — 데이터 오너 지정 (evidence-backed)
- `POST /plane/catalog-objects/{catalog_object_id}/critical-data-elements` — CDE 등록
- `POST /plane/critical-data-elements/{critical_data_element_id}/quality-rules` — 품질 규칙 정의
- `POST /plane/quality-rules/{data_quality_rule_id}/observations` — 품질 observation 기록 (immutable)
- `GET /plane/catalog-objects/{catalog_object_id}/data-management-profile` — evidence 완결도 프로파일
- `GET /plane/query?q=...` — title/alias/concept/document-KG id 검색

Production 필수 헤더: `Authorization: Bearer <Keyverse access token>`,
Expand Down
3 changes: 3 additions & 0 deletions docs/implementation-compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,12 @@

### CAT-010 Ontology/catalog plane above the document KG (issue #13)
- 대응: `POST/GET /plane/catalog-objects`, `GET /plane/catalog-objects/{catalog_object_id}`, `POST .../document-kg-links`, `POST .../concept-bindings`, `GET /plane/query`
- 데이터 관리 증거 확장(issue #74): `POST .../data-owner-assignments`, `POST .../critical-data-elements`, `POST /plane/critical-data-elements/{id}/quality-rules`, `POST /plane/quality-rules/{id}/observations`, `GET .../data-management-profile`
- 핵심 코드:
- [src/sdp/catalog_plane.py](src/sdp/catalog_plane.py)
- [src/sdp/catalog_plane_store.py](src/sdp/catalog_plane_store.py)
- [src/sdp/data_management_evidence.py](src/sdp/data_management_evidence.py)
- [src/sdp/data_management_store.py](src/sdp/data_management_store.py)
- [src/sdp/tenant_binding.py](src/sdp/tenant_binding.py)
- [src/sdp_core/catalog_plane.py](src/sdp_core/catalog_plane.py)
- [migrations/0002_ontology_catalog_plane.sql](migrations/0002_ontology_catalog_plane.sql)
Expand Down
96 changes: 96 additions & 0 deletions migrations/0003_data_management_evidence.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
-- Framework-neutral data-management evidence profile (3NF, 2+ word snake_case).
--
-- This migration stores CWL-owned ownership, critical-element, quality-rule,
-- and observation facts. It deliberately does not reproduce licensed
-- DAMA-DMBOK or DCAM framework prose, questions, scoring, or evidence lists.

SET search_path = public;

CREATE TABLE IF NOT EXISTS data_owner_assignments (
data_owner_assignment_id TEXT PRIMARY KEY,
catalog_object_id TEXT NOT NULL REFERENCES catalog_objects (catalog_object_id),
tenant_reference TEXT NOT NULL,
owner_subject TEXT NOT NULL,
owner_display_name TEXT NOT NULL,
valid_from TIMESTAMPTZ NOT NULL,
valid_to TIMESTAMPTZ,
evidence_reference TEXT NOT NULL CHECK (evidence_reference LIKE 'https://%'),
truth_status TEXT NOT NULL
CHECK (truth_status IN ('authoritative', 'observed', 'inferred', 'proposed')),
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (catalog_object_id, owner_subject, valid_from),
CHECK (valid_to IS NULL OR valid_to > valid_from)
);
CREATE INDEX IF NOT EXISTS data_owner_assignments_object_idx
ON data_owner_assignments (tenant_reference, catalog_object_id);
CREATE INDEX IF NOT EXISTS data_owner_assignments_active_idx
ON data_owner_assignments (tenant_reference, catalog_object_id, valid_to);

CREATE TABLE IF NOT EXISTS critical_data_elements (
critical_data_element_id TEXT PRIMARY KEY,
catalog_object_id TEXT NOT NULL REFERENCES catalog_objects (catalog_object_id),
tenant_reference TEXT NOT NULL,
element_key TEXT NOT NULL,
display_name TEXT NOT NULL,
definition_text TEXT NOT NULL,
data_classification TEXT NOT NULL
CHECK (data_classification IN (
'public', 'internal', 'confidential', 'restricted_pii', 'restricted_financial'
)),
evidence_reference TEXT NOT NULL CHECK (evidence_reference LIKE 'https://%'),
truth_status TEXT NOT NULL
CHECK (truth_status IN ('authoritative', 'observed', 'inferred', 'proposed')),
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (catalog_object_id, element_key)
);
CREATE INDEX IF NOT EXISTS critical_data_elements_object_idx
ON critical_data_elements (tenant_reference, catalog_object_id);

CREATE TABLE IF NOT EXISTS data_quality_rules (
data_quality_rule_id TEXT PRIMARY KEY,
critical_data_element_id TEXT NOT NULL
REFERENCES critical_data_elements (critical_data_element_id),
catalog_object_id TEXT NOT NULL REFERENCES catalog_objects (catalog_object_id),
tenant_reference TEXT NOT NULL,
rule_code TEXT NOT NULL,
rule_description TEXT NOT NULL,
metric_code TEXT NOT NULL,
threshold_operator TEXT NOT NULL,
threshold_value NUMERIC NOT NULL,
unit_code TEXT NOT NULL,
evidence_reference TEXT NOT NULL CHECK (evidence_reference LIKE 'https://%'),
truth_status TEXT NOT NULL
CHECK (truth_status IN ('authoritative', 'observed', 'inferred', 'proposed')),
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (critical_data_element_id, rule_code)
);
CREATE INDEX IF NOT EXISTS data_quality_rules_element_idx
ON data_quality_rules (tenant_reference, critical_data_element_id);
CREATE INDEX IF NOT EXISTS data_quality_rules_object_idx
ON data_quality_rules (tenant_reference, catalog_object_id);

CREATE TABLE IF NOT EXISTS data_quality_observations (
data_quality_observation_id TEXT PRIMARY KEY,
data_quality_rule_id TEXT NOT NULL REFERENCES data_quality_rules (data_quality_rule_id),
critical_data_element_id TEXT NOT NULL
REFERENCES critical_data_elements (critical_data_element_id),
catalog_object_id TEXT NOT NULL REFERENCES catalog_objects (catalog_object_id),
tenant_reference TEXT NOT NULL,
source_observation_id TEXT NOT NULL,
observed_value NUMERIC NOT NULL,
observed_at TIMESTAMPTZ NOT NULL,
quality_status TEXT NOT NULL,
evidence_reference TEXT NOT NULL CHECK (evidence_reference LIKE 'https://%'),
truth_status TEXT NOT NULL
CHECK (truth_status IN ('authoritative', 'observed', 'inferred', 'proposed')),
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (data_quality_rule_id, source_observation_id)
);
CREATE INDEX IF NOT EXISTS data_quality_observations_rule_idx
ON data_quality_observations (tenant_reference, data_quality_rule_id, observed_at);
CREATE INDEX IF NOT EXISTS data_quality_observations_object_idx
ON data_quality_observations (tenant_reference, catalog_object_id, observed_at);

INSERT INTO schema_migrations (migration_id)
VALUES ('0003_data_management_evidence')
ON CONFLICT (migration_id) DO NOTHING;
107 changes: 107 additions & 0 deletions src/sdp/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
enterprise_rbac_matrix,
enterprise_readiness_manifest,
)
from sdp_core.data_management_evidence import (
CriticalDataElementDraft,
DataOwnerAssignmentDraft,
DataQualityObservationDraft,
DataQualityRuleDraft,
)
from sdp_core.catalog_plane import (
CatalogObjectCreateRequest,
ConceptBindingDraft,
Expand All @@ -33,6 +39,13 @@
list_catalog_objects,
query_catalog_objects,
)
from .data_management_evidence import (
assign_data_owner,
build_data_management_profile,
define_data_quality_rule,
record_data_quality_observation,
register_critical_data_element,
)
from .config import get_app_config
from .console import render_enterprise_console
from .tenant_binding import TenantBindingError, bind_keyverse_tenant
Expand Down Expand Up @@ -826,6 +839,100 @@ def plane_query_catalog_objects(
raise HTTPException(status_code=400, detail=str(exc)) from exc


@app.post("/plane/catalog-objects/{catalog_object_id}/data-owner-assignments")
def plane_assign_data_owner(
catalog_object_id: str,
payload: DataOwnerAssignmentDraft,
request: Request,
) -> dict[str, Any]:
"""Assign an evidence-backed data owner to one catalog object."""

Comment thread
seonghobae marked this conversation as resolved.
actor = _plane_actor(request)
try:
return assign_data_owner(actor, catalog_object_id, payload).model_dump()
except PermissionError as exc:
raise HTTPException(status_code=403, detail=str(exc)) from exc
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc


@app.post("/plane/catalog-objects/{catalog_object_id}/critical-data-elements")
def plane_register_critical_data_element(
catalog_object_id: str,
payload: CriticalDataElementDraft,
request: Request,
) -> dict[str, Any]:
"""Register an evidence-backed CDE under a catalog dataset."""

actor = _plane_actor(request)
try:
return register_critical_data_element(actor, catalog_object_id, payload).model_dump()
except PermissionError as exc:
raise HTTPException(status_code=403, detail=str(exc)) from exc
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc


@app.post("/plane/critical-data-elements/{critical_data_element_id}/quality-rules")
def plane_define_data_quality_rule(
critical_data_element_id: str,
payload: DataQualityRuleDraft,
request: Request,
) -> dict[str, Any]:
"""Define one evidence-backed quality rule for a CDE."""

actor = _plane_actor(request)
try:
return define_data_quality_rule(actor, critical_data_element_id, payload).model_dump()
except PermissionError as exc:
raise HTTPException(status_code=403, detail=str(exc)) from exc
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc


@app.post("/plane/quality-rules/{data_quality_rule_id}/observations")
def plane_record_data_quality_observation(
data_quality_rule_id: str,
payload: DataQualityObservationDraft,
request: Request,
) -> dict[str, Any]:
"""Append one immutable evidence-backed quality observation."""

actor = _plane_actor(request)
try:
return record_data_quality_observation(actor, data_quality_rule_id, payload).model_dump()
except PermissionError as exc:
raise HTTPException(status_code=403, detail=str(exc)) from exc
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc


@app.get("/plane/catalog-objects/{catalog_object_id}/data-management-profile")
def plane_data_management_profile(
catalog_object_id: str,
request: Request,
) -> dict[str, Any]:
"""Return the explainable evidence-completeness profile."""

actor = _plane_actor(request)
try:
return build_data_management_profile(actor, catalog_object_id).model_dump()
except PermissionError as exc:
raise HTTPException(status_code=403, detail=str(exc)) from exc
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc


@app.get("/ontology/term/{term}/graph")
def ontology_term_graph(term: str) -> dict[str, Any]:
# Backed by the persistent graph store now; falls back to the legacy
Expand Down
Loading