Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f19a663
feat(ontology): catalog observed occupations by published title
seonghobae Aug 26, 2026
c8a702e
feat(ui): filter occupation catalog by published title
seonghobae Aug 26, 2026
a1a35fb
Merge remote-tracking branch 'origin/feat/onet-occupation-selector' i…
Aug 26, 2026
cdba73f
Merge remote-tracking branch 'origin/feat/onet-occupation-selector' i…
seonghobae Aug 26, 2026
e55189e
feat(ui): filter occupation catalog by published title
seonghobae Aug 26, 2026
6611df1
Merge remote-tracking branch 'origin/feat/onet-occupation-selector' i…
seonghobae Aug 26, 2026
14ffd0a
Merge remote-tracking branch 'origin/feat/onet-rating-occupation-filt…
Aug 26, 2026
eb5824e
Merge remote-tracking branch 'origin/feat/onet-occupation-selector' i…
Aug 26, 2026
7051e84
test(ui): require catalog identity selection after filtering
Aug 26, 2026
80ea2cb
Merge remote-tracking branch 'origin/feat/onet-rating-occupation-filt…
Aug 26, 2026
281d768
Merge remote-tracking branch 'origin/feat/onet-occupation-selector' i…
seonghobae Aug 26, 2026
8121921
Merge remote-tracking branch 'origin/feat/onet-rating-occupation-filt…
seonghobae Aug 26, 2026
5317154
test(ui): remove duplicate filter coverage
seonghobae Aug 26, 2026
8aa9734
Merge remote-tracking branch 'origin/feat/onet-occupation-selector' i…
seonghobae Aug 26, 2026
30b769f
Merge branch 'feat/onet-occupation-selector' of https://github.com/Co…
Aug 26, 2026
771cc88
Merge branch 'feat/onet-occupation-selector' into feat/onet-rating-oc…
seonghobae Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ flowchart LR
| `ask_time_axis.py` | Event-time vs ingestion-time clock choice for that window (ADR 0202) |
| `ontology.py` | Loads the governed Turtle source tree (`lineageweave-kg.ttl` plus generated fragments), the formal OWL 2/RDFS/SKOS vocabulary for the Knowledge Graph's node/edge types, source taxonomies, and published O*NET linkages (ADR 0004, ADR 0252, ADR 0255, ADR 0256) |
| `backend/app/occupation_rating_ingestion.py` | Projects authenticated occupation-rating evidence plus persisted source and represented-occupation catalogs (ADR 0258, ADR 0260, ADR 0261) |
| `frontend/src/components/OccupationRatingProfile.tsx` | Selects imported source and stored occupation title before reading exact Dashboard evidence, preserving absence, uncertainty, and warning semantics (ADR 0259–0261) |
| `frontend/src/components/OccupationRatingProfile.tsx` | Selects an imported source, filters stored occupation titles without ranking, and reads exact Dashboard evidence while preserving absence, uncertainty, and warning semantics (ADR 0259–0262) |
| `ontology_neighborhood.py` | Bounded typed ontology/provenance neighborhood (ADR 0184); PostgreSQL stays authoritative, OWL subclass is not an instance edge |
| `ontology_source_cursor.py` | Opaque HMAC source-window continuation (ADR 0124); keyset pagination, never OFFSET |
| `period_report.py` | Fit GRM/GPCM on persisted IRT rows, FIPC-select, EAP-score a period (ADR 0003 slice 3; Bock & Mislevy, 1982) |
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.d/2.21.0-occupation-catalog-filter.md
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).
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ All notable changes to this project are documented here. Format follows

### Added

- Occupation evidence now filters the imported occupation catalog by published
title or retained code without ranking or typed-code fallback, and fails
closed when the filter matches nothing (ADR 0262).
- Each imported rating source now exposes its exact represented O*NET-SOC
code/title catalog, and the Dashboard uses that catalog instead of requiring
users to know or type an occupation code (ADR 0261).
Expand Down
41 changes: 41 additions & 0 deletions docs/adr/0262-occupation-catalog-title-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# ADR 0262: Occupation catalog title filter

