-
Notifications
You must be signed in to change notification settings - Fork 1
feat(api,ui): expose FJA worker-function I/O-Psychology profiles and catalog #758
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| """Read-only API projection over the FJA I/O-Psychology semantic layer. | ||
|
|
||
| These serializers expose the DOT/FJA worker-function I/O-Psychology | ||
| profiles and the cognitive/affective/behavioral construct catalog | ||
| (ADR 0251) as plain, JSON-safe dictionaries for the Evidence API, | ||
| without importing database or HTTP concerns. Fail-closed behavior | ||
| mirrors the semantic layer: an undeclared worker function or construct | ||
| is an honest ``None``, and invalid domains raise ``ValueError``. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any | ||
|
|
||
| from lineageweave.iopsy_taxonomy import ( | ||
| IOPsyConstructRecord, | ||
| IOPsyRelationRecord, | ||
| WorkerFunctionIOPsyProfile, | ||
| all_iopsy_construct_records, | ||
| all_iopsy_relation_records, | ||
| iopsy_profile_for_worker_function, | ||
| ) | ||
|
|
||
| #: Profile attribute names in the same order as the typed record slots. | ||
| _PROFILE_SLOTS: tuple[tuple[str, str], ...] = ( | ||
| ("cognitive_demands", "cognitive_demands"), | ||
| ("mental_workload_demands", "mental_workload_demands"), | ||
| ("affective_demands", "affective_demands"), | ||
| ("emotional_labor_demands", "emotional_labor_demands"), | ||
| ("behavioral_manifestations", "behavioral_manifestations"), | ||
| ("psychomotor_behaviors", "psychomotor_behaviors"), | ||
| ("interpersonal_behaviors", "interpersonal_behaviors"), | ||
| ) | ||
|
|
||
|
|
||
| def construct_to_payload(construct: IOPsyConstructRecord) -> dict[str, str]: | ||
| """Project one I/O psychology construct into its JSON-safe payload shape. | ||
|
|
||
| Args: | ||
| construct: The typed semantic-layer construct record. | ||
|
|
||
| Returns: | ||
| dict[str, str]: iri, category, label, dimension, theoretical_basis, and | ||
| definition fields as plain strings. | ||
| """ | ||
| return { | ||
| "iri": construct.iri, | ||
| "category": construct.category, | ||
| "label": construct.label, | ||
| "dimension": construct.dimension, | ||
| "theoretical_basis": construct.theoretical_basis, | ||
| "definition": construct.definition, | ||
| } | ||
|
|
||
|
|
||
| def relation_to_payload(relation: IOPsyRelationRecord) -> dict[str, str]: | ||
| """Project one I/O psychology relation into its JSON-safe payload shape. | ||
|
|
||
| Args: | ||
| relation: The typed semantic-layer relation record. | ||
|
|
||
| Returns: | ||
| dict[str, str]: source_iri, source_label, predicate_iri, | ||
| predicate_label, target_iri, target_label, and target_category. | ||
| """ | ||
| return { | ||
| "source_iri": relation.source_iri, | ||
| "source_label": relation.source_label, | ||
| "predicate_iri": relation.predicate_iri, | ||
| "predicate_label": relation.predicate_label, | ||
| "target_iri": relation.target_iri, | ||
| "target_label": relation.target_label, | ||
| "target_category": relation.target_category, | ||
| } | ||
|
|
||
|
|
||
| def _profile_slot(profile: WorkerFunctionIOPsyProfile, field: str) -> list[dict[str, str]]: | ||
| """Serialize one named profile attribute into a sorted construct list. | ||
|
|
||
| Args: | ||
| profile: The typed worker-function I/O psychology profile. | ||
| field: The profile attribute name (e.g. ``cognitive_demands``). | ||
|
|
||
| Returns: | ||
| list[dict[str, str]]: Construct payload dictionaries, label-sorted. | ||
| """ | ||
| return sorted( | ||
| (construct_to_payload(construct) for construct in getattr(profile, field)), | ||
| key=lambda item: item["label"], | ||
| ) | ||
|
|
||
|
|
||
| def worker_function_profile_payload(domain: str, rank: int) -> dict[str, Any] | None: | ||
| """Serialize one worker function's I/O psychology demand profile. | ||
|
|
||
| Args: | ||
| domain: FJA domain (``data``, ``people``, or ``things``). | ||
| rank: Ordinal rank within the published domain limits. | ||
|
|
||
| Returns: | ||
| dict[str, Any] | None: The demand/manifestation profile payload, or | ||
| ``None`` when the function is not declared. Raises ``ValueError`` | ||
| for an unrecognized domain (caller error). | ||
| """ | ||
| profile = iopsy_profile_for_worker_function(domain, rank) | ||
| if profile is None: | ||
| return None | ||
| payload: dict[str, Any] = { | ||
| "function_domain": profile.function_domain, | ||
| "function_rank": profile.function_rank, | ||
| "function_label": profile.function_label, | ||
| } | ||
| for field, attribute in _PROFILE_SLOTS: | ||
| payload[field] = _profile_slot(profile, attribute) | ||
| return payload | ||
|
|
||
|
|
||
| def construct_catalog_payload() -> dict[str, Any]: | ||
| """Serialize the full I/O psychology construct and relation catalog. | ||
|
|
||
| Returns: | ||
| dict[str, Any]: Deterministic, JSON-safe payload with a ``constructs`` | ||
| map grouped by category plus the complete ``relations`` list. | ||
| """ | ||
| constructs = all_iopsy_construct_records() | ||
| by_category: dict[str, list[dict[str, str]]] = {} | ||
| for construct in constructs: | ||
| by_category.setdefault(construct.category, []).append( | ||
| construct_to_payload(construct) | ||
| ) | ||
| for category in by_category: | ||
| by_category[category] = sorted(by_category[category], key=lambda item: item["label"]) | ||
| relations = [relation_to_payload(relation) for relation in all_iopsy_relation_records()] | ||
| return { | ||
| "constructs": by_category, | ||
| "relations": relations, | ||
| } |
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,103 @@ | ||
| """API contract tests for the FJA I/O-Psychology ontology endpoints (ADR 0251). | ||
|
|
||
| Exercises the read-only worker-function psychology and construct-catalog | ||
| routes through their handler aliases with the authorization gate stubbed | ||
| out, mirroring the sibling ``backend/tests`` style. The payloads are pure | ||
| projections of the published ontology, so no database is needed. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import builtins | ||
| from types import SimpleNamespace | ||
|
|
||
| import pytest | ||
| from fastapi import HTTPException | ||
|
|
||
| from backend.app import iopsy_ontology_api | ||
| from backend.app import main | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def _stub_gate(monkeypatch: pytest.MonkeyPatch) -> None: | ||
| """Stub the coarse post_read authorization gate for handler tests.""" | ||
| if hasattr(main, "_require_post_read"): | ||
| monkeypatch.setattr(main, "_require_post_read", lambda account: None) | ||
|
|
||
|
|
||
| def _account() -> SimpleNamespace: | ||
| """Return a minimal authorized account double.""" | ||
| return SimpleNamespace(corporate_entity_ids=set(), process_unit_ids=set()) | ||
|
|
||
|
|
||
| def _run(function: object, *args: object) -> object: | ||
| """Await one async handler with the given arguments.""" | ||
| return asyncio.run(function(*args)) | ||
|
|
||
|
|
||
| def test_worker_function_psychology_returns_profile() -> None: | ||
| """High-complexity data work demands analytic cognition and core performance.""" | ||
| payload = _run(main.read_worker_function_psychology, "data", 2, _account()) | ||
|
|
||
| assert payload["function_domain"] == "data" | ||
| assert payload["function_rank"] == 2 | ||
| assert payload["function_label"] == "Analyzing" | ||
| cognitive_labels = {item["label"] for item in payload["cognitive_demands"]} | ||
| assert "Diagnostic Reasoning" in cognitive_labels | ||
| assert "Inductive & Deductive Reasoning" in cognitive_labels | ||
| assert payload["mental_workload_demands"] | ||
| assert all(item["category"] == "cognitive" for item in payload["mental_workload_demands"]) | ||
|
|
||
|
|
||
| def test_people_function_includes_emotional_labor() -> None: | ||
| """Negotiating demands deep acting and affective regulation.""" | ||
| payload = _run(main.read_worker_function_psychology, "people", 1, _account()) | ||
| assert payload["function_label"] == "Negotiating" | ||
| emotional_labor = {item["label"] for item in payload["emotional_labor_demands"]} | ||
| assert "Emotional Labor — Deep Acting" in emotional_labor | ||
| assert payload["affective_demands"] | ||
|
|
||
|
|
||
| def test_things_function_requires_safety() -> None: | ||
| """Things functions manifest safety compliance and psychomotor behavior.""" | ||
| payload = _run(main.read_worker_function_psychology, "things", 2, _account()) | ||
| behavioral_labels = {item["label"] for item in payload["behavioral_manifestations"]} | ||
| assert "Safety Compliance" in behavioral_labels | ||
| assert payload["psychomotor_behaviors"] | ||
|
|
||
|
|
||
| def test_undeclared_function_is_honest_404() -> None: | ||
| """An absent domain/rank pair fails closed as an honest 404.""" | ||
| with pytest.raises(HTTPException) as exc_info: | ||
| _run(main.read_worker_function_psychology, "data", 99, _account()) | ||
| assert exc_info.value.status_code == 404 | ||
|
|
||
|
|
||
| def test_invalid_domain_is_client_error() -> None: | ||
| """An unrecognized FJA domain is client error, never fabricated output.""" | ||
| with pytest.raises(HTTPException) as exc_info: | ||
| _run(main.read_worker_function_psychology, "bogus", 0, _account()) | ||
| assert exc_info.value.status_code == 422 | ||
|
|
||
|
|
||
| def test_construct_catalog_is_deterministic_and_complete() -> None: | ||
| """The catalog groups constructs by psychological domain with metadata.""" | ||
| payload = iopsy_ontology_api.construct_catalog_payload() | ||
| constructs = payload["constructs"] | ||
| assert {"cognitive", "affective", "behavioral"} <= set(constructs) | ||
| for category in ("cognitive", "affective", "behavioral"): | ||
| assert constructs[category] | ||
| cognitive = {item["label"] for item in constructs["cognitive"]} | ||
| assert "Mental Workload" in cognitive | ||
| affective = {item["label"] for item in constructs["affective"]} | ||
| assert "Burnout — Emotional Exhaustion" in affective | ||
| assert all(item["theoretical_basis"] for item in constructs["cognitive"]) | ||
| assert payload["relations"] | ||
|
|
||
|
|
||
| def test_construct_catalog_isolated_from_main_import() -> None: | ||
| """The serializer module imports no FastAPI application concerns.""" | ||
| code = builtins.open(iopsy_ontology_api.__file__, encoding="utf-8").read() | ||
|
|
||
| assert "import main" not in code | ||
| assert "fastapi" not in code | ||
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.
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: Rank accepted well beyond table extents
In
read_worker_function_psychologyany rank 0-100 passes validation and undeclared ranks return 404, though the 422 message claims ranks must stay within the published table extents (data 0-6, people 0-8, things 0-7). Behavior matches the fail-closed intent; the message is loose.Was this helpful? React with 👍 or 👎 to provide feedback.