-
Notifications
You must be signed in to change notification settings - Fork 1
feat(ontology): promote O*NET occupation-rating stack onto main #759
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
3157645
2fc2f77
91f342f
c7c2788
ec9a135
8ef4090
a3fb32b
0cea830
7cf2deb
7833a0d
571be28
550c466
85099a6
5cf1c09
6c49b3d
2a353d8
2723fea
d843672
6ee2278
b729a82
9a21e17
4dd8f0c
cc59925
e4599dc
10c3af4
96bd422
8a9e564
f566a08
f5ee37b
8ab2248
82153e5
38812fc
9a4a560
f42c42a
2c84837
314f36a
c9b6e04
b2dd06f
1c62254
d583435
88ae579
3b0655a
9e714f6
fdec8f6
99f098f
dcecf7e
76b3ef2
ce002f6
7cee035
8d86b0e
d7bac53
bad1866
64018df
9654bb8
437008e
3e30402
0dedf33
56d8cbf
3ee8b90
1b229c6
549a7e1
14cdb7b
c0f31f1
9bc9e93
8f5b87b
1d2f805
b3b9b36
7f60aa8
4c3677a
547b30a
026ba80
b035599
8932d19
b74a841
5281435
317205b
3d1d317
2130bab
33512d4
5697189
e6a1d62
e204209
1c3c548
5826fb7
25b8afd
4f7b928
e21d265
3074246
b8bc5cf
87ebc4e
2209ba6
4bf505f
bfdff9a
b24e8fa
f19a663
37a63cc
a18e89f
e76d182
c8a702e
ff4d176
f998111
8b420ce
e31f528
0f3017a
e6efa6f
a1a35fb
cdba73f
579be9c
e55189e
6611df1
14ffd0a
eb5824e
7051e84
80ea2cb
d23e1de
ec8be16
281d768
8121921
5317154
4983b29
8aa9734
8dd5adb
3d3463e
8f47be3
616b9da
97ad933
fe78ee6
a881fe9
ee380cf
30b769f
5fbdb9c
36aa851
85af0fc
17e8774
771cc88
7965c74
ed50a2a
8c15930
d64356e
f84741c
b81983a
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,5 @@ | ||
| ### Added | ||
|
|
||
| - Occupation evidence now filters the imported occupation catalog by published | ||
| title or retained code without ranking or typed SOC fallback, and fails closed | ||
| when the filter matches nothing (ADR 0262). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| """Read exact imported occupation ratings without deriving a score or weight.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from decimal import Decimal | ||
| from typing import Any, Protocol | ||
|
|
||
|
|
||
| class RatingReadConnection(Protocol): | ||
| """Small asyncpg-compatible surface used by the rating read projection.""" | ||
|
|
||
| async def fetchrow(self, query: str, *args: object) -> Any: | ||
| """Return one row or ``None``.""" | ||
|
|
||
| async def fetch(self, query: str, *args: object) -> list[Any]: | ||
| """Return ordered rows.""" | ||
|
|
||
|
|
||
| def _decimal_text(value: Decimal | None) -> str | None: | ||
| """Return the exact database decimal representation or honest absence.""" | ||
| return str(value) if value is not None else None | ||
|
|
||
|
|
||
| async def fetch_occupation_ratings( | ||
| conn: RatingReadConnection, | ||
| *, | ||
| data_release_code: str, | ||
| source_table_code: str, | ||
| onetsoc_code: str, | ||
| limit: int, | ||
| offset: int, | ||
| ) -> dict[str, object]: | ||
| """Return one bounded source profile and explicit artifact availability.""" | ||
| source = await conn.fetchrow( | ||
| """select rating_source.source_table_name, | ||
| rating_source.source_artifact_url, | ||
| rating_source.source_artifact_sha256, | ||
| rating_source.source_row_count, | ||
| scale_source.source_artifact_url as scale_artifact_url, | ||
| scale_source.source_artifact_sha256 as scale_artifact_sha256, | ||
| scale_source.source_row_count as scale_source_row_count | ||
| from occupational_source_table rating_source | ||
| left join occupational_source_table scale_source | ||
| on scale_source.data_release_code = rating_source.data_release_code | ||
| and scale_source.source_table_code = 'scales_reference' | ||
| where rating_source.data_release_code = $1 | ||
| and rating_source.source_table_code = $2""", | ||
| data_release_code, | ||
| source_table_code, | ||
| ) | ||
| if source is None: | ||
| return { | ||
| "data_release_code": data_release_code, | ||
| "source_table_code": source_table_code, | ||
| "onetsoc_code": onetsoc_code, | ||
| "source_available": False, | ||
| "source": None, | ||
| "items": [], | ||
| "next_offset": None, | ||
| } | ||
| rows = await conn.fetch( | ||
| """select observation.element_id, element.element_name, | ||
| observation.scale_id, scale.scale_name, | ||
| scale.minimum_value, scale.maximum_value, | ||
| observation.category_value, observation.data_value, | ||
| observation.sample_size, observation.standard_error, | ||
| observation.lower_ci_bound, observation.upper_ci_bound, | ||
| observation.recommend_suppress, observation.not_relevant, | ||
| observation.source_updated_month, observation.domain_source_code | ||
| from occupational_rating_observation observation | ||
| join occupational_element_definition element | ||
| on element.data_release_code = observation.data_release_code | ||
| and element.element_id = observation.element_id | ||
| join occupational_scale_definition scale | ||
| on scale.data_release_code = observation.data_release_code | ||
| and scale.scale_id = observation.scale_id | ||
| where observation.data_release_code = $1 | ||
| and observation.source_table_code = $2 | ||
| and observation.onetsoc_code = $3 | ||
| order by observation.element_id, observation.scale_id, | ||
| observation.category_value nulls first | ||
| limit $4 offset $5""", | ||
| data_release_code, | ||
| source_table_code, | ||
| onetsoc_code, | ||
| limit + 1, | ||
| offset, | ||
| ) | ||
| page = rows[:limit] | ||
| items = [ | ||
| { | ||
| "element_id": row["element_id"], | ||
| "element_name": row["element_name"], | ||
| "scale_id": row["scale_id"], | ||
| "scale_name": row["scale_name"], | ||
| "minimum_value": _decimal_text(row["minimum_value"]), | ||
| "maximum_value": _decimal_text(row["maximum_value"]), | ||
| "category_value": row["category_value"], | ||
| "data_value": _decimal_text(row["data_value"]), | ||
| "sample_size": row["sample_size"], | ||
| "standard_error": _decimal_text(row["standard_error"]), | ||
| "lower_ci_bound": _decimal_text(row["lower_ci_bound"]), | ||
| "upper_ci_bound": _decimal_text(row["upper_ci_bound"]), | ||
| "recommend_suppress": row["recommend_suppress"], | ||
| "not_relevant": row["not_relevant"], | ||
| "source_updated_month": row["source_updated_month"], | ||
| "domain_source_code": row["domain_source_code"], | ||
| } | ||
| for row in page | ||
| ] | ||
| return { | ||
| "data_release_code": data_release_code, | ||
| "source_table_code": source_table_code, | ||
| "onetsoc_code": onetsoc_code, | ||
| "source_available": True, | ||
| "source": { | ||
| "source_table_name": source["source_table_name"], | ||
| "source_artifact_url": source["source_artifact_url"], | ||
| "source_artifact_sha256": source["source_artifact_sha256"], | ||
| "source_row_count": source["source_row_count"], | ||
| "scale_artifact_url": source["scale_artifact_url"], | ||
| "scale_artifact_sha256": source["scale_artifact_sha256"], | ||
| "scale_source_row_count": source["scale_source_row_count"], | ||
| }, | ||
| "items": items, | ||
| "next_offset": offset + limit if len(rows) > limit else None, | ||
| } | ||
|
|
||
|
|
||
| async def fetch_occupation_rating_sources( | ||
| conn: RatingReadConnection, | ||
| ) -> dict[str, list[dict[str, object]]]: | ||
| """Return imported rating artifacts that contain at least one observation.""" | ||
| rows = await conn.fetch( | ||
| """select source.data_release_code, release.release_version, | ||
| release.source_publisher_name, release.source_license_url, | ||
| source.source_table_code, source.source_table_name, | ||
| source.source_artifact_url, source.source_artifact_sha256, | ||
| source.source_row_count | ||
| from occupational_source_table source | ||
| join occupational_data_release release | ||
| on release.data_release_code = source.data_release_code | ||
| where source.source_table_code <> 'scales_reference' | ||
| and exists ( | ||
| select 1 | ||
| from occupational_rating_observation observation | ||
| where observation.data_release_code = source.data_release_code | ||
| and observation.source_table_code = source.source_table_code | ||
| ) | ||
| order by release.imported_at desc, source.data_release_code, | ||
| source.source_table_name, source.source_table_code""" | ||
| ) | ||
| return {"sources": [dict(row) for row in rows]} | ||
|
|
||
|
|
||
| async def fetch_rating_source_occupations( | ||
| conn: RatingReadConnection, | ||
| *, | ||
| data_release_code: str, | ||
| source_table_code: str, | ||
| ) -> dict[str, object]: | ||
| """Return occupations with observations in one exact imported source.""" | ||
| source = await conn.fetchrow( | ||
| """select 1 | ||
| from occupational_source_table | ||
| where data_release_code = $1 and source_table_code = $2""", | ||
| data_release_code, | ||
| source_table_code, | ||
| ) | ||
| if source is None: | ||
| return { | ||
| "data_release_code": data_release_code, | ||
| "source_table_code": source_table_code, | ||
| "source_available": False, | ||
| "occupations": [], | ||
| } | ||
| rows = await conn.fetch( | ||
| """select classification.onetsoc_code, classification.occupation_title | ||
| from occupational_classification_entry classification | ||
| where classification.data_release_code = $1 | ||
| and exists ( | ||
| select 1 | ||
| from occupational_rating_observation observation | ||
| where observation.data_release_code = classification.data_release_code | ||
| and observation.source_table_code = $2 | ||
| and observation.onetsoc_code = classification.onetsoc_code | ||
| ) | ||
| order by classification.occupation_title, | ||
| classification.onetsoc_code""", | ||
| data_release_code, | ||
| source_table_code, | ||
| ) | ||
| return { | ||
| "data_release_code": data_release_code, | ||
| "source_table_code": source_table_code, | ||
| "source_available": True, | ||
| "occupations": [dict(row) for row in rows], | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,10 @@ | |||||||||||||||||||||
| **Status:** Accepted | ||||||||||||||||||||||
| **Date:** 2026-08-26 | ||||||||||||||||||||||
| **Extends:** [ADR 0004](0004-knowledge-graph-ontology.md), [ADR 0145](0145-psychometric-channel-weight-estimation.md), [ADR 0207](0207-repository-case-ontology-namespace-canonical.md), [ADR 0232](0232-worker-function-taxonomy-in-the-published-ontology.md) | ||||||||||||||||||||||
| <<<<<<< HEAD | ||||||||||||||||||||||
| ======= | ||||||||||||||||||||||
| **Superseded in part by:** [ADR 0252](0252-complete-2018-soc-hierarchy.md), which expands the major-group-only scheme into the complete 2018 SOC hierarchy. | ||||||||||||||||||||||
| >>>>>>> origin/feat/onet-rating-occupation-filter | ||||||||||||||||||||||
|
Comment on lines
+6
to
+9
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. 🟡 Merge conflict markers left in ADR 0245 Unresolved
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback.
Comment on lines
+6
to
+9
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 병합 충돌 마커를 제거하십시오.
수정 예시-<<<<<<< HEAD
-=======
**Superseded in part by:** [ADR 0252](0252-complete-2018-soc-hierarchy.md), which expands the major-group-only scheme into the complete 2018 SOC hierarchy.
->>>>>>> origin/feat/onet-rating-occupation-filter📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Context | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
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: Rating endpoints intentionally skip RBAC/ABAC
The new occupation-rating endpoints gate only on a valid account, with no
_require_post_reador ABAC filtering that other read routes apply. ADR 0258/0260 declare O*NET observations public reference data readable by any authenticated account, so this is intended rather than an access-control gap.Was this helpful? React with 👍 or 👎 to provide feedback.