- Status: Accepted
- Date: 2026-08-27
- Extends: ADR 0259, ADR 0260, ADR 0261

## Context

ADR 0261 replaced typed O*NET-SOC entry with a native select of occupations
that have observations in the chosen source. An official rating artifact can
cover hundreds of occupations, so a user still cannot find a published title
without scanning the full catalog. A free-typed code would reintroduce the
gap ADR 0261 closed.

## Decision

1. Keep the occupation control as a native select populated only from the
imported occupation catalog for the selected source.
2. Add a native search field that filters that catalog by case-insensitive
substring of the published title or retained O*NET-SOC code. Do not rank,
boost, or infer similarity.
3. If the filter matches no catalog row, disable profile submission and give
a next action. If the current selection leaves the filtered set, move to
the first remaining catalog identity or clear the selection.
4. Reset the filter when the source or occupation catalog reloads. Never
submit a value that is not in the loaded catalog.
5. Authentication, provenance, and fail-closed unavailable/empty catalog
states remain ADR 0261.

## Consequences

A user can find a published occupation by title without typing an internal
code and without treating filter order as a recommendation.

## References

National Center for O*NET Development. (2026). *O*NET 31.0 database* [Data
set]. https://www.onetcenter.org/database.html

World Wide Web Consortium. (2024). *Web Content Accessibility Guidelines
(WCAG) 2.2* (W3C Recommendation). https://www.w3.org/TR/WCAG22/
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ decision from them.
| Occupation-rating Dashboard evidence view | [0259](0259-occupation-rating-evidence-ui.md) |
| Imported occupation-rating source catalog | [0260](0260-occupation-rating-source-catalog.md) |
| Rating-source occupation selector | [0261](0261-rating-source-occupation-selector.md) |
| Occupation catalog title filter | [0262](0262-occupation-catalog-title-filter.md) |

