-
Notifications
You must be signed in to change notification settings - Fork 1
fix: bind R&R catalog ids without homonym joins (v0.86.2) #141
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
Merged
seonghobae
merged 1 commit into
feat/role-responsibility-agent-ontology
from
cursor/bc-753c193c-32a4-46e6-9c9b-4cbf81f5437a-fd39
Aug 16, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| R&R organization buttons walk the catalog id stored on the role row. A | ||
| shared display name no longer attaches a homonym. Team related matches | ||
| person/entity 403/404. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| """Persist and load the popup's Korean summary / key events / R&R. | ||
|
|
||
| ADR 0009: an R&R actor is not just per-post free text -- when it is a | ||
| team or organization, it is resolved to a shared catalog identity | ||
| (``cataloged_team`` / ``corporate_entity``) and a Knowledge Graph | ||
| mention edge is written, so the same "설계팀" or organization named | ||
| across two posts becomes one linkable node, not two unrelated strings. | ||
| ADR 0009 / 0019: an R&R actor is not just per-post free text -- when it | ||
| is a team or organization, it is resolved to a shared catalog identity | ||
| (``cataloged_team`` / ``corporate_entity``) stored on the role row and | ||
| a Knowledge Graph mention edge is written, so the same "설계팀" or | ||
| organization named across two posts becomes one linkable node. Fetch | ||
| never reconstructs that id by ``entity_name``; that column is not unique. | ||
| A person actor is opportunistically joined to an *existing* | ||
| ``cataloged_person`` row by name when Keyman extraction has already | ||
| cataloged that name. The R&R evidence is written to | ||
|
|
@@ -57,7 +58,12 @@ | |
| async def fetch_persisted_summary( | ||
| conn: asyncpg.Connection, post_id: str | ||
| ) -> dict[str, Any] | None: | ||
| """Return the stored summary payload, or None when none has been written.""" | ||
| """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``. | ||
| """ | ||
| header = await conn.fetchrow( | ||
| "select korean_summary from post_summary_result where post_id = $1", | ||
| post_id, | ||
|
|
@@ -72,23 +78,9 @@ async def fetch_persisted_summary( | |
| """ | ||
| select role.actor_name, role.responsibility, role.actor_type_code, | ||
| role.affiliated_organization_name, | ||
| team_mention.team_id, | ||
| org_mention.corporate_entity_id | ||
| role.cataloged_team_id, | ||
| role.cataloged_corporate_entity_id | ||
| from post_summary_role role | ||
| left join cataloged_team team | ||
| on role.actor_type_code = 'prov_team' | ||
| and team.team_name = role.actor_name | ||
| and team.affiliated_organization_name | ||
| is not distinct from role.affiliated_organization_name | ||
| left join post_team_mention team_mention | ||
| on team_mention.post_id = role.post_id | ||
| and team_mention.team_id = team.team_id | ||
| left join corporate_entity org | ||
| on role.actor_type_code = 'prov_organization' | ||
| and org.entity_name = role.actor_name | ||
| left join post_organization_mention org_mention | ||
| on org_mention.post_id = role.post_id | ||
| and org_mention.corporate_entity_id = org.corporate_entity_id | ||
| where role.post_id = $1 | ||
| order by role.actor_name | ||
| """, | ||
|
|
@@ -98,11 +90,11 @@ async def fetch_persisted_summary( | |
| for row in roles: | ||
| catalog_node_id = None | ||
| catalog_node_type_code = None | ||
| if row["team_id"] is not None: | ||
| catalog_node_id = str(row["team_id"]) | ||
| if row["cataloged_team_id"] is not None: | ||
| catalog_node_id = str(row["cataloged_team_id"]) | ||
| catalog_node_type_code = NODE_TEAM | ||
| elif row["corporate_entity_id"] is not None: | ||
| catalog_node_id = str(row["corporate_entity_id"]) | ||
| 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 | ||
| payload_roles.append( | ||
| { | ||
|
|
@@ -218,44 +210,51 @@ async def _replace_summary_projection( | |
| ordinal, | ||
| event_text, | ||
| ) | ||
| for role in summary.roles_and_responsibilities: | ||
| # ADR 0009 / 0019: resolve catalog identity before writing the role | ||
| # row so fetch never reconstructs it by a non-unique name. | ||
| for role_index, role in enumerate(summary.roles_and_responsibilities): | ||
| cataloged_team_id = None | ||
| cataloged_corporate_entity_id = None | ||
| if role.actor_type_code == ACTOR_TYPE_TEAM: | ||
| cataloged_team_id = await upsert_team( | ||
| conn, | ||
| role.actor_name, | ||
| role.affiliated_organization_name, | ||
| candidates, | ||
| ) | ||
| elif role.actor_type_code == ACTOR_TYPE_ORGANIZATION: | ||
| cataloged_corporate_entity_id = resolved_organization_ids.get( | ||
| role_index | ||
| ) | ||
| await conn.execute( | ||
| "insert into post_summary_role " | ||
| "(post_id, actor_name, responsibility, actor_type_code, " | ||
| "affiliated_organization_name) values ($1, $2, $3, $4, $5)", | ||
| "affiliated_organization_name, cataloged_team_id, " | ||
| "cataloged_corporate_entity_id) values " | ||
| "($1, $2, $3, $4, $5, $6, $7)", | ||
| post_id, | ||
| role.actor_name, | ||
| role.responsibility, | ||
| role.actor_type_code, | ||
| role.affiliated_organization_name, | ||
| cataloged_team_id, | ||
| cataloged_corporate_entity_id, | ||
| ) | ||
|
|
||
| # ADR 0009: cross-post identity resolution for team/organization/person | ||
| # actors -- see module docstring. | ||
| for role_index, role in enumerate(summary.roles_and_responsibilities): | ||
| if role.actor_type_code == ACTOR_TYPE_TEAM: | ||
| team_id = await upsert_team( | ||
| conn, | ||
| role.actor_name, | ||
| role.affiliated_organization_name, | ||
| candidates, | ||
| ) | ||
| if cataloged_team_id is not None: | ||
| await conn.execute( | ||
| "insert into post_team_mention (post_id, team_id) values ($1, $2) " | ||
| "on conflict do nothing", | ||
| post_id, | ||
| team_id, | ||
| cataloged_team_id, | ||
| ) | ||
| elif cataloged_corporate_entity_id is not None: | ||
| await conn.execute( | ||
| "insert into post_organization_mention " | ||
| "(post_id, corporate_entity_id) values ($1, $2) " | ||
| "on conflict do nothing", | ||
| post_id, | ||
| cataloged_corporate_entity_id, | ||
| ) | ||
| elif role.actor_type_code == ACTOR_TYPE_ORGANIZATION: | ||
| corporate_entity_id = resolved_organization_ids.get(role_index) | ||
| if corporate_entity_id is not None: | ||
| await conn.execute( | ||
| "insert into post_organization_mention " | ||
| "(post_id, corporate_entity_id) values ($1, $2) " | ||
| "on conflict do nothing", | ||
| post_id, | ||
| 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", | ||
|
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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # ADR 0019 — R&R catalog identity lives on the role row | ||
|
|
||
| **Decision status:** Accepted | ||
| **Date:** 2026-08-16 | ||
|
|
||
| ## Context | ||
|
|
||
| ADR 0009 writes `post_team_mention` and `post_organization_mention` so a | ||
| cataloged team or organization can start a related-node walk. ADR 0018 | ||
| exposes `catalog_node_id` on the summary payload by joining those | ||
| mentions back to `post_summary_role` through `actor_name`. | ||
|
|
||
| `corporate_entity.entity_name` is not unique. Two catalog rows can share | ||
| a display name (different `corporate_entity_code`, different parents). | ||
| A fetch join on name therefore: | ||
|
|
||
| - attaches a homonym that this post never resolved, or | ||
| - duplicates the role when more than one same-named row exists. | ||
|
|
||
| Mention tables are post-scoped, not role-scoped. They cannot reconstruct | ||
| which catalog id was chosen for a specific R&R row. That reconstruction | ||
| is a transitive dependency on a non-key attribute, so it is not third | ||
| normal form (Codd, 1970; Date, 2019). | ||
|
|
||
| Team identity is already unique on | ||
| `(team_name, affiliated_organization_name)`. Organization identity is | ||
| not. | ||
|
|
||
| ## Decision | ||
|
|
||
| `post_summary_role` stores the resolved catalog foreign keys | ||
| (`cataloged_team_id`, `cataloged_corporate_entity_id`) written during | ||
| `persist_post_summary`. `fetch_persisted_summary` reads those columns. | ||
| It does not join `corporate_entity` by `entity_name`. | ||
|
|
||
| Migration `0019_role_catalog_identity.sql` backfills existing rows from | ||
| a post-scoped mention only when the name match is unique on that post. | ||
| Two same-named mentions stay unbound rather than guessing. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Open a post whose R&R names an organization that shares a display | ||
| name with another catalog row. The button walks the resolved id, not | ||
| the homonym. | ||
| - Clicking that name still uses `GET /api/corporate-entities/{id}/related` | ||
| or `GET /api/teams/{id}/related`. Authz stays person/entity-parity: | ||
| a team mentioned only on another corp's private post is 403; an | ||
| unknown UUID is 404. | ||
|
|
||
| ## References | ||
|
|
||
| Codd, E. F. (1970). A relational model of data for large shared data | ||
| banks. *Communications of the ACM, 13*(6), 377–387. | ||
| https://doi.org/10.1145/362384.362685 | ||
|
|
||
| Date, C. J. (2019). *Database design and relational theory: Normal forms | ||
| and all that jazz* (2nd ed.). Apress. | ||
| https://doi.org/10.1007/978-1-4842-5540-7 | ||
|
|
||
| International Organization for Standardization. (2023). *ISO/IEC | ||
| 11179-1:2023: Information technology—Metadata registries (MDR)—Part 1: | ||
| Framework*. | ||
|
|
||
| Reynolds, D. (Ed.). (2014). *The organization ontology*. World Wide Web | ||
| Consortium. https://www.w3.org/TR/vocab-org/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.