Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
137 changes: 137 additions & 0 deletions backend/app/iopsy_ontology_api.py
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,
}
42 changes: 42 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@
parse_allowed_property_query,
visible_ontology_neighborhood,
)
from backend.app.iopsy_ontology_api import (
construct_catalog_payload,
worker_function_profile_payload,
)
from backend.app.post_chat_ingestion import (
fetch_persisted_chat,
fetch_persisted_chats,
Expand Down Expand Up @@ -2298,6 +2302,44 @@ async def read_ontology_neighborhood(
return payload


@app.get("/api/ontology/worker-functions/{domain}/{rank}")
async def read_worker_function_psychology(
domain: str,
rank: int,
account: CurrentAccount = Depends(get_current_account),
) -> dict[str, Any]:
"""I/O-Psychology demand profile for one DOT/FJA worker function (ADR 0251).

Serves the grounded cognitive, affective, and behavioral construct
relations declared in the published ontology. An undeclared domain/rank
pair is an honest 404 -- never a fabricated profile. Unrecognized
domains are client errors.
"""
_require_post_read(account)
if rank < 0 or rank > 100 or domain not in {"data", "people", "things"}:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_CONTENT,
"domain must be one of data, people, things; rank within the published table extents",
)
Comment on lines +2319 to +2323

Copy link
Copy Markdown

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_psychology any 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.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

payload = worker_function_profile_payload(domain, rank)
if payload is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "undeclared worker function")
return payload


@app.get("/api/ontology/worker-function-constructs")
async def read_worker_function_construct_catalog(
account: CurrentAccount = Depends(get_current_account),
) -> dict[str, Any]:
"""Cognitive, affective, and behavioral construct catalog (ADR 0251).

Returns the deterministic typed construct groups and their nomological
relations from the published ontology, for ontology/evidence surfaces.
"""
_require_post_read(account)
return construct_catalog_payload()


@app.get("/api/posts/{post_id}/counterparties")
async def read_post_counterparties(
post_id: str,
Expand Down
103 changes: 103 additions & 0 deletions backend/tests/test_iopsy_ontology_api.py
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
53 changes: 53 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,59 @@ export function fetchOntologyNeighborhood(
return backendFetch(`/api/ontology/neighborhood?${params.toString()}`, accessToken);
}

export interface WorkerFunctionConstructPayload {
iri: string;
category: "cognitive" | "affective" | "behavioral";
label: string;
dimension: string;
theoretical_basis: string;
definition: string;
}

export interface WorkerFunctionProfilePayload {
function_domain: "data" | "people" | "things";
function_rank: number;
function_label: string;
cognitive_demands: WorkerFunctionConstructPayload[];
mental_workload_demands: WorkerFunctionConstructPayload[];
affective_demands: WorkerFunctionConstructPayload[];
emotional_labor_demands: WorkerFunctionConstructPayload[];
behavioral_manifestations: WorkerFunctionConstructPayload[];
psychomotor_behaviors: WorkerFunctionConstructPayload[];
interpersonal_behaviors: WorkerFunctionConstructPayload[];
}

export interface WorkerFunctionRelationPayload {
source_iri: string;
source_label: string;
predicate_iri: string;
predicate_label: string;
target_iri: string;
target_label: string;
target_category: string;
}

export interface WorkerFunctionConstructCatalogPayload {
constructs: Partial<
Record<"cognitive" | "affective" | "behavioral", WorkerFunctionConstructPayload[]>
>;
relations: WorkerFunctionRelationPayload[];
}

export function fetchWorkerFunctionProfile(
accessToken: string,
domain: string,
rank: number,
): Promise<WorkerFunctionProfilePayload> {
return backendFetch(`/api/ontology/worker-functions/${domain}/${rank}`, accessToken);
}

export function fetchWorkerFunctionConstructCatalog(
accessToken: string,
): Promise<WorkerFunctionConstructCatalogPayload> {
return backendFetch("/api/ontology/worker-function-constructs", accessToken);
}

export function extractPostKeymen(
accessToken: string,
postId: string,
Expand Down
Loading
Loading