-
Notifications
You must be signed in to change notification settings - Fork 1
feat(ontology): promote temporal primary Voice history onto main (ADR 0252) #761
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
17b25b3
09838e1
fea73e1
cc3dfc1
dce623a
8b8a9be
461a4d1
118cc38
771a8ed
182f5b8
0a8ce31
b0f6ca4
684df3d
e7816ce
90b3999
52bb01b
e3ecb3b
edc0ab9
991753d
d206fd8
9db158c
9c0a3e1
850494c
021cc75
f0d072a
e4d72e4
48af574
2644fbb
3ad9d74
c6a4c26
34f8d10
55a5b27
bf35587
11b7fff
4aab6e5
bb2eec1
d738bd0
dde0a83
3e18e7c
c731706
3db9c44
ebb4ef1
d5fe482
b2e0c96
3e332fd
201ac4e
7a259a4
c293323
11fc2fa
ad6dc5d
474dfdf
4b48c55
0ff51f8
f778a01
b0eedab
d0f6e7e
123fdb7
05f5bf6
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 |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # ADR 0252: Temporal history for imported primary Voice | ||
|
|
||
| ## Status | ||
|
|
||
| Accepted (2026-08-27). Extends ADR 0251 and closes issue #748. | ||
|
|
||
| ## Context | ||
|
|
||
| ADR 0251 records when a Voice assignment starts, but migration 0237 deletes | ||
| the former imported primary when `source_post.voc_type_code` changes. The live | ||
| value is honest, yet an authorized knowledge-cutoff read after that update can | ||
| no longer recover the primary that was effective at the cutoff. The existing | ||
| `(post_id, voice_type_code)` key also cannot represent A → B → A. | ||
|
|
||
| OWL-Time distinguishes instants from intervals and gives an interval explicit | ||
| beginning and end bounds. PostgreSQL range types and exclusion constraints are | ||
| the native database mechanism for rejecting overlapping periods. Neither | ||
| source supplies a missing business-effective instant, so LineageWeave must not | ||
| invent one: an imported change becomes effective at the database transaction | ||
| instant when no source change instant exists. | ||
|
|
||
| ## Decision | ||
|
|
||
| - Keep `source_post_voice` as the normalized assignment relation. Add nullable | ||
| `effective_to`; each row is a half-open interval | ||
| `[effective_from, effective_to)`. Null means current. | ||
| - Change the key to `(post_id, voice_type_code, effective_from)`, allowing the | ||
| same atomic Voice to recur in non-overlapping periods. | ||
| - Use PostgreSQL GiST exclusion constraints to reject overlapping primary | ||
| intervals for one Post. A partial unique index also permits at most one | ||
| current row for a `(post_id, voice_type_code)` pair. | ||
| - When the imported primary changes, one trigger transaction closes both the | ||
| current primary and any current additional assignment for the incoming | ||
| Voice, then inserts the new observed primary at one trigger-execution | ||
| timestamp. PostgreSQL `clock_timestamp()` is read after the source-row lock | ||
| is acquired, so a waiting concurrent update cannot backdate its interval to | ||
| the earlier statement start. It never overwrites or fabricates the former | ||
| interval. | ||
| - Live reads select `effective_to is null`. Cutoff reads select the row whose | ||
| interval contains the cutoff. Ontology continuation reads use their frozen | ||
| `snapshot_at` when no knowledge cutoff was requested, so a page minted | ||
| before a change cannot silently switch to the new primary. | ||
| - Existing rows migrate as open intervals. Migration replay changes neither | ||
| their starts nor their history. History before ADR 0252 remains unavailable | ||
| because the deleted facts cannot be reconstructed honestly. | ||
| - This is valid-time history for a source assignment, not psychometric or | ||
| mathematical modeling. No weight, confidence, inference, or new Voice code | ||
| is introduced. | ||
|
|
||
| ## Data model | ||
|
|
||
| ```mermaid | ||
| classDiagram | ||
| class SourcePost { | ||
| uuid post_id | ||
| text voc_type_code | ||
| } | ||
| class SourcePostVoice { | ||
| uuid post_id | ||
| text voice_type_code | ||
| boolean is_primary | ||
| timestamptz effective_from | ||
| timestamptz effective_to | ||
| timestamptz recorded_at | ||
| } | ||
| SourcePost "1" --> "1..*" SourcePostVoice | ||
| ``` | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant Import | ||
| participant SourcePost | ||
| participant VoiceHistory | ||
| Import->>SourcePost: update primary A to B | ||
| SourcePost->>VoiceHistory: close current A after source-row lock | ||
| SourcePost->>VoiceHistory: close current additional B, if present | ||
| SourcePost->>VoiceHistory: insert observed primary B at same instant | ||
| VoiceHistory-->>Import: one non-overlapping current primary | ||
| ``` | ||
|
|
||
| ## Consequences | ||
|
|
||
| - A → B → A is auditable without copying source content or exposing real | ||
| identifiers. | ||
| - Half-open bounds assign the exact change instant to the new primary and avoid | ||
| double matches. | ||
| - The exclusion constraint adds a GiST index and write-time check. This table | ||
| is bounded by Voice assignments per Post; partitioning is not warranted | ||
| until observed volume or lock evidence shows otherwise. | ||
|
|
||
| ## References | ||
|
|
||
| Cox, S. J. D., & Little, C. (2022). *Time ontology in OWL*. World Wide | ||
| Web Consortium. https://www.w3.org/TR/owl-time/ | ||
|
|
||
| PostgreSQL Global Development Group. (2025). *PostgreSQL 18 documentation: | ||
| Range types*. https://www.postgresql.org/docs/18/rangetypes.html |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,9 +9,11 @@ decision from them. | |||||||
|
|
||||||||
| | Supporting document | Normative ADR | | ||||||||
| |---|---| | ||||||||
| | [`product-requirements.md`](../product-requirements.md) | Product requirements projection across the ADR set; ADRs remain normative | | ||||||||
| | [`product-requirements.md`](../product-requirements.md) | Product requirements projection across the ADR set; ADRs remain normative, including [0252](0252-temporal-primary-voice-history.md) | | ||||||||
| | [`product-technical-gap-baseline.md`](../product-technical-gap-baseline.md) | Product/technical traceability projection across the ADR set; ADRs remain normative | | ||||||||
| | [`lineage-bi-research-notes.md`](../lineage-bi-research-notes.md) | [0084](0084-lineage-research-grounding.md), [0062](0062-semantic-unit-embedding.md), [0064](0064-lineage-evidence-and-tree-assembly.md), [0024](0024-rankweave-fusion-fail-closed.md), [0165](0165-quantity-script-display.md), [0167](0167-rankweave-ranking-channel-evidence.md), [0169](0169-ask-batched-lineage-graph.md), [0172](0172-event-lineage-channel-evidence.md), [0202](0202-ask-event-time-filter.md), [0223](0223-explicit-semantic-content-unit-kinds.md), [0238](0238-source-conversation-turn-import-contract.md) | | ||||||||
| | [`voice-combination-technical-requirements.md`](../voice-combination-technical-requirements.md) | [0246](0246-expanded-voice-of-x-post-taxonomy.md), [0251](0251-evidence-bearing-voice-combinations.md), [0252](0252-temporal-primary-voice-history.md) | | ||||||||
| | [`lineage-bi-research-notes.md`](../lineage-bi-research-notes.md) | [0084](0084-lineage-research-grounding.md), [0062](0062-semantic-unit-embedding.md), [0064](0064-lineage-evidence-and-tree-assembly.md), [0024](0024-rankweave-fusion-fail-closed.md), [0165](0165-quantity-script-display.md), [0167](0167-rankweave-ranking-channel-evidence.md), [0169](0169-ask-batched-lineage-graph.md), [0172](0172-event-lineage-channel-evidence.md), [0202](0202-ask-event-time-filter.md), [0223](0223-explicit-semantic-content-unit-kinds.md) | | ||||||||
|
Comment on lines
+15
to
+16
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. 🟡 Duplicate research-notes row in ADR map The supporting-document map now lists
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||
| | [`PROV_O_IMPLEMENTATION.md`](../PROV_O_IMPLEMENTATION.md) | [0065](0065-prov-o-provenance-boundary.md) | | ||||||||
| | [`PROV_O_IMPLEMENTATION_MATRIX.md`](../PROV_O_IMPLEMENTATION_MATRIX.md) | [0065](0065-prov-o-provenance-boundary.md) | | ||||||||
| | [`ONTOLOGY_NAMESPACE_INVENTORY.md`](../doctoring/ONTOLOGY_NAMESPACE_INVENTORY.md) | [0207](0207-repository-case-ontology-namespace-canonical.md), [0157](0157-public-ontology-namespace-identity.md) | | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # Voice-of-X Combination Technical Requirements | ||
|
|
||
| This supporting TRD projects ADR 0246, ADR 0251, and ADR 0252. Those ADRs are | ||
| normative when this document and an implementation differ. | ||
|
|
||
| ## Scope | ||
|
|
||
| LineageWeave represents a Post's explicitly supplied stakeholder perspectives | ||
| without assuming a company, B2B2C chain, or exhaustive industry taxonomy. One | ||
| imported primary Voice and zero or more evidence-bearing additional Voices are | ||
| atomic assignments; combinations are sets of rows, never compound codes. | ||
|
|
||
| ## Requirements | ||
|
|
||
| | ID | Requirement | Verification | | ||
| |---|---|---| | ||
| | VOC-TR-1 | `source_post.voc_type_code` owns the imported primary; additional assignments cannot demote it | Database trigger and API conflict tests | | ||
| | VOC-TR-2 | Every additional Voice references a normalized PROV-O derivation and governed truth status | Foreign keys, category trigger, authenticated write test | | ||
| | VOC-TR-3 | Primary assignments use non-overlapping half-open effective intervals and allow A → B → A under serialized concurrent source updates | GiST exclusion constraint and PostgreSQL integration tests | | ||
| | VOC-TR-4 | Live reads select current rows; cutoff reads select the containing interval; ontology continuation uses its frozen snapshot when no cutoff exists | Backend SQL-contract tests and authenticated cutoff API test | | ||
| | VOC-TR-5 | Post, filter, ontology JSON-LD, exact-value CSV, and UI apply the same RBAC/ABAC and source-eligibility boundary | API, SHACL, frontend interaction, and accessibility tests | | ||
| | VOC-TR-6 | Voice stays separate from counterparty relationship, role, topic, channel, lifecycle, and stakeholder salience | ADR/schema review and ontology round-trip tests | | ||
| | VOC-TR-7 | Migration replay preserves existing starts and never reconstructs deleted pre-migration history | Migration replay test and non-identifying runtime evidence | | ||
|
|
||
| ## Read contract | ||
|
|
||
| ```text | ||
| reference_time = knowledge_cutoff ?? ontology_snapshot ?? live | ||
| live = effective_to IS NULL | ||
| historical = effective_from <= reference_time < effective_to | ||
| open historical = effective_from <= reference_time AND effective_to IS NULL | ||
| ``` | ||
|
|
||
| The interval is lower-inclusive and upper-exclusive. The exact primary-change | ||
| instant belongs to the new primary, so a read cannot return two primary rows. | ||
|
|
||
| ## Component flow | ||
|
|
||
| ```mermaid | ||
| flowchart LR | ||
| Import[Authorized source import] --> SourcePost[(source_post)] | ||
| SourcePost --> Trigger[Primary Voice sync trigger] | ||
| Trigger --> History[(source_post_voice intervals)] | ||
| Admin[post_admin + visible evidence] --> API[Voice assignment API] | ||
| API --> Provenance[(PROV-O assertion)] | ||
| Provenance --> History | ||
| History --> PostRead[Post and filters] | ||
| History --> Ontology[Ontology JSON-LD and CSV] | ||
| PostRead --> UI[Post and board UI] | ||
| Ontology --> Explorer[Ontology explorer] | ||
| ``` | ||
|
|
||
| ## Failure behavior | ||
|
|
||
| - Missing or hidden evidence rejects or omits the additional assignment; it is | ||
| never replaced with a placeholder. | ||
| - Unknown Voice/truth categories fail with a database check error. | ||
| - Overlapping imported-primary intervals fail at the database boundary. | ||
| - Cutoffs before retained history return an explicit unavailable state rather | ||
| than the current value. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| -- ADR 0252: preserve non-overlapping imported primary Voice intervals. | ||
|
|
||
| begin; | ||
|
|
||
| create extension if not exists btree_gist; | ||
|
|
||
| alter table source_post_voice | ||
| add column if not exists effective_to timestamptz; | ||
|
|
||
| alter table source_post_voice | ||
| drop constraint if exists source_post_voice_effective_interval_check; | ||
| alter table source_post_voice | ||
| add constraint source_post_voice_effective_interval_check | ||
| check (effective_to is null or effective_from < effective_to); | ||
|
|
||
| create unique index if not exists source_post_voice_current_pair_idx | ||
| on source_post_voice (post_id, voice_type_code) | ||
| where effective_to is null; | ||
|
Comment on lines
+16
to
+18
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: Duplicate current-pair unique index The new migration re-adds Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| alter table source_post_voice | ||
| drop constraint if exists source_post_voice_primary_period_excl; | ||
| alter table source_post_voice | ||
| add constraint source_post_voice_primary_period_excl | ||
| exclude using gist ( | ||
| post_id with =, | ||
| tstzrange(effective_from, effective_to, '[)') with && | ||
| ) where (is_primary); | ||
|
|
||
| create or replace function synchronize_source_post_primary_voice() | ||
| returns trigger | ||
| language plpgsql | ||
| as $$ | ||
| declare | ||
| change_at timestamptz := clock_timestamp(); | ||
| begin | ||
| update source_post_voice | ||
| set effective_to = change_at | ||
| where post_id = new.post_id | ||
| and effective_to is null | ||
| and (is_primary or voice_type_code = new.voc_type_code); | ||
|
|
||
| insert into source_post_voice | ||
| (post_id, voice_type_code, is_primary, truth_status_code, | ||
| effective_from, recorded_at) | ||
| values ( | ||
| new.post_id, | ||
| new.voc_type_code, | ||
| true, | ||
| 'truth_observed', | ||
| case when tg_op = 'INSERT' then least(new.created_at, change_at) else change_at end, | ||
| change_at | ||
| ); | ||
|
Comment on lines
+36
to
+52
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: 0243 trigger replaces 0237's upsert with close-then-insert Sorted migration order makes 0243's Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| return new; | ||
| end; | ||
| $$; | ||
|
|
||
| commit; | ||
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.
📝 Info: No-cutoff read keyed to snapshot_at
When
knowledge_cutoffis null the predicate now keys offsnapshot_atrather thaneffective_to is null. For a fresh live read the two agree, but a voice witheffective_fromafter the frozen snapshot is now dropped. This matches the ADR's snapshot-consistency goal for paged continuation, and half-open intervals still yield exactly one primary.Was this helpful? React with 👍 or 👎 to provide feedback.