[0011](0011-prov-o-standard-relations.md) and [0065](0065-prov-o-provenance-boundary.md) cite the dated W3C PROV-O and PROV-DM Recommendations (https://www.w3.org/TR/2013/REC-prov-o-20130430/ and https://www.w3.org/TR/2013/REC-prov-dm-20130430/).

Expand Down
12 changes: 12 additions & 0 deletions docs/product-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,18 @@ the PostgreSQL integration test proves the source membership predicate; and
component tests prove selector changes clear prior evidence and pagination
stays bound to the loaded profile identifiers.

### PRD-FR-2I — Occupation catalog title filter

- Let an authenticated user filter the imported occupation catalog by
published title or retained code without ranking or typed-code fallback
(ADR 0262).
- Reset the filter when the source changes.
- Disable profile submission and state the next action when the filter
matches no catalog occupation.

Acceptance: submitting still sends only a catalog identity; a non-matching
filter never creates a request; and Storybook covers a no-match state.

### PRD-FR-3 — Bounded ontology exploration

- Apply RBAC/ABAC, source eligibility, and knowledge cutoff before graph
Expand Down
2 changes: 1 addition & 1 deletion docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ explicit unavailable state, not a reason to infer mappings from labels.
| Occupation-to-construct relations | ADR 0257 defines a candidate 3NF, release/source-partitioned immutable observation store and deterministic pinned-CSV importer preserving value, optional category, sample/error/CI, suppression, relevance, exact `MM/YYYY` source update month, source digest, and domain provenance. The official O*NET 31.0 Abilities file (94,640 rows, 910 occupations, 52 elements; SHA-256 `7e9cd79791ce6014e1d26d0a449ae5b1e7aa7ef52d39b3934c3bb8d438104b88`) and all 33 Scales Reference rows (SHA-256 `bcba23858ce21ecaacbde303a8993e35d46724b4afb8c9ec2b10e04f42adcfc9`) imported into a throwaway local PostgreSQL database with all 94,640 observations, 55 suppression flags, 7,572 not-relevant flags, and source months from `12/2004` through `08/2026`; every scale retained `scales_reference` artifact provenance, the database was dropped afterward, and no corpus is committed or claimed deployed | Pass exact-head review/checks and protected merge; validate and import every selected official rating artifact through an authorized runtime, returning only aggregate evidence; never invent or locally normalize a weight |
| Job-family and job-series semantics | No authoritative employer-specific job architecture is present | Define an organization-neutral import contract that preserves the authorized source hierarchy and distinguishes standard occupation codes from employer job families/series; no label-based binding |
| Temporal and multilevel interpretation | Static vocabulary only; no person-level inference is asserted | Version valid and transaction time, preserve occupation/organization/unit nesting and multiple membership, and require TEPP or the owning Rust psychometric service before any calibrated temporal or multilevel result |
| Product consumption | ADR 0258 defines a candidate authenticated occupation-rating API; ADR 0259 adds a candidate Dashboard evidence view; ADR 0260 replaces release/source code entry with persisted artifact selection; ADR 0261 replaces occupation-code entry with stored titles represented in that source. Component/API/PostgreSQL tests and Storybook scenes cover value/error/warning, absence, source/occupation selection, stale-response, pagination, and safe-link contracts; populated synthetic scenes were visually audited at 1440×900 and 390×844. Protected delivery and authenticated runtime evidence remain absent | Pass exact-head review/checks and protected merge; verify the authenticated catalogs, profile API, and rendered Dashboard against an authorized imported source using only aggregate/non-identifying evidence |
| Product consumption | ADR 0258 defines a candidate authenticated occupation-rating API; ADR 0259 adds a candidate Dashboard evidence view; ADR 0260 replaces release/source code entry with persisted artifact selection; ADR 0261 replaces occupation-code entry with stored titles represented in that source; ADR 0262 filters that catalog by title or retained code without ranking. Component/API/PostgreSQL tests and Storybook scenes cover value/error/warning, absence, source/occupation selection, no-match filtering, stale-response, pagination, and safe-link contracts; populated synthetic scenes were visually audited at 1440×900 and 390×844. Protected delivery and authenticated runtime evidence remain absent | Pass exact-head review/checks and protected merge; verify the authenticated catalogs, title filter, profile API, and rendered Dashboard against an authorized imported source using only aggregate/non-identifying evidence |

### Current exact-head PR queue

Expand Down
2 changes: 1 addition & 1 deletion docs/storybook-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ operator-facing control you can click before changing product CSS.
| Story | Operator next action | Token / module |
|---|---|---|
| `Workspace/OperationsDashboard` | Compare Event and post counts, inspect external-information coverage, then open the cited source behind a claim, handover, or repeat-issue fact. `EvidenceReady`, `NarrowViewport`, `AnalysisPendingAndMissingEvidence`, `AnalysisFailed`, and `LoadError` cover populated, mobile, unavailable-evidence, analysis-pending, retryable failure, and transport-error states. | `--color-dashboard-*`, `OperationsDashboard` |
| `Ontology/OccupationRatingProfile` | Select an imported release/source and stored occupation title, inspect the published value beside its sample/error and warning, then open the rating or scale artifact. `InteractiveEvidenceReady`, `EvidenceReady`, `NarrowViewport`, `CatalogEmpty`, `CatalogUnavailable`, `OccupationsEmpty`, `SourceUnavailable`, and `EmptyOccupation` cover both selectors, populated table, horizontal mobile access, and honest catalog/profile absence states. | `OccupationRatingProfile`, native select/table, `--color-border`, `--size-control-min` |
| `Ontology/OccupationRatingProfile` | Select an imported release/source, filter the stored occupation catalog by title or code, inspect the published value beside its sample/error and warning, then open the rating or scale artifact. `InteractiveEvidenceReady`, `EvidenceReady`, `NarrowViewport`, `CatalogEmpty`, `CatalogUnavailable`, `OccupationsEmpty`, `OccupationFilterEmpty`, `SourceUnavailable`, and `EmptyOccupation` cover both selectors, no-match filtering, populated table, horizontal mobile access, and honest catalog/profile absence states. | `OccupationRatingProfile`, native search/select/table, `--color-border`, `--size-control-min` |
| `Post/SimilarVocPanel` | Compare ontology/semantic similar VOC and prior action evidence, then open the source; unavailable states show no fabricated TEPP theta or weight. | `SimilarVocPanel.css`, `SimilarVocPanel` |
| `Evidence/CitationChip` | Click a cited title to open that source post. | `--color-chip-border`, `--radius-chip`, `CitationChip` |
| `Evidence/OrganizationAliasChip` | Click a cataloged org; the parenthetical is the unique corroborated SKOS companion. | `--color-chip-border`, `--radius-chip`, `OrganizationAliasChip` |
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -1448,13 +1448,14 @@

.occupation-rating-form {
display: grid;
grid-template-columns: minmax(14rem, 1fr) minmax(16rem, 1fr) auto;
grid-template-columns: minmax(16rem, 1fr) minmax(18rem, 1.4fr) auto;
align-items: end;
gap: var(--space-control-gap);
margin: 1rem 0;
}

.occupation-rating-form label,
.occupation-rating-occupation-select,
.occupation-rating-source {
display: grid;
gap: var(--space-control-gap);
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/components/OccupationRatingProfile.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,14 @@ export const OccupationsEmpty: Story = {
await expect(within(canvasElement).findByText(/선택할 수 있는 직업이 없습니다/)).resolves.toBeVisible();
},
};
export const OccupationFilterEmpty: Story = {
...InteractiveEvidenceReady,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByRole("option", { name: "Software Developers · 15-1252.00" });
await userEvent.type(canvas.getByLabelText("직업 찾기"), "unknown-occupation");
await expect(canvas.findByText(/입력한 조건에 맞는 직업이 없습니다/)).resolves.toBeVisible();
},
};
export const SourceUnavailable: Story = { args: { profile: { ...ready, source_available: false, source: null, items: [] } } };
export const EmptyOccupation: Story = { args: { profile: { ...ready, items: [] } } };
27 changes: 27 additions & 0 deletions frontend/src/components/OccupationRatingProfile.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const ready: Payload = {
};

