Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion CHANGELOG.d/2.16.1-planned-facility-relation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

- Add the fail-closed `lw_plans_to_operate` semantic relationship for planned
facilities backed by the same literal actor/facility evidence, a matching
R&R actor, and an independently extracted project mention (ADR 0142).
R&R actor, and an independently extracted project mention; migration 0138
adds the predicate to the database write constraint (ADR 0142).
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docker/postgres-init/migrate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ for migration in /opt/lineageweave/migrations/*.sql; do
migration_name=${migration##*/}
case "$migration_name" in
0012_*|0013_*|0014_*|0015_*|0016_*|0017_*|0018_*|0019_*|0020_*|0021_*|0022_*|0023_*|0024_*|0025_*|0026_*|0027_*|0028_*|0029_*|0030_*|0031_*|0032_*|0033_*|0034_*|0035_*|0036_*|0037_*|0038_*|0039_*|0040_*|0041_*|0042_*|0043_*|0044_*|0045_*|0046_*|0047_*|0048_*|0049_*|0050_*) ;;
0060_*|0100_*|0101_*|0102_*|0103_*|0104_*|0105_*|0106_*|0107_*|0108_*|0109_*|0110_*|0111_*|0112_*|0113_*|0114_*|0130_*|0132_*|0133_*|0134_*|0136_*|0137_*) ;;
0060_*|0100_*|0101_*|0102_*|0103_*|0104_*|0105_*|0106_*|0107_*|0108_*|0109_*|0110_*|0111_*|0112_*|0113_*|0114_*|0130_*|0132_*|0133_*|0134_*|0136_*|0137_*|0138_*) ;;
*) continue ;;
esac
printf 'Applying %s\n' "$migration_name"
Expand Down
13 changes: 6 additions & 7 deletions docs/adr/0142-planned-facility-project-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,12 @@ governs when the extractor may emit it.
R&R actor, project backing, facility type, and literal evidence span before
retaining a model-emitted `lw_plans_to_operate` row. Missing evidence drops
only that relationship; other supported semantic relationships remain.
- Because the predicate is additive to an existing closed vocabulary and
channel, no migration is needed beyond the ontology TTL update and the
Python frozenset — `post_summary_semantic_relationship`'s schema already
supports an arbitrary `predicate_code` string with a foreign-key-style
check against the ontology annotation lookup at read time
(`semantic_predicate_annotations`), not at write time, so no schema
migration accompanies this ADR.
- Because the table enforces the closed predicate vocabulary at write time,
migration 0138 expands
`post_summary_semantic_relationship_predicate_code_check` with
`lw_plans_to_operate`. The Python frozenset, database constraint, and
ontology annotation lookup therefore reject vocabulary drift at their
respective parse, persistence, and reader boundaries.
- A downside accepted here: `lw_plans_to_operate` is a LineageWeave-local
(`lw_*`) predicate, not a borrowed PROV-O/SKOS/ODRL term, because none of
the already-adopted standard vocabularies (PROV-O, SKOS, DCT, SOSA, ODRL)
Expand Down
7 changes: 6 additions & 1 deletion lineageweave/post_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
seonghobae marked this conversation as resolved.
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(),
Expand Down
30 changes: 30 additions & 0 deletions migrations/0138_planned_facility_relationship_predicate.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- ADR 0142: admit the explicit planned-facility predicate at write time.
-- Rebuild the closed check from ADR 0129 and add only lw_plans_to_operate.

alter table post_summary_semantic_relationship
drop constraint if exists post_summary_semantic_relationship_predicate_code_check;

