-
Notifications
You must be signed in to change notification settings - Fork 1
feat(journey): admit digest-bound temporal evidence #791
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -133,8 +133,31 @@ async def fetch(self, query: str, *args: object) -> Sequence[Mapping[str, Any]]: | |
| order by role.post_id, role.actor_type_code, role.actor_name, role.responsibility | ||
| """ | ||
| _EDGE_SQL = """ | ||
| select edge.parent_post_id, edge.child_post_id, edge.fused_score | ||
| select edge.parent_post_id, edge.child_post_id, edge.fused_score, | ||
| temporal.observed as temporal_observed, | ||
| temporal.allen_relations, | ||
| temporal.artifact_digest_sha256 | ||
| from post_lineage_edge edge | ||
| left join lateral ( | ||
| select relation.observed, | ||
| array_agg(kind.relation_code order by kind.relation_ordinal) as allen_relations, | ||
| artifact.artifact_digest_sha256 | ||
| from project_journey_temporal_relation relation | ||
| join project_journey_temporal_artifact artifact | ||
| on artifact.analysis_run_id = relation.analysis_run_id | ||
| join analysis_run temporal_run | ||
| on temporal_run.analysis_run_id = artifact.analysis_run_id | ||
| join project_journey_temporal_relation_kind kind | ||
| on kind.analysis_run_id = relation.analysis_run_id | ||
| and kind.left_post_id = relation.left_post_id | ||
| and kind.right_post_id = relation.right_post_id | ||
| where relation.left_post_id = edge.parent_post_id | ||
| and relation.right_post_id = edge.child_post_id | ||
| and temporal_run.knowledge_cutoff <= $2 | ||
| group by relation.observed, artifact.artifact_digest_sha256, artifact.admitted_at | ||
| order by artifact.admitted_at desc, artifact.artifact_digest_sha256 desc | ||
| limit 1 | ||
| ) temporal on true | ||
|
Comment on lines
+141
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Temporal evidence not bound to view snapshot The lateral join matches temporal relations only by edge endpoints and Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| where edge.parent_post_id = any($1::uuid[]) | ||
| and edge.child_post_id = any($1::uuid[]) | ||
| order by edge.child_post_id, edge.parent_post_id | ||
|
|
@@ -219,6 +242,7 @@ async def fetch_project_history_projection( | |
| conn, | ||
| visible_ids=visible_ids, | ||
| normalized_key=normalized_key, | ||
| knowledge_cutoff=knowledge_cutoff, | ||
| ) | ||
| return build_project_history_projection( | ||
| project_key=project_key, | ||
|
|
@@ -237,10 +261,11 @@ async def _fetch_project_children( | |
| *, | ||
| visible_ids: Sequence[str], | ||
| normalized_key: str, | ||
| knowledge_cutoff: datetime, | ||
| ) -> tuple[list[Mapping[str, Any]], list[Mapping[str, Any]], list[Mapping[str, Any]]]: | ||
| """Fetch only child evidence whose endpoints are already authorized.""" | ||
|
|
||
| matches = list(await conn.fetch(_MATCH_SQL, list(visible_ids), normalized_key)) | ||
| roles = list(await conn.fetch(_ROLE_SQL, list(visible_ids))) | ||
| edges = list(await conn.fetch(_EDGE_SQL, list(visible_ids))) | ||
| edges = list(await conn.fetch(_EDGE_SQL, list(visible_ids), knowledge_cutoff)) | ||
| return matches, roles, edges | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| """Persist provider-owned temporal evidence for already-admitted journey edges.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any, Protocol | ||
|
|
||
| from lineageweave.temporal_journey_artifact import ( | ||
| ALLEN_RELATIONS, | ||
| TemporalJourneyArtifact, | ||
| parse_temporal_journey_artifact, | ||
| ) | ||
|
|
||
|
|
||
| class TemporalArtifactConnection(Protocol): | ||
| """Minimal transaction-scoped database port for artifact admission.""" | ||
|
|
||
| async def fetchrow(self, query: str, *args: object) -> Any: | ||
| """Read one binding row.""" | ||
|
|
||
| ... | ||
|
|
||
| async def execute(self, query: str, *args: object) -> Any: | ||
| """Execute one immutable persistence statement.""" | ||
|
|
||
| ... | ||
|
|
||
| async def executemany(self, query: str, args: list[tuple[object, ...]]) -> Any: | ||
| """Execute bounded normalized child inserts.""" | ||
|
|
||
| ... | ||
|
|
||
|
|
||
| class TemporalArtifactAdmissionError(ValueError): | ||
| """The artifact cannot be bound to the declared persisted run.""" | ||
|
|
||
|
|
||
| async def persist_project_journey_temporal_artifact( | ||
| conn: TemporalArtifactConnection, | ||
| *, | ||
| analysis_run_id: str, | ||
| payload: bytes, | ||
| expected_run_id: str, | ||
| expected_snapshot_id: str, | ||
| expected_input_digest_sha256: str, | ||
| expected_artifact_digest_sha256: str, | ||
| ) -> TemporalJourneyArtifact: | ||
| """Validate and immutably persist temporal evidence for existing edges. | ||
|
|
||
| The foreign key to ``post_lineage_edge`` is the semantic admission gate: | ||
| interval order can corroborate an admitted predecessor, but cannot create | ||
| a predecessor, branch, responsibility handoff, or causal transition. | ||
| """ | ||
|
|
||
| artifact = parse_temporal_journey_artifact( | ||
| payload, | ||
| expected_run_id=expected_run_id, | ||
| expected_snapshot_id=expected_snapshot_id, | ||
| expected_input_digest_sha256=expected_input_digest_sha256, | ||
| expected_artifact_digest_sha256=expected_artifact_digest_sha256, | ||
| ) | ||
| binding = await conn.fetchrow( | ||
| "select remote_run_id from analysis_run_tepp_result where analysis_run_id = $1::uuid", | ||
| analysis_run_id, | ||
| ) | ||
| if binding is None or str(binding["remote_run_id"]) != expected_run_id: | ||
| raise TemporalArtifactAdmissionError("artifact run does not match a persisted terminal result") | ||
| existing = await conn.fetchrow( | ||
| "select artifact_digest_sha256 from project_journey_temporal_artifact " | ||
| "where analysis_run_id = $1::uuid for update", | ||
| analysis_run_id, | ||
| ) | ||
| if existing is not None: | ||
| if str(existing["artifact_digest_sha256"]) != expected_artifact_digest_sha256: | ||
| raise TemporalArtifactAdmissionError("analysis run already has a different artifact") | ||
| return artifact | ||
| await conn.execute( | ||
| "insert into project_journey_temporal_artifact " | ||
| "(analysis_run_id, remote_run_id, schema_version, snapshot_id, input_digest_sha256, artifact_digest_sha256) " | ||
| "values ($1::uuid, $2, $3, $4, $5, $6)", | ||
| analysis_run_id, | ||
| expected_run_id, | ||
| "tepp.tdt_chronos_interval_consistency.v1", | ||
| expected_snapshot_id, | ||
| expected_input_digest_sha256, | ||
| expected_artifact_digest_sha256, | ||
| ) | ||
|
Comment on lines
+67
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: FOR UPDATE lock depends on caller's transaction persist_project_journey_temporal_artifact runs Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| relation_rows = [ | ||
| (analysis_run_id, relation.left_event_id, relation.right_event_id, relation.observed) | ||
| for relation in artifact.relations | ||
| ] | ||
| await conn.executemany( | ||
| "insert into project_journey_temporal_relation " | ||
| "(analysis_run_id, left_post_id, right_post_id, observed) " | ||
| "values ($1::uuid, $2::uuid, $3::uuid, $4)", | ||
| relation_rows, | ||
| ) | ||
| await conn.executemany( | ||
| "insert into project_journey_temporal_relation_kind " | ||
| "(analysis_run_id, left_post_id, right_post_id, relation_code, relation_ordinal) " | ||
| "values ($1::uuid, $2::uuid, $3::uuid, $4, $5)", | ||
| [ | ||
| (analysis_run_id, relation.left_event_id, relation.right_event_id, code, ALLEN_RELATIONS.index(code)) | ||
| for relation in artifact.relations | ||
| for code in relation.allen_relations | ||
| ], | ||
| ) | ||
| await conn.executemany( | ||
| "insert into project_journey_temporal_support " | ||
| "(analysis_run_id, left_post_id, right_post_id, assertion_ordinal) " | ||
| "values ($1::uuid, $2::uuid, $3::uuid, $4)", | ||
| [ | ||
| (analysis_run_id, relation.left_event_id, relation.right_event_id, ordinal) | ||
| for relation in artifact.relations | ||
| for ordinal in relation.support_assertion_ordinals | ||
| ], | ||
| ) | ||
| return artifact | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # ADR 0270: Digest-bound project-journey temporal evidence | ||
|
|
||
| - Status: Accepted on this stacked branch; not protected-main truth until merge | ||
| - Date: 2026-08-28 | ||
| - Depends on: ADR 0132, ADR 0231, ADR 0243; TEPP PR #291 | ||
| - Figma file ID: `SBpgot7uTvMxEaxUwvoc0S` | ||
|
|
||
| ## Context | ||
|
|
||
| TEPP PR #291 publishes canonical JSON and GraphML for bounded Allen interval- | ||
| consistency results. The artifact binds a run, snapshot, exact input digest, | ||
| ordered event pair, observed/derived status, and supporting assertion ordinals. | ||
| It deliberately does not claim that temporal order is a causal transition, | ||
| project predecessor, or business-process branch. | ||
|
|
||
| LineageWeave already admits related predecessor paths through authorized | ||
| `post_lineage_edge` evidence. Promoting every temporally ordered pair to a | ||
| project journey would contradict PRD-FR-5E and ADR 0243. | ||
|
|
||
| ## Decision | ||
|
|
||
| LineageWeave accepts only canonical artifact bytes whose SHA-256, run, | ||
| snapshot, and exact input digest match caller-computed expected values. The | ||
| remote run must also match a persisted terminal TEPP result. Metadata, | ||
| relations, elementary Allen kinds, and support ordinals persist in normalized | ||
| tables. | ||
|
|
||
| Every admitted temporal pair must already be an exact `post_lineage_edge`. | ||
| The database foreign key enforces that boundary. Temporal evidence may | ||
| corroborate the time order of an existing related-history path; it never | ||
| creates a predecessor, branch, responsibility handoff, or causal transition. | ||
| A branch is visible only when the independently admitted lineage graph already | ||
| contains that topology. A transition still requires its separately governed | ||
| observed business or responsibility evidence. | ||
|
|
||
| The Project History API attaches the newest immutable temporal evidence whose | ||
| analysis cutoff does not exceed the requested view cutoff to the corresponding | ||
| visible edge after ABAC selects both endpoints. The customer UI says what the user can do next—open the supporting | ||
| records and compare dates—and never names the calculation module. | ||
|
|
||
| GraphML is an equivalent provider export, not the ingestion authority. The | ||
| canonical typed JSON is the sole admitted payload so two representations | ||
| cannot diverge inside the database. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Exact temporal consistency becomes durable and auditable without duplicating | ||
| mathematical reasoning in Python. | ||
| - A valid artifact containing a pair absent from Event Lineage fails closed at | ||
| the foreign-key boundary and rolls back its transaction. | ||
| - A future contract that explicitly carries business predecessor or transition | ||
| semantics requires a new ADR; this artifact cannot be reinterpreted later. | ||
|
|
||
| ## References | ||
|
|
||
| Allen, J. F. (1983). Maintaining knowledge about temporal intervals. | ||
| *Communications of the ACM, 26*(11), 832–843. | ||
| https://doi.org/10.1145/182.358434 | ||
|
|
||
| World Wide Web Consortium. (2013). *PROV-O: The PROV ontology*. | ||
| https://www.w3.org/TR/prov-o/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Newest temporal artifact selected by admitted_at
The lateral subquery in _EDGE_SQL ranks candidate artifacts by
admitted_at descthen digest, taking one. When two runs cover the same edge, the most recently admitted wins even if another has a later analysis cutoff. Confirm this matches the intended "newest" semantics in ADR 0270.Was this helpful? React with 👍 or 👎 to provide feedback.