-
Notifications
You must be signed in to change notification settings - Fork 1
fix: persist R&R person catalog ids after landed 0019 (v0.86.3) #153
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
f34f557
4464be2
e4aeb16
82f38eb
9328850
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,3 @@ | ||
| R&R person chips read the stored catalog id. Historical person backfill | ||
| leaves homonym mentions unbound. A Pending run detail says reconstruction | ||
| has not started yet. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,19 @@ All notable changes to this project are documented here. Format follows | |
| [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows | ||
| [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
|
||
| ## [0.86.3] - 2026-08-16 | ||
|
|
||
| ### Fixed | ||
|
|
||
| - R&R person chips now read `cataloged_person_id` from | ||
| `post_summary_role` (ADR 0021). Open a post whose R&R names a | ||
| cataloged person: the chip is a button even when Keyman extraction | ||
| was not run on that post. Click it to walk that person, not a later | ||
| same-named row. Historical backfill leaves a role unbound when two | ||
| same-named mentions already exist. | ||
| - Request a lineage reconstruction: the opened Pending panel says | ||
|
Contributor
Author
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. #148 already landed the Pending reconstruction sentence on #74 Next action: drop this bullet. Keep only the
Contributor
Author
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. Live Next action: drop this bullet. Keep only the |
||
| reconstruction has not started yet. | ||
|
|
||
| ## [0.86.2] - 2026-08-16 | ||
|
|
||
| ### Fixed | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,7 +35,11 @@ | |
| NullCorporateHierarchyInferenceClient, | ||
| ) | ||
| from lineageweave.fixtures import fixture_thread_cast | ||
| from lineageweave.knowledge_graph import NODE_CORPORATE_ENTITY, NODE_TEAM | ||
| from lineageweave.knowledge_graph import ( | ||
| NODE_CORPORATE_ENTITY, | ||
| NODE_PERSON, | ||
| NODE_TEAM, | ||
| ) | ||
| from lineageweave.ontology import ontology_annotations | ||
| from lineageweave.post_summary import ( | ||
| ACTOR_TYPE_ORGANIZATION, | ||
|
|
@@ -61,8 +65,8 @@ async def fetch_persisted_summary( | |
| """Return the stored summary payload, or None when none has been written. | ||
|
|
||
| ``catalog_node_id`` comes from the role row's catalog foreign keys | ||
| (ADR 0019). This function does not join ``corporate_entity`` by | ||
| ``entity_name``. | ||
| (ADR 0019 / 0021). This function does not join ``corporate_entity`` | ||
| by ``entity_name``. Person chips read ``cataloged_person_id``. | ||
| """ | ||
| header = await conn.fetchrow( | ||
| "select korean_summary from post_summary_result where post_id = $1", | ||
|
|
@@ -79,7 +83,8 @@ async def fetch_persisted_summary( | |
| select role.actor_name, role.responsibility, role.actor_type_code, | ||
| role.affiliated_organization_name, | ||
| role.cataloged_team_id, | ||
| role.cataloged_corporate_entity_id | ||
| role.cataloged_corporate_entity_id, | ||
| role.cataloged_person_id | ||
| from post_summary_role role | ||
| where role.post_id = $1 | ||
| order by role.actor_name | ||
|
|
@@ -96,6 +101,9 @@ async def fetch_persisted_summary( | |
| elif row["cataloged_corporate_entity_id"] is not None: | ||
| catalog_node_id = str(row["cataloged_corporate_entity_id"]) | ||
| catalog_node_type_code = NODE_CORPORATE_ENTITY | ||
| elif row["cataloged_person_id"] is not None: | ||
| catalog_node_id = str(row["cataloged_person_id"]) | ||
| catalog_node_type_code = NODE_PERSON | ||
| payload_roles.append( | ||
| { | ||
| "actor_name": row["actor_name"], | ||
|
|
@@ -215,6 +223,7 @@ async def _replace_summary_projection( | |
| for role_index, role in enumerate(summary.roles_and_responsibilities): | ||
| cataloged_team_id = None | ||
| cataloged_corporate_entity_id = None | ||
| cataloged_person_id = None | ||
| if role.actor_type_code == ACTOR_TYPE_TEAM: | ||
| cataloged_team_id = await upsert_team( | ||
| conn, | ||
|
|
@@ -226,19 +235,29 @@ async def _replace_summary_projection( | |
| cataloged_corporate_entity_id = resolved_organization_ids.get( | ||
| role_index | ||
| ) | ||
| elif role.actor_type_code == ACTOR_TYPE_PERSON: | ||
| person_row = await conn.fetchrow( | ||
| "select person_id from cataloged_person " | ||
| "where person_name = $1 " | ||
| "order by created_at, person_id limit 1", | ||
|
Contributor
Author
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. Persist still first-wins among catalog homonyms ( A later click walks the oldest Kim Cheolsu, not the person this post named. That guess is now durable on Next action: bind by name only when exactly one
Contributor
Author
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. Persist still first-wins among catalog homonyms ( A later click walks the oldest Kim Cheolsu, not the person this post named. That guess is now durable on Next action: bind by name only when exactly one |
||
| role.actor_name, | ||
| ) | ||
| if person_row is not None: | ||
| cataloged_person_id = str(person_row["person_id"]) | ||
| await conn.execute( | ||
| "insert into post_summary_role " | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| "(post_id, actor_name, responsibility, actor_type_code, " | ||
| "affiliated_organization_name, cataloged_team_id, " | ||
| "cataloged_corporate_entity_id) values " | ||
| "($1, $2, $3, $4, $5, $6, $7)", | ||
| "cataloged_corporate_entity_id, cataloged_person_id) values " | ||
| "($1, $2, $3, $4, $5, $6, $7, $8)", | ||
| post_id, | ||
| role.actor_name, | ||
| role.responsibility, | ||
| role.actor_type_code, | ||
| role.affiliated_organization_name, | ||
| cataloged_team_id, | ||
| cataloged_corporate_entity_id, | ||
| cataloged_person_id, | ||
| ) | ||
| if cataloged_team_id is not None: | ||
| await conn.execute( | ||
|
|
@@ -255,18 +274,13 @@ async def _replace_summary_projection( | |
| post_id, | ||
| cataloged_corporate_entity_id, | ||
| ) | ||
| elif role.actor_type_code == ACTOR_TYPE_PERSON: | ||
| person_row = await conn.fetchrow( | ||
| "select person_id from cataloged_person where person_name = $1 limit 1", | ||
| role.actor_name, | ||
| elif cataloged_person_id is not None: | ||
| await conn.execute( | ||
| "insert into post_summary_person_mention (post_id, person_id) " | ||
| "values ($1, $2) on conflict do nothing", | ||
| post_id, | ||
| cataloged_person_id, | ||
| ) | ||
| if person_row is not None: | ||
| await conn.execute( | ||
| "insert into post_summary_person_mention (post_id, person_id) " | ||
| "values ($1, $2) on conflict do nothing", | ||
| post_id, | ||
| str(person_row["person_id"]), | ||
| ) | ||
| await persist_edges_for_post(conn, post_id) | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # ADR 0021 — Persist the R&R person catalog identity on the role row | ||
|
|
||
| **Decision status:** Accepted | ||
| **Date:** 2026-08-16 | ||
| **Depends on:** ADR 0009 cross-post actor identity; ADR 0019 role catalog | ||
| identity | ||
|
|
||
| ## Context | ||
|
|
||
| ADR 0019 stores `cataloged_team_id` and `cataloged_corporate_entity_id` | ||
| on `post_summary_role` so fetch does not rejoin by display name. Person | ||
| actors were still joined at read time by `person_name`, or dropped from | ||
| the payload entirely. Two people can share a display name. A later | ||
| Keyman row with the same name then steals the chip, and a person named | ||
| only in R&R (Keyman not extracted on that post) has no button. | ||
|
|
||
| Fellegi and Sunter (1969) treat a match decision as a binding to one | ||
| record, not a later re-search by a non-unique attribute. | ||
|
|
||
| ## Decision | ||
|
|
||
| `post_summary_role` stores `cataloged_person_id` written during | ||
| `persist_post_summary`. `fetch_persisted_summary` reads that column into | ||
| `catalog_node_id` / `node_person`. Person lookup, when it still resolves | ||
| by name, orders by `created_at`, then `person_id`. It still does not | ||
| create a new `cataloged_person` row (ADR 0009 gap). | ||
|
|
||
| Migration `0021_role_person_catalog_identity.sql` backfills existing | ||
| rows from a post-scoped mention only when the name match is unique on | ||
| that post (`HAVING count(*) = 1`). Two same-named mentions stay unbound. | ||
|
|
||
| At most one of `cataloged_team_id`, `cataloged_corporate_entity_id`, and | ||
| `cataloged_person_id` is set, and the set column must match | ||
| `actor_type_code`. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Open a post whose R&R names a cataloged person. The chip is a button | ||
| even when Keyman extraction was not run on that post. Click it to walk | ||
| that person, not a later same-named row. | ||
| - Pre-0021 rows with two same-named mentions stay unbound until an | ||
| operator re-persists the summary. Do not guess a UUID at migrate time. | ||
|
|
||
| ## References | ||
|
|
||
| Fellegi, I. P., & Sunter, A. B. (1969). A theory for record linkage. | ||
| *Journal of the American Statistical Association, 64*(328), 1183–1210. | ||
| https://doi.org/10.1080/01621459.1969.10501049 | ||
|
|
||
| Bhattacharya, I., & Getoor, L. (2007). Collective entity resolution in | ||
| relational data. *ACM Transactions on Knowledge Discovery from Data, | ||
| 1*(1), Article 5. https://doi.org/10.1145/1217299.1217304 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # R&R catalog identity — doctoring | ||
|
|
||
| These are the standards and papers that ground ADR 0019 and ADR 0021. | ||
| Cite them in APA 7th when you extend role identity binding or | ||
| related-node authorization. | ||
|
|
||
| Bhattacharya, I., & Getoor, L. (2007). Collective entity resolution in | ||
| relational data. *ACM Transactions on Knowledge Discovery from Data, | ||
| 1*(1), Article 5. https://doi.org/10.1145/1217299.1217304 | ||
|
|
||
| Fellegi, I. P., & Sunter, A. B. (1969). A theory for record linkage. | ||
| *Journal of the American Statistical Association, 64*(328), 1183–1210. | ||
| https://doi.org/10.1080/01621459.1969.10501049 | ||
|
|
||
| Hu, V. C., Ferraiolo, D., Kuhn, R., Schnitzer, A., Sandlin, K., | ||
| Miller, R., & Scarfone, K. (2014). *Guide to attribute based access | ||
| control (ABAC) definition and considerations* (NIST Special Publication | ||
| 800-162). National Institute of Standards and Technology. | ||
| https://doi.org/10.6028/NIST.SP.800-162 | ||
|
|
||
| Reynolds, D. (Ed.). (2014). *The organization ontology*. World Wide Web | ||
| Consortium. https://www.w3.org/TR/vocab-org/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1281,7 +1281,24 @@ function PostDetailPopup({ | |
| const catalogId = rr.catalog_node_id; | ||
| const catalogType = rr.catalog_node_type_code; | ||
| let actorName: ReactNode = <strong>{rr.actor_name}</strong>; | ||
| if (person) { | ||
| if (catalogType === NODE_PERSON && catalogId) { | ||
|
Contributor
Author
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. Keep this |
||
| actorName = ( | ||
| <button | ||
| className="keyman-select" | ||
| aria-label={`R&R person: ${rr.actor_name}`} | ||
| onClick={() => { | ||
| setFocusEntity(null); | ||
| setFocusTeam(null); | ||
| setFocusPerson({ | ||
| personId: catalogId, | ||
| personName: rr.actor_name, | ||
| }); | ||
| }} | ||
| > | ||
| <strong>{rr.actor_name}</strong> | ||
| </button> | ||
| ); | ||
| } else if (person) { | ||
| actorName = ( | ||
| <button | ||
| className="keyman-select" | ||
|
|
@@ -1648,6 +1665,7 @@ function AnalysisRunsPanel({ | |
| if (runs === null) return <p>Loading analysis runs...</p>; | ||
|
|
||
| const corpusHint = selected ? analysisRunCorpusHint(selected) : null; | ||
| const selectedNextAction = selected ? analysisRunNextAction(selected) : null; | ||
|
|
||
| return ( | ||
| <section className="popup-section lineage-home"> | ||
|
|
@@ -1704,6 +1722,7 @@ function AnalysisRunsPanel({ | |
| {" · "} | ||
| Requested {selected.requested_at.slice(0, 10)} | ||
| </p> | ||
| {selectedNextAction && <p className="post-meta">{selectedNextAction}</p>} | ||
|
Contributor
Author
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. #148 already writes this detail sentence from kind-specific Next action: rebase onto #74
Contributor
Author
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. This head still calls kind-blind Next action: rebase onto |
||
| <AnalysisRunReproducibilityDigests | ||
| codeRevisionSha={selected.code_revision_sha} | ||
| configurationSha256={selected.configuration_sha256} | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.