alter table post_summary_semantic_relationship
add constraint post_summary_semantic_relationship_predicate_code_check check
(predicate_code in (
'org_member_of', 'org_unit_of', 'org_reports_to',
'org_has_membership', 'org_role', 'org_organization',
'org_member_during', 'org_head_of', 'org_suborganization_of',
'skos_broader', 'skos_related', 'prov_was_derived_from',
'prov_used', 'prov_was_generated_by', 'prov_was_attributed_to',
'prov_was_associated_with', 'prov_acted_on_behalf_of',
'prov_had_primary_source', 'prov_was_influenced_by',
'prov_specialization_of', 'prov_alternate_of', 'prov_had_member',
'time_has_time', 'time_before', 'time_after',
'time_interval_during', 'sosa_has_result',
'sosa_observed_property', 'sosa_phenomenon_time',
'sosa_has_feature_of_interest', 'odrl_target', 'odrl_action',
'odrl_constraint', 'odrl_duty', 'odrl_permission',
'odrl_prohibition', 'dct_references', 'dct_provenance',
'dct_conforms_to', 'lw_observes_event', 'lw_clue_for',
'lw_clue_supports', 'lw_has_cause', 'lw_has_goal',
'lw_has_consequence', 'lw_has_next_step', 'lw_has_time',
'lw_at_place', 'lw_has_actor', 'lw_has_result',
'lw_has_condition', 'lw_inferred_from', 'lw_plans_to_operate',
'lw_responsible_for', 'lw_supports'
));
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- Roll back ADR 0142's predicate while preserving the ADR 0129 vocabulary.

alter table post_summary_semantic_relationship
drop constraint if exists post_summary_semantic_relationship_predicate_code_check;

alter table post_summary_semantic_relationship
add constraint post_summary_semantic_relationship_predicate_code_check check
(predicate_code in (
'org_member_of', 'org_unit_of', 'org_reports_to',
'org_has_membership', 'org_role', 'org_organization',
'org_member_during', 'org_head_of', 'org_suborganization_of',
'skos_broader', 'skos_related', 'prov_was_derived_from',
'prov_used', 'prov_was_generated_by', 'prov_was_attributed_to',
'prov_was_associated_with', 'prov_acted_on_behalf_of',
'prov_had_primary_source', 'prov_was_influenced_by',
'prov_specialization_of', 'prov_alternate_of', 'prov_had_member',
'time_has_time', 'time_before', 'time_after',
'time_interval_during', 'sosa_has_result',
'sosa_observed_property', 'sosa_phenomenon_time',
'sosa_has_feature_of_interest', 'odrl_target', 'odrl_action',
'odrl_constraint', 'odrl_duty', 'odrl_permission',
'odrl_prohibition', 'dct_references', 'dct_provenance',
'dct_conforms_to', 'lw_observes_event', 'lw_clue_for',
'lw_clue_supports', 'lw_has_cause', 'lw_has_goal',
'lw_has_consequence', 'lw_has_next_step', 'lw_has_time',
'lw_at_place', 'lw_has_actor', 'lw_has_result',
'lw_has_condition', 'lw_inferred_from', 'lw_responsible_for',
'lw_supports'
));
15 changes: 15 additions & 0 deletions tests/test_migration_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,21 @@ def test_migrate_sh_replays_cross_post_customer_identity_on_existing_volumes() -
assert "UNIQUE NULLS NOT DISTINCT (source_system_code, source_customer_code)" in migration


def test_migrate_sh_replays_planned_facility_predicate_on_existing_volumes() -> None:
"""Existing Compose volumes must accept the ADR 0142 relationship code."""
root = Path(__file__).resolve().parents[1]
script = (root / "docker" / "postgres-init" / "migrate.sh").read_text(
encoding="utf-8"
)
migration = (
root / "migrations" / "0138_planned_facility_relationship_predicate.sql"
).read_text(encoding="utf-8")

assert "0138_*" in script
assert "post_summary_semantic_relationship_predicate_code_check" in migration
assert "'lw_plans_to_operate'" in migration


def test_tenant_identity_migration_repairs_legacy_values_before_constraints() -> None:
"""Legacy blank settings cannot make an existing-volume replay fail closed."""
migration = (
Expand Down
14 changes: 14 additions & 0 deletions tests/test_post_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":['
Expand Down
70 changes: 70 additions & 0 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_relationship_predicate.sql"
)


def _postgres_available() -> bool:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down