beforeEach(() => {
vi.mocked(fetchOccupationRatings).mockClear();
vi.mocked(fetchOccupationRatingSources).mockResolvedValue({
sources: [{
data_release_code: "onet-31.0", release_version: "31.0",
Expand Down Expand Up @@ -157,6 +158,32 @@ describe("OccupationRatingProfile", () => {
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});

it("filters stored titles and submits only the selected catalog identity", async () => {
vi.mocked(fetchOccupationRatings).mockResolvedValue(ready);
render(<OccupationRatingProfile accessToken="synthetic-token" />);
await screen.findByRole("option", { name: "Software Developers · 15-1252.00" });

await userEvent.type(screen.getByLabelText("직업 찾기"), "15-1252");
expect(screen.queryByRole("option", { name: "Chief Executives · 11-1011.00" })).not.toBeInTheDocument();
await userEvent.selectOptions(screen.getByLabelText("직업"), "15-1252.00");
await userEvent.click(screen.getByRole("button", { name: "직업 근거 열기" }));

expect(fetchOccupationRatings).toHaveBeenCalledWith("synthetic-token", {
onetsocCode: "15-1252.00", dataReleaseCode: "onet-31.0", sourceTableCode: "abilities", offset: 0,
});
});

it("fails closed when the title filter matches no catalog occupation", async () => {
render(<OccupationRatingProfile accessToken="synthetic-token" />);
await screen.findByRole("option", { name: "Software Developers · 15-1252.00" });

await userEvent.type(screen.getByLabelText("직업 찾기"), "unknown-occupation");

expect(await screen.findByText(/입력한 조건에 맞는 직업이 없습니다/)).toBeInTheDocument();
expect(screen.getByRole("button", { name: "직업 근거 열기" })).toBeDisabled();
expect(fetchOccupationRatings).not.toHaveBeenCalled();
});

it("clears evidence and ignores an in-flight response when authentication changes", async () => {
let finishExpired: ((profile: Payload) => void) | undefined;
vi.mocked(fetchOccupationRatingSources).mockResolvedValue({ sources: [{
Expand Down
Loading