diff --git a/CHANGELOG.md b/CHANGELOG.md index 3349f4e07..196799391 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,8 @@ All notable changes to this project are documented here. Format follows - Planned-facility evidence now reuses the semantic-relationship channel with `lw_plans_to_operate` (ADR 0142). The relationship is retained only when the same source span names a matching R&R actor and project-backed facility; it - never represents an already-operating facility. + never represents an already-operating facility. Migration 0138 keeps the + database write constraint aligned with the closed predicate vocabulary. - Event Lineage now reports why a post has no DAG (ADR 0143): "no_relation_found" when reconstruct compared it against real candidates and found no relation, or "no_comparison_group" when it was diff --git a/lineageweave/post_summary.py b/lineageweave/post_summary.py index 9ece420b1..226e3dcd9 100644 --- a/lineageweave/post_summary.py +++ b/lineageweave/post_summary.py @@ -1848,12 +1848,17 @@ def _optional_text(*keys: str, _entry: dict[str, object] = entry) -> str | None: for entry in raw_relationships: if not isinstance(entry, dict): continue + predicate_code = str(entry.get("predicate_code", "")).strip() + # The legacy JSON contract has no source text to re-check against + # ADR 0142, so it cannot safely admit a planned-facility claim. + if predicate_code == "lw_plans_to_operate": + continue try: semantic_relationships.append( SemanticRelationship( subject_name=str(entry.get("subject_name", "")).strip(), subject_type=str(entry.get("subject_type", "")).strip().casefold(), - predicate_code=str(entry.get("predicate_code", "")).strip(), + predicate_code=predicate_code, object_name=str(entry.get("object_name", "")).strip(), object_type=str(entry.get("object_type", "")).strip().casefold(), evidence_text=str(entry.get("evidence_text", "")).strip(), diff --git a/tests/test_post_summary.py b/tests/test_post_summary.py index e8726bb77..eec3e9477 100644 --- a/tests/test_post_summary.py +++ b/tests/test_post_summary.py @@ -155,6 +155,20 @@ def test_parses_allow_list_semantic_relationships_with_evidence() -> None: ) +def test_legacy_json_parser_drops_planned_facility_relation_without_source_context() -> None: + summary = parse_summary_response( + '{"korean_summary":"요약", "semantic_relationships":[' + '{"subject_name":"Synthetic Utility", "subject_type":"organization", ' + '"predicate_code":"lw_plans_to_operate", ' + '"object_name":"Aurora Charging Hub", "object_type":"industrial_asset", ' + '"evidence_text":"Synthetic Utility plans to operate Aurora Charging Hub", ' + '"confidence":0.93}]}' + ) + + assert summary is not None + assert summary.semantic_relationships == () + + def test_parses_standard_profile_relation_for_industrial_why_path() -> None: summary = parse_summary_response( '{"korean_summary":"요약", "key_events":[], "semantic_relationships":[' diff --git a/tests/test_schema.py b/tests/test_schema.py index 65ae38196..0f4ff9a36 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -96,6 +96,11 @@ / "migrations" / "0114_semantic_relationship_standard_predicates.sql" ) +_PLANNED_FACILITY_RELATIONSHIP_PREDICATE_MIGRATION = ( + Path(__file__).resolve().parents[1] + / "migrations" + / "0138_planned_facility_relation_predicate.sql" +) def _postgres_available() -> bool: @@ -149,6 +154,11 @@ def schema_db(): cur.execute(_EVENT_CLUE_MIGRATION.read_text()) cur.execute(_BROAD_FACT_TYPES_MIGRATION.read_text()) cur.execute(_SEMANTIC_RELATIONSHIP_PREDICATES_MIGRATION.read_text()) + planned_predicate_migration = ( + _PLANNED_FACILITY_RELATIONSHIP_PREDICATE_MIGRATION.read_text() + ) + cur.execute(planned_predicate_migration) + cur.execute(planned_predicate_migration) conn.commit() yield conn finally: @@ -207,6 +217,66 @@ def test_migration_applies_cleanly(schema_db) -> None: assert expected <= tables +def test_planned_facility_relationship_predicate_persists(schema_db) -> None: + """Migration 0138 must admit the source-backed ADR 0142 relationship.""" + with schema_db.cursor() as cur: + cur.execute( + """ + insert into common_lookup_value ( + lookup_category, lookup_code, lookup_label + ) values + ('corporate_entity_level', 'company', 'Company'), + ('voc_type', 'voc', 'Voice of Customer'), + ('post_visibility', 'public', 'Public') + """ + ) + cur.execute( + """ + with synthetic_entity as ( + insert into corporate_entity ( + corporate_entity_code, entity_name, entity_level_code + ) values ('SYNTHETIC-PLAN-ORG', 'Synthetic Planning Org', 'company') + returning corporate_entity_id + ), synthetic_account as ( + insert into user_account ( + external_subject_id, display_name, email_address + ) values ( + 'synthetic-plan-account', 'Synthetic Planner', + 'synthetic.planner@example.test' + ) returning user_account_id + ), synthetic_post as ( + insert into source_post ( + author_account_id, corporate_entity_id, post_title, post_body, + voc_type_code, visibility_code + ) + select user_account_id, corporate_entity_id, + 'Synthetic facility plan', + 'Synthetic Planning Org plans to operate Aurora Charging Hub', + 'voc', 'public' + from synthetic_account cross join synthetic_entity + returning post_id + ), synthetic_summary as ( + insert into post_summary_result (post_id, korean_summary) + select post_id, '합성 계획 요약입니다.' from synthetic_post + returning post_id + ) + insert into post_summary_semantic_relationship ( + post_id, relation_ordinal, subject_name, subject_type, + predicate_code, object_name, object_type, evidence_text, + relation_confidence + ) + select post_id, 0, 'Synthetic Planning Org', 'organization', + 'lw_plans_to_operate', 'Aurora Charging Hub', + 'industrial_asset', + 'Synthetic Planning Org plans to operate Aurora Charging Hub', + 0.93 + from synthetic_summary + returning predicate_code + """ + ) + assert cur.fetchone() == ("lw_plans_to_operate",) + + def test_major_event_action_project_reference_is_normalized(schema_db) -> None: with schema_db.cursor() as cur: cur.execute(