From fdd66c4f3386bf446bb520342a6b44efc0bf8024 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 20:24:22 -0700 Subject: [PATCH 01/18] test(people): define liveness and readiness contract --- .../people-api/tests/test_operability_http.py | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 services/people-api/tests/test_operability_http.py diff --git a/services/people-api/tests/test_operability_http.py b/services/people-api/tests/test_operability_http.py new file mode 100644 index 000000000..dacb0b6df --- /dev/null +++ b/services/people-api/tests/test_operability_http.py @@ -0,0 +1,179 @@ +"""Executable liveness and owned-dependency readiness contracts for People API.""" + +from __future__ import annotations + +import json +import unittest + +from orgmetra_people_api.operability import PeopleOperabilityAsgiApp, PostgresReadinessProbe + + +class FakeReadinessProbe: + """Record readiness checks and optionally model an unavailable owned dependency.""" + + def __init__(self, *, error: Exception | None = None) -> None: + """Configure the probe with an optional failure raised during readiness.""" + self.error = error + self.calls = 0 + + def check_ready(self) -> None: + """Record one readiness check and raise the configured failure when present.""" + self.calls += 1 + if self.error is not None: + raise self.error + + +class FakeCursor: + """Capture SQL issued by the concrete PostgreSQL readiness probe.""" + + def __init__(self, *, row: object = (1,)) -> None: + """Configure the row returned by the readiness query.""" + self.row = row + self.executed: list[str] = [] + + def __enter__(self) -> FakeCursor: + """Enter the fake DB-API cursor context.""" + return self + + def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: + """Exit the fake cursor context without suppressing failures.""" + del exc_type, exc, traceback + + def execute(self, statement: str) -> None: + """Record one SQL statement exactly as the adapter issued it.""" + self.executed.append(statement) + + def fetchone(self) -> object: + """Return the configured readiness result row.""" + return self.row + + +class FakeConnection: + """Expose one fake cursor through the DB-API connection context shape.""" + + def __init__(self, cursor: FakeCursor) -> None: + """Bind the cursor returned by this connection.""" + self._cursor = cursor + + def __enter__(self) -> FakeConnection: + """Enter the fake connection context.""" + return self + + def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: + """Exit the fake connection context without suppressing failures.""" + del exc_type, exc, traceback + + def cursor(self) -> FakeCursor: + """Return the configured cursor.""" + return self._cursor + + +class PeopleOperabilityHttpTests(unittest.IsolatedAsyncioTestCase): + """Prove probes disclose no HR data and distinguish process from dependency health.""" + + async def _request( + self, + app: PeopleOperabilityAsgiApp, + *, + method: object = "GET", + path: object = "/health", + ) -> tuple[int, dict[bytes, bytes], dict[str, object]]: + """Execute one dependency-light ASGI request against the operability app.""" + scope = {"type": "http", "method": method, "path": path} + messages: list[dict[str, object]] = [] + + async def receive() -> dict[str, object]: + """Return one empty request frame; probes never consume request bodies.""" + return {"type": "http.request", "body": b"", "more_body": False} + + async def send(message: dict[str, object]) -> None: + """Capture an ASGI response message for assertions.""" + messages.append(message) + + await app(scope, receive, send) + start, body = messages + return ( + int(start["status"]), + dict(start["headers"]), + json.loads(bytes(body["body"])), + ) + + def test_constructor_requires_a_readiness_probe_contract(self) -> None: + """Reject incomplete dependency injection before a probe endpoint can serve.""" + with self.assertRaisesRegex(TypeError, "readiness_probe"): + PeopleOperabilityAsgiApp(readiness_probe=object()) + + def test_postgres_probe_requires_callable_factory_and_exact_success_row(self) -> None: + """Use a read-only owned-database check and fail closed on an unexpected result.""" + with self.assertRaisesRegex(TypeError, "connection_factory"): + PostgresReadinessProbe(connection_factory=object()) + + cursor = FakeCursor() + probe = PostgresReadinessProbe(connection_factory=lambda: FakeConnection(cursor)) + probe.check_ready() + self.assertEqual(cursor.executed, ["SET TRANSACTION READ ONLY", "SELECT 1"]) + + bad_cursor = FakeCursor(row=(0,)) + bad_probe = PostgresReadinessProbe(connection_factory=lambda: FakeConnection(bad_cursor)) + with self.assertRaisesRegex(RuntimeError, "readiness query"): + bad_probe.check_ready() + + async def test_health_is_live_without_touching_owned_dependencies(self) -> None: + """Keep liveness independent from PostgreSQL so orchestration avoids restart loops.""" + probe = FakeReadinessProbe(error=RuntimeError("database is down")) + status, headers, payload = await self._request(PeopleOperabilityAsgiApp(probe)) + + self.assertEqual((status, payload), (200, {"status": "ok"})) + self.assertEqual(headers[b"content-type"], b"application/json") + self.assertEqual(headers[b"cache-control"], b"no-store") + self.assertEqual(probe.calls, 0) + + async def test_ready_checks_owned_dependency_and_reports_success(self) -> None: + """Return ready only after the injected owned-dependency probe succeeds.""" + probe = FakeReadinessProbe() + status, _, payload = await self._request(PeopleOperabilityAsgiApp(probe), path="/ready") + + self.assertEqual((status, payload), (200, {"status": "ready"})) + self.assertEqual(probe.calls, 1) + + async def test_ready_normalizes_dependency_failure_without_leaking_details(self) -> None: + """Return a bounded 503 and useful next action without exposing DB secrets.""" + probe = FakeReadinessProbe(error=RuntimeError("postgres password=do-not-leak")) + status, _, payload = await self._request(PeopleOperabilityAsgiApp(probe), path="/ready") + + self.assertEqual(status, 503) + self.assertEqual(payload["error"], "not_ready") + self.assertIn("Retry", payload["message"]) + self.assertNotIn("password", json.dumps(payload)) + self.assertEqual(probe.calls, 1) + + async def test_unknown_route_and_wrong_method_are_bounded_transport_errors(self) -> None: + """Expose only the two reviewed probe routes and GET method.""" + app = PeopleOperabilityAsgiApp(FakeReadinessProbe()) + + status, _, payload = await self._request(app, path="/metrics") + self.assertEqual((status, payload["error"]), (404, "route_not_found")) + status, _, payload = await self._request(app, path=42) + self.assertEqual((status, payload["error"]), (404, "route_not_found")) + status, headers, payload = await self._request(app, method="POST", path="/health") + self.assertEqual((status, payload["error"]), (405, "method_not_allowed")) + self.assertEqual(headers[b"allow"], b"GET") + + async def test_non_http_scope_is_rejected_as_a_programming_error(self) -> None: + """Keep the operability adapter limited to HTTP ASGI scopes.""" + app = PeopleOperabilityAsgiApp(FakeReadinessProbe()) + + async def receive() -> dict[str, object]: + """Return one synthetic lifespan frame.""" + return {"type": "lifespan.startup"} + + async def send(message: dict[str, object]) -> None: + """Discard response messages; no response should be emitted.""" + del message + + with self.assertRaisesRegex(ValueError, "HTTP ASGI scopes"): + await app({"type": "lifespan"}, receive, send) + + +if __name__ == "__main__": + unittest.main() From 831de2302ccce6836870edc294afca2e572fe72c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 20:25:03 -0700 Subject: [PATCH 02/18] feat(people): add liveness and owned-db readiness probes --- .../src/orgmetra_people_api/operability.py | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 services/people-api/src/orgmetra_people_api/operability.py diff --git a/services/people-api/src/orgmetra_people_api/operability.py b/services/people-api/src/orgmetra_people_api/operability.py new file mode 100644 index 000000000..f6379edd0 --- /dev/null +++ b/services/people-api/src/orgmetra_people_api/operability.py @@ -0,0 +1,143 @@ +"""PII-free liveness and owned-dependency readiness surfaces for People API. + +Liveness proves only that the People API process can serve HTTP. Readiness is +stricter: it calls an injected probe for dependencies owned by this service and +returns unavailable until those dependencies can serve work. Identity providers +and other dedicated-writer CWL services are intentionally outside this probe so +the People API never reaches into their private implementation boundaries. +""" + +from __future__ import annotations + +from contextlib import AbstractContextManager +from dataclasses import dataclass +import json +from typing import Any, Awaitable, Callable, Mapping, Protocol, runtime_checkable + +AsgiReceive = Callable[[], Awaitable[dict[str, object]]] +AsgiSend = Callable[[dict[str, object]], Awaitable[None]] +PostgresConnectionFactory = Callable[[], AbstractContextManager[Any]] + +_READ_ONLY_SQL = "SET TRANSACTION READ ONLY" +_READINESS_SQL = "SELECT 1" + + +@runtime_checkable +class ReadinessProbe(Protocol): + """Check only dependencies whose availability is owned by the People API.""" + + def check_ready(self) -> None: + """Return normally when owned dependencies are ready; otherwise raise.""" + + +@dataclass(frozen=True, slots=True) +class PostgresReadinessProbe: + """Verify the Orgmetra PostgreSQL dependency without reading HR business data. + + Deployment code supplies the same kind of DB-API connection factory used by + the People persistence adapters. The probe enters a read-only transaction + and executes only ``SELECT 1``; it does not set tenant context or touch any + application table because readiness is infrastructure evidence, not an HR + data query. + """ + + connection_factory: PostgresConnectionFactory + + def __post_init__(self) -> None: + """Reject an unusable connection factory before serving readiness traffic.""" + if not callable(self.connection_factory): + raise TypeError("connection_factory must be callable") + + def check_ready(self) -> None: + """Raise when PostgreSQL cannot complete the reviewed read-only probe.""" + with self.connection_factory() as connection: + with connection.cursor() as cursor: + cursor.execute(_READ_ONLY_SQL) + cursor.execute(_READINESS_SQL) + row = cursor.fetchone() + if row != (1,): + raise RuntimeError("owned PostgreSQL readiness query returned an unexpected result") + + +@dataclass(frozen=True, slots=True) +class PeopleOperabilityAsgiApp: + """Expose dependency-light `/health` and `/ready` endpoints for orchestration. + + ``/health`` never invokes dependencies and therefore remains suitable for a + liveness probe that should not create restart loops during a database outage. + ``/ready`` invokes the supplied owned-dependency probe and returns HTTP 503 + on any dependency failure. Neither route accepts credentials, reads HR data, + returns dependency details, or claims that foreign CWL services are healthy. + """ + + readiness_probe: ReadinessProbe + + def __post_init__(self) -> None: + """Require a concrete readiness contract before the app can be served.""" + if not isinstance(self.readiness_probe, ReadinessProbe): + raise TypeError("readiness_probe must implement ReadinessProbe") + + async def __call__(self, scope: Mapping[str, object], receive: AsgiReceive, send: AsgiSend) -> None: + """Serve one PII-free operability request with bounded failure responses.""" + del receive + if scope.get("type") != "http": + raise ValueError("PeopleOperabilityAsgiApp accepts only HTTP ASGI scopes") + + if scope.get("method") != "GET": + await _send_json( + send, + status=405, + payload={ + "error": "method_not_allowed", + "message": "Use GET for People API health and readiness probes.", + }, + extra_headers=((b"allow", b"GET"),), + ) + return + + path = scope.get("path") + if path == "/health": + await _send_json(send, status=200, payload={"status": "ok"}) + return + if path == "/ready": + try: + self.readiness_probe.check_ready() + except Exception: # noqa: BLE001 - readiness must normalize dependency details. + await _send_json( + send, + status=503, + payload={ + "error": "not_ready", + "message": "Retry after an Orgmetra operator restores the owned People API dependency.", + }, + ) + return + await _send_json(send, status=200, payload={"status": "ready"}) + return + + await _send_json( + send, + status=404, + payload={ + "error": "route_not_found", + "message": "Use /health for liveness or /ready for owned-dependency readiness.", + }, + ) + + +async def _send_json( + send: AsgiSend, + *, + status: int, + payload: Mapping[str, object], + extra_headers: tuple[tuple[bytes, bytes], ...] = (), +) -> None: + """Emit deterministic non-cacheable JSON without HR or dependency details.""" + body = json.dumps(payload, ensure_ascii=True, separators=(",", ":"), sort_keys=True).encode("utf-8") + headers = ( + (b"content-type", b"application/json"), + (b"cache-control", b"no-store"), + *extra_headers, + ) + await send({"type": "http.response.start", "status": status, "headers": list(headers)}) + await send({"type": "http.response.body", "body": body, "more_body": False}) From 7b2e1932c3ec2d071cc497f5c625d2fd06904d02 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 20:25:45 -0700 Subject: [PATCH 03/18] feat(people): export operability probe contracts --- .../people-api/src/orgmetra_people_api/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/services/people-api/src/orgmetra_people_api/__init__.py b/services/people-api/src/orgmetra_people_api/__init__.py index b043bed33..48910035b 100644 --- a/services/people-api/src/orgmetra_people_api/__init__.py +++ b/services/people-api/src/orgmetra_people_api/__init__.py @@ -1,4 +1,4 @@ -"""Request-edge, governed read, confirmed-hire, and People mutation contracts.""" +"""Request-edge, governed read, confirmed-hire, mutation, and operability contracts.""" from orgmetra_people_api.auth import ( AuthenticatedPrincipal, @@ -32,6 +32,11 @@ create_employment_record, create_position_record, ) +from orgmetra_people_api.operability import ( + PeopleOperabilityAsgiApp, + PostgresReadinessProbe, + ReadinessProbe, +) from orgmetra_people_api.people import ( AuthorizedWorkerPeopleView, PeopleReadPort, @@ -59,6 +64,7 @@ "PeopleMutationIntegrityError", "PeopleMutationNotFound", "PeopleMutationPort", + "PeopleOperabilityAsgiApp", "PeopleReadPort", "PeopleRecordIntegrityError", "PeopleRecordNotFound", @@ -67,6 +73,8 @@ "PostgresHireAcceptancePort", "PostgresPeopleMutationPort", "PostgresPeopleReadPort", + "PostgresReadinessProbe", + "ReadinessProbe", "AssignmentMutationCommand", "AssignmentMutationResult", "EmploymentMutationCommand", From 331989f6140f1e2100600d1fbb398bb1e33e4db5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 20:26:08 -0700 Subject: [PATCH 04/18] docs(people): explain probe failure domains --- services/people-api/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/people-api/README.md b/services/people-api/README.md index 548a83446..6f8795643 100644 --- a/services/people-api/README.md +++ b/services/people-api/README.md @@ -10,6 +10,8 @@ The service exposes a governed hire-to-employment read contract. `read_worker_pe `PeopleAsgiApp` exposes that governed read use case as a dependency-light ASGI route: `GET /v1/tenants/{tenant_record_id}/people/{person_record_id}?effective_on=YYYY-MM-DD&purpose=people_read&fields=...`. It validates the exact route and query shape before authentication, accepts exactly one ASCII Bearer credential, delegates authentication and purpose-bound authorization to injected contracts, and never reads protected worker values after a denied authorization decision. Successful responses contain only authorized fields; all HTTP responses use `Cache-Control: no-store` and `Vary: Authorization`. Authentication, authorization, missing-record, integrity-conflict, and unexpected-backend failures are mapped to stable non-disclosing responses with a useful next action, and bearer tokens are never returned in response text. +`PeopleOperabilityAsgiApp` is a separate PII-free probe surface for deployment composition. `GET /health` reports process liveness and never calls PostgreSQL, while `GET /ready` invokes only the injected Orgmetra-owned readiness dependency and returns `503 not_ready` with a bounded operator action when that dependency fails. `PostgresReadinessProbe` implements the production owned-database check as a read-only transaction containing only `SELECT 1`; it does not set tenant context, read HR tables, accept credentials, or reach into Keyverse/Naruon/other dedicated-writer services. Deployment routing and network policy should expose these endpoints only where orchestration requires them. Metrics, Kubernetes manifests, startup-probe timing, and release certification remain separate release work. + The People API quality workflow is part of this contract and must run for pull requests to every supported protected/default integration branch, including `develop`. Its service tests enforce 100% owned statement and branch coverage and include regression coverage for the workflow dispatch boundary and HTTP security/transport behavior. `HireAcceptanceAsgiApp` exposes confirmed-hire materialization as `POST /v1/tenants/{tenant_record_id}/candidate-worker-conversions?purpose=candidate_hire`. Authentication and tenant binding occur before request-body parsing, so an unauthenticated or foreign-tenant caller cannot use body parsing or command construction as an oracle. Authenticated requests then pass the validated `Idempotency-Key`, content-type, JSON/schema, authorization, and governed command checks under a 64 KiB cumulative request-body limit, at most 1024 ASGI request frames, and 128 nested JSON containers below the top-level command object. `PostgresHireAcceptancePort` acquires a transaction-scoped lock for the tenant/route/key before it persists Person, Employment, `candidate_worker_conversion_record`, governed audit/outbox evidence, and `people_mutation_idempotency_record` in one tenant-bound transaction. An exact retry returns the first committed person/employment/conversion identities without repeating necessary PII, audit, or outbox writes; reusing the key for changed command semantics fails closed. The legacy `candidate_worker_link` write path is not used. From 33d0033f36cc90191bcc25449fb3de973fd8c30c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 20:26:19 -0700 Subject: [PATCH 05/18] docs(operability): trace People probe semantics --- docs/traceability/people-api-operability.md | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/traceability/people-api-operability.md diff --git a/docs/traceability/people-api-operability.md b/docs/traceability/people-api-operability.md new file mode 100644 index 000000000..5fee74b5a --- /dev/null +++ b/docs/traceability/people-api-operability.md @@ -0,0 +1,25 @@ +# People API operability traceability + +## Status + +This document describes the active PR that introduces executable People API probe semantics. Protected `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` does not yet contain these routes, and this document does not claim production deployment or release readiness. + +## Requirement-to-evidence map + +| Requirement | Active implementation | Executable evidence | Failure rule | +|---|---|---|---| +| Process liveness must not depend on PostgreSQL availability | `PeopleOperabilityAsgiApp` `GET /health` | `test_health_is_live_without_touching_owned_dependencies` | PostgreSQL failure cannot make liveness fail or trigger a dependency call. | +| Traffic readiness must reflect the service-owned PostgreSQL dependency | `PeopleOperabilityAsgiApp` `GET /ready` + `PostgresReadinessProbe` | `test_ready_checks_owned_dependency_and_reports_success`, `test_postgres_probe_requires_callable_factory_and_exact_success_row` | Readiness is 200 only after the reviewed read-only probe returns exactly `(1,)`. | +| Backend failure details must not cross the HTTP boundary | bounded `503 not_ready` response | `test_ready_normalizes_dependency_failure_without_leaking_details` | Any owned-dependency exception becomes a stable 503 with a next operator action and no backend message. | +| Probe routes must not become a hidden HR-data or identity integration path | no tenant context, credentials, HR table SQL, or foreign-service calls | source contract plus route tests | `/health` and `/ready` expose status only; Keyverse and other dedicated-writer services remain outside this boundary. | +| Unsupported transport shapes must stay bounded | exact GET-only `/health` and `/ready` surface | `test_unknown_route_and_wrong_method_are_bounded_transport_errors`, `test_non_http_scope_is_rejected_as_a_programming_error` | Unknown route is 404, wrong method is 405 with `Allow: GET`, non-HTTP ASGI is rejected. | + +## Operational interpretation + +Kubernetes distinguishes liveness from readiness: liveness is used to decide when a container should be restarted, while readiness determines whether it should receive traffic. The official Kubernetes probe guidance specifically warns that an incorrect liveness dependency can cause cascading restarts and notes that a strict backend dependency can be checked by readiness while liveness continues to reflect the application itself. Orgmetra therefore keeps PostgreSQL out of `/health` and checks it only in `/ready`. + +The PostgreSQL readiness query deliberately proves infrastructure availability only. It does not prove tenant authorization, HR-data correctness, migration compatibility, downstream integrations, or release acceptance. Those remain separate evidence gates. + +## Ownership boundary + +This slice writes only Orgmetra. PostgreSQL is an Orgmetra-owned runtime dependency. Keyverse, Naruon, contextual-orchestrator, and all other dedicated-writer CWL repositories are not queried or mutated by the probe and are not claimed healthy by its result. From 0f1b75509e0479adc452e7fd7850acd8b8816d05 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 20:26:25 -0700 Subject: [PATCH 06/18] docs(operability): record primary probe references --- .../people-api-operability-references.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/doctoring/people-api-operability-references.md diff --git a/docs/doctoring/people-api-operability-references.md b/docs/doctoring/people-api-operability-references.md new file mode 100644 index 000000000..983c8d12f --- /dev/null +++ b/docs/doctoring/people-api-operability-references.md @@ -0,0 +1,15 @@ +# People API operability references + +## Scope + +These references support the active People API liveness/readiness design. They are design evidence only; they do not claim Kubernetes conformance, certification, production deployment, or release acceptance. + +## APA 7 references + +Kubernetes Authors. (2026). *Liveness, readiness, and startup probes*. Kubernetes. https://kubernetes.io/docs/concepts/workloads/pods/probes/ + +Kubernetes Authors. (2026). *Configure liveness, readiness and startup probes*. Kubernetes. https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + +## Design use + +The Kubernetes documentation distinguishes liveness, which can trigger container restart, from readiness, which removes an unavailable Pod from service traffic. It also cautions that incorrectly coupling liveness to transient dependencies can create cascading failures and describes the pattern where readiness additionally verifies required backend services. Orgmetra applies that distinction by keeping `/health` dependency-free and binding `/ready` only to the People API's owned PostgreSQL availability check. From ccd736da14aa185f81b6b26b8eac56850a6605dc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 21:06:18 -0700 Subject: [PATCH 07/18] test(operability): require canonical probe runbook --- services/people-api/tests/test_operability_http.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/services/people-api/tests/test_operability_http.py b/services/people-api/tests/test_operability_http.py index dacb0b6df..a87907d3e 100644 --- a/services/people-api/tests/test_operability_http.py +++ b/services/people-api/tests/test_operability_http.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +from pathlib import Path import unittest from orgmetra_people_api.operability import PeopleOperabilityAsgiApp, PostgresReadinessProbe @@ -118,6 +119,18 @@ def test_postgres_probe_requires_callable_factory_and_exact_success_row(self) -> with self.assertRaisesRegex(RuntimeError, "readiness query"): bad_probe.check_ready() + def test_canonical_operability_doc_describes_the_probe_contract(self) -> None: + """Keep the canonical operator runbook aligned with the executable probe surface.""" + repository_root = Path(__file__).resolve().parents[3] + operability_text = (repository_root / "docs" / "OPERABILITY.md").read_text(encoding="utf-8") + + self.assertIn("### People API liveness and readiness", operability_text) + self.assertIn("`GET /health`", operability_text) + self.assertIn("`GET /ready`", operability_text) + self.assertIn("must not call PostgreSQL", operability_text) + self.assertIn("`SELECT 1`", operability_text) + self.assertIn("503", operability_text) + async def test_health_is_live_without_touching_owned_dependencies(self) -> None: """Keep liveness independent from PostgreSQL so orchestration avoids restart loops.""" probe = FakeReadinessProbe(error=RuntimeError("database is down")) From ec49aec82fff1439fe13f16dbbabb08069f5ffca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 21:06:46 -0700 Subject: [PATCH 08/18] docs(operability): bind People probes to operator runbook --- docs/OPERABILITY.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/OPERABILITY.md b/docs/OPERABILITY.md index 31f3ff23e..b796cf5b1 100644 --- a/docs/OPERABILITY.md +++ b/docs/OPERABILITY.md @@ -6,6 +6,14 @@ - High-impact command audit append success: 99.99% within accepted maintenance windows. - Integration adapter error visibility: every failed outbound command produces an operator-safe event. +### People API liveness and readiness + +- `GET /health` is process-liveness evidence only. It must not call PostgreSQL, Keyverse, Naruon, or another external dependency; a healthy process returns `200` with the bounded body `{"status":"ok"}`. Orchestrators may use this route for restart decisions without turning a transient database outage into a restart loop. +- `GET /ready` is traffic-eligibility evidence for the People API's owned PostgreSQL dependency. The current adapter opens a read-only transaction, issues only `SET TRANSACTION READ ONLY` and `SELECT 1`, and does not query HR application tables or require tenant context. A successful check returns `200` with `{"status":"ready"}`. +- A readiness dependency failure returns bounded HTTP `503` evidence with `error=not_ready` and a retry-oriented next action. The response must not disclose database exception text, credentials, connection strings, SQL details, tenant identifiers, or HR values. +- Readiness deliberately does not probe read-only dedicated-writer dependencies such as Keyverse or Naruon. Their outages retain their separately governed fail-closed/degraded-mode semantics and must not be converted into an Orgmetra process-liveness failure. +- These routes are an executable operability contract, not a deployment or certification claim. Production orchestration still requires reviewed startup/liveness/readiness timing, network policy, resource limits, telemetry, SLO alerting, and recovery evidence on the released artifact. + ## Degraded modes ### Keyverse unavailable From dd862fe318a2deb2ca513b35e825825fd37543b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 21:09:46 -0700 Subject: [PATCH 09/18] fix(provenance): seal operability runbook evidence --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 97f2bab14..bc2811923 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"32cc4ef78d1eca557fa01731026840be01211a043eb0ada552e4e6cb9eace353","bytes":17295,"lines":76},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a","bytes":13366,"lines":85},{"path":"docs/ERD.md","sha256":"546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe","bytes":6964,"lines":70},{"path":"docs/OPERABILITY.md","sha256":"82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62","bytes":11189,"lines":71},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac","bytes":11185,"lines":64},{"path":"docs/STORYBOARD.md","sha256":"6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2","bytes":1342,"lines":28},{"path":"docs/STORYBOOK.md","sha256":"82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9","bytes":1389,"lines":50},{"path":"docs/TEST_STRATEGY.md","sha256":"d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"lines":40},{"path":"docs/TRD.md","sha256":"23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077","bytes":9064,"lines":101},{"path":"docs/UML.md","sha256":"fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9","bytes":5528,"lines":122},{"path":"docs/USER_STORIES.md","sha256":"5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f","bytes":2670,"lines":37},{"path":"docs/WIREFRAMES.md","sha256":"b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e","bytes":2005,"lines":77},{"path":"docs/adr/0001-orgmetra-authoritative-hris-record.md","sha256":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"path":"docs/adr/0008-purpose-bound-pii-authorization.md","sha256":"c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7","bytes":5988,"lines":55},{"path":"docs/adr/0009-performance-criterion-observation-scope.md","sha256":"1ac10bb2747b0a5b4d62f627825cfd7f978f3fa88d7575bffc23d56371240a64","bytes":7057,"lines":57},{"path":"docs/adr/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"1656ef8b57c836ef7936a8e9cb6a824681eb7563157a1ab0a29deb25849a457b","bytes":5568,"lines":53},{"path":"docs/adr/0012-governed-migration-handoff.md","sha256":"713855d670001d3964ecb36cc653830502fb1d82a58b9e39f564b6992dd2bd80","bytes":5965,"lines":59},{"path":"docs/adr/0013-governed-requisition-review-packet.md","sha256":"70bf2cbdf903a8793d6d8bc116a08331931090118341f42010236e09c6cc1802","bytes":4693,"lines":46},{"path":"docs/adr/0014-job-analysis-snapshot-persistence.md","sha256":"a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md","sha256":"b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd","bytes":8227,"lines":226},{"path":"docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md","sha256":"4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d","bytes":6237,"lines":187},{"path":"package.json","sha256":"59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5","bytes":388,"lines":9},{"path":"packages/hris-kernel/src/orgmetra_hris_kernel/audit.py","sha256":"3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190","bytes":7707,"lines":160},{"path":"packages/hris-kernel/tests/test_audit_outbox.py","sha256":"5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c","bytes":7556,"lines":200},{"path":"schemas/openapi.yaml","sha256":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"0ee9539ee57f840c27d08009f7868cdc8662669df78a01dbc8be39216b8f1a3d","bytes":17811,"lines":469},{"path":"tests/test_evidence_sealing_postgres.sh","sha256":"57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7","bytes":11349,"lines":370},{"path":"tests/test_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"path":"tests/test_operational_uuid_postgres.sh","sha256":"7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7","bytes":3346,"lines":101},{"path":"tests/test_outbox_claim_postgres.sh","sha256":"1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b","bytes":14817,"lines":429},{"path":"tests/test_outbox_dead_letter_postgres.sh","sha256":"0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d","bytes":14008,"lines":377},{"path":"tests/test_people_mutation_idempotency_postgres.sh","sha256":"3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5","bytes":16191,"lines":381},{"path":"tests/test_tenant_isolation_postgres.sh","sha256":"dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a","bytes":15134,"lines":388},{"path":"tests/test_validity_study_case_postgres.sh","sha256":"0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02","bytes":14708,"lines":301},{"path":"tests/validate_repository.py","sha256":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"32cc4ef78d1eca557fa01731026840be01211a043eb0ada552e4e6cb9eace353","bytes":17295,"lines":76},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a","bytes":13366,"lines":85},{"path":"docs/ERD.md","sha256":"546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe","bytes":6964,"lines":70},{"path":"docs/OPERABILITY.md","sha256":"c8839e24a8df803259c506c93b5ca34bd1c7db52b2ee7d7fcd31b94b95f47fc9","bytes":12692,"lines":79},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac","bytes":11185,"lines":64},{"path":"docs/STORYBOARD.md","sha256":"6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2","bytes":1342,"lines":28},{"path":"docs/STORYBOOK.md","sha256":"82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9","bytes":1389,"lines":50},{"path":"docs/TEST_STRATEGY.md","sha256":"d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"lines":40},{"path":"docs/TRD.md","sha256":"23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077","bytes":9064,"lines":101},{"path":"docs/UML.md","sha256":"fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9","bytes":5528,"lines":122},{"path":"docs/USER_STORIES.md","sha256":"5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f","bytes":2670,"lines":37},{"path":"docs/WIREFRAMES.md","sha256":"b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e","bytes":2005,"lines":77},{"path":"docs/adr/0001-orgmetra-authoritative-hris-record.md","sha256":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"path":"docs/adr/0008-purpose-bound-pii-authorization.md","sha256":"c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7","bytes":5988,"lines":55},{"path":"docs/adr/0009-performance-criterion-observation-scope.md","sha256":"1ac10bb2747b0a5b4d62f627825cfd7f978f3fa88d7575bffc23d56371240a64","bytes":7057,"lines":57},{"path":"docs/adr/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"1656ef8b57c836ef7936a8e9cb6a824681eb7563157a1ab0a29deb25849a457b","bytes":5568,"lines":53},{"path":"docs/adr/0012-governed-migration-handoff.md","sha256":"713855d670001d3964ecb36cc653830502fb1d82a58b9e39f564b6992dd2bd80","bytes":5965,"lines":59},{"path":"docs/adr/0013-governed-requisition-review-packet.md","sha256":"70bf2cbdf903a8793d6d8bc116a08331931090118341f42010236e09c6cc1802","bytes":4693,"lines":46},{"path":"docs/adr/0014-job-analysis-snapshot-persistence.md","sha256":"a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md","sha256":"b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd","bytes":8227,"lines":226},{"path":"docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md","sha256":"4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d","bytes":6237,"lines":187},{"path":"package.json","sha256":"59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5","bytes":388,"lines":9},{"path":"packages/hris-kernel/src/orgmetra_hris_kernel/audit.py","sha256":"3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190","bytes":7707,"lines":160},{"path":"packages/hris-kernel/tests/test_audit_outbox.py","sha256":"5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c","bytes":7556,"lines":200},{"path":"schemas/openapi.yaml","sha256":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"0ee9539ee57f840c27d08009f7868cdc8662669df78a01dbc8be39216b8f1a3d","bytes":17811,"lines":469},{"path":"tests/test_evidence_sealing_postgres.sh","sha256":"57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7","bytes":11349,"lines":370},{"path":"tests/test_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"path":"tests/test_operational_uuid_postgres.sh","sha256":"7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7","bytes":3346,"lines":101},{"path":"tests/test_outbox_claim_postgres.sh","sha256":"1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b","bytes":14817,"lines":429},{"path":"tests/test_outbox_dead_letter_postgres.sh","sha256":"0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d","bytes":14008,"lines":377},{"path":"tests/test_people_mutation_idempotency_postgres.sh","sha256":"3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5","bytes":16191,"lines":381},{"path":"tests/test_tenant_isolation_postgres.sh","sha256":"dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a","bytes":15134,"lines":388},{"path":"tests/test_validity_study_case_postgres.sh","sha256":"0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02","bytes":14708,"lines":301},{"path":"tests/validate_repository.py","sha256":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} \ No newline at end of file From 7a860810be754d3ee0f89ae62803c43ed31ebdd4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 21:14:41 -0700 Subject: [PATCH 10/18] test(operability): accept mapping readiness rows --- services/people-api/tests/test_operability_http.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/people-api/tests/test_operability_http.py b/services/people-api/tests/test_operability_http.py index a87907d3e..1fcadc6f7 100644 --- a/services/people-api/tests/test_operability_http.py +++ b/services/people-api/tests/test_operability_http.py @@ -119,6 +119,15 @@ def test_postgres_probe_requires_callable_factory_and_exact_success_row(self) -> with self.assertRaisesRegex(RuntimeError, "readiness query"): bad_probe.check_ready() + def test_postgres_probe_accepts_mapping_row_factory(self) -> None: + """Treat an equivalent mapping row as healthy instead of coupling readiness to tuple rows.""" + cursor = FakeCursor(row={"readiness_value": 1}) + probe = PostgresReadinessProbe(connection_factory=lambda: FakeConnection(cursor)) + + probe.check_ready() + + self.assertEqual(cursor.executed, ["SET TRANSACTION READ ONLY", "SELECT 1"]) + def test_canonical_operability_doc_describes_the_probe_contract(self) -> None: """Keep the canonical operator runbook aligned with the executable probe surface.""" repository_root = Path(__file__).resolve().parents[3] From f27420b89af4e4681b84d6d2e7f5289ab7c7e170 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 21:15:19 -0700 Subject: [PATCH 11/18] fix(operability): decouple readiness from cursor row shape --- services/people-api/src/orgmetra_people_api/operability.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/people-api/src/orgmetra_people_api/operability.py b/services/people-api/src/orgmetra_people_api/operability.py index f6379edd0..bda9356d7 100644 --- a/services/people-api/src/orgmetra_people_api/operability.py +++ b/services/people-api/src/orgmetra_people_api/operability.py @@ -55,8 +55,8 @@ def check_ready(self) -> None: cursor.execute(_READ_ONLY_SQL) cursor.execute(_READINESS_SQL) row = cursor.fetchone() - if row != (1,): - raise RuntimeError("owned PostgreSQL readiness query returned an unexpected result") + if row is None: + raise RuntimeError("owned PostgreSQL readiness query returned no result") @dataclass(frozen=True, slots=True) From 82a1f43a2c0bce276008a4fbd0f6fb03baaef67a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 21:15:50 -0700 Subject: [PATCH 12/18] test(operability): make readiness result row-shape neutral --- services/people-api/tests/test_operability_http.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/people-api/tests/test_operability_http.py b/services/people-api/tests/test_operability_http.py index 1fcadc6f7..fd68e11c9 100644 --- a/services/people-api/tests/test_operability_http.py +++ b/services/people-api/tests/test_operability_http.py @@ -104,8 +104,8 @@ def test_constructor_requires_a_readiness_probe_contract(self) -> None: with self.assertRaisesRegex(TypeError, "readiness_probe"): PeopleOperabilityAsgiApp(readiness_probe=object()) - def test_postgres_probe_requires_callable_factory_and_exact_success_row(self) -> None: - """Use a read-only owned-database check and fail closed on an unexpected result.""" + def test_postgres_probe_requires_callable_factory_and_a_result_row(self) -> None: + """Use a read-only owned-database check and fail closed when SELECT 1 yields no row.""" with self.assertRaisesRegex(TypeError, "connection_factory"): PostgresReadinessProbe(connection_factory=object()) @@ -114,10 +114,10 @@ def test_postgres_probe_requires_callable_factory_and_exact_success_row(self) -> probe.check_ready() self.assertEqual(cursor.executed, ["SET TRANSACTION READ ONLY", "SELECT 1"]) - bad_cursor = FakeCursor(row=(0,)) - bad_probe = PostgresReadinessProbe(connection_factory=lambda: FakeConnection(bad_cursor)) + missing_cursor = FakeCursor(row=None) + missing_probe = PostgresReadinessProbe(connection_factory=lambda: FakeConnection(missing_cursor)) with self.assertRaisesRegex(RuntimeError, "readiness query"): - bad_probe.check_ready() + missing_probe.check_ready() def test_postgres_probe_accepts_mapping_row_factory(self) -> None: """Treat an equivalent mapping row as healthy instead of coupling readiness to tuple rows.""" From 82c8ef9597e878d09812ba64c66beb0c0438aa7a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 08:09:02 -0700 Subject: [PATCH 13/18] test(operability): pin driver-independent readiness traceability --- services/people-api/tests/test_operability_http.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/services/people-api/tests/test_operability_http.py b/services/people-api/tests/test_operability_http.py index fd68e11c9..c3e607446 100644 --- a/services/people-api/tests/test_operability_http.py +++ b/services/people-api/tests/test_operability_http.py @@ -140,6 +140,16 @@ def test_canonical_operability_doc_describes_the_probe_contract(self) -> None: self.assertIn("`SELECT 1`", operability_text) self.assertIn("503", operability_text) + def test_traceability_matches_driver_independent_readiness_row_contract(self) -> None: + """Do not let traceability reintroduce the tuple-only readiness contract that production removed.""" + repository_root = Path(__file__).resolve().parents[3] + traceability_text = ( + repository_root / "docs" / "traceability" / "people-api-operability.md" + ).read_text(encoding="utf-8") + + self.assertNotIn("returns exactly `(1,)`", traceability_text) + self.assertIn("produces a row regardless of DB-API row factory", traceability_text) + async def test_health_is_live_without_touching_owned_dependencies(self) -> None: """Keep liveness independent from PostgreSQL so orchestration avoids restart loops.""" probe = FakeReadinessProbe(error=RuntimeError("database is down")) From 7847f3abe1d8acca6bbdfcce525e78c18e649c3f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 08:11:08 -0700 Subject: [PATCH 14/18] docs(operability): align readiness traceability with row factories --- docs/traceability/people-api-operability.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/traceability/people-api-operability.md b/docs/traceability/people-api-operability.md index 5fee74b5a..296aef052 100644 --- a/docs/traceability/people-api-operability.md +++ b/docs/traceability/people-api-operability.md @@ -9,7 +9,7 @@ This document describes the active PR that introduces executable People API prob | Requirement | Active implementation | Executable evidence | Failure rule | |---|---|---|---| | Process liveness must not depend on PostgreSQL availability | `PeopleOperabilityAsgiApp` `GET /health` | `test_health_is_live_without_touching_owned_dependencies` | PostgreSQL failure cannot make liveness fail or trigger a dependency call. | -| Traffic readiness must reflect the service-owned PostgreSQL dependency | `PeopleOperabilityAsgiApp` `GET /ready` + `PostgresReadinessProbe` | `test_ready_checks_owned_dependency_and_reports_success`, `test_postgres_probe_requires_callable_factory_and_exact_success_row` | Readiness is 200 only after the reviewed read-only probe returns exactly `(1,)`. | +| Traffic readiness must reflect the service-owned PostgreSQL dependency | `PeopleOperabilityAsgiApp` `GET /ready` + `PostgresReadinessProbe` | `test_ready_checks_owned_dependency_and_reports_success`, `test_postgres_probe_requires_callable_factory_and_a_result_row`, `test_postgres_probe_accepts_mapping_row_factory` | Readiness is 200 only after the reviewed read-only probe produces a row regardless of DB-API row factory. | | Backend failure details must not cross the HTTP boundary | bounded `503 not_ready` response | `test_ready_normalizes_dependency_failure_without_leaking_details` | Any owned-dependency exception becomes a stable 503 with a next operator action and no backend message. | | Probe routes must not become a hidden HR-data or identity integration path | no tenant context, credentials, HR table SQL, or foreign-service calls | source contract plus route tests | `/health` and `/ready` expose status only; Keyverse and other dedicated-writer services remain outside this boundary. | | Unsupported transport shapes must stay bounded | exact GET-only `/health` and `/ready` surface | `test_unknown_route_and_wrong_method_are_bounded_transport_errors`, `test_non_http_scope_is_rejected_as_a_programming_error` | Unknown route is 404, wrong method is 405 with `Allow: GET`, non-HTTP ASGI is rejected. | @@ -18,7 +18,7 @@ This document describes the active PR that introduces executable People API prob Kubernetes distinguishes liveness from readiness: liveness is used to decide when a container should be restarted, while readiness determines whether it should receive traffic. The official Kubernetes probe guidance specifically warns that an incorrect liveness dependency can cause cascading restarts and notes that a strict backend dependency can be checked by readiness while liveness continues to reflect the application itself. Orgmetra therefore keeps PostgreSQL out of `/health` and checks it only in `/ready`. -The PostgreSQL readiness query deliberately proves infrastructure availability only. It does not prove tenant authorization, HR-data correctness, migration compatibility, downstream integrations, or release acceptance. Those remain separate evidence gates. +The PostgreSQL readiness query deliberately proves infrastructure availability only. It does not prove tenant authorization, HR-data correctness, migration compatibility, downstream integrations, or release acceptance. The constant `SELECT 1` contract requires a result row but does not prescribe whether a DB-API driver represents that row as a tuple, mapping, or another row-factory-owned shape. Those concerns remain separate evidence gates. ## Ownership boundary From 303133ec656a95ac397ca459e17631862e42e5a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 10:03:44 -0700 Subject: [PATCH 15/18] test(operability): expose readiness event-loop blocking --- .../tests/test_operability_concurrency.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 services/people-api/tests/test_operability_concurrency.py diff --git a/services/people-api/tests/test_operability_concurrency.py b/services/people-api/tests/test_operability_concurrency.py new file mode 100644 index 000000000..fc63e47fa --- /dev/null +++ b/services/people-api/tests/test_operability_concurrency.py @@ -0,0 +1,56 @@ +"""Concurrency regressions for the People API operability surface.""" + +from __future__ import annotations + +import asyncio +import unittest + +from orgmetra_people_api.operability import PeopleOperabilityAsgiApp + + +class ImmediateReadinessProbe: + """Model one synchronous DB-API readiness check without external dependencies.""" + + def __init__(self) -> None: + """Track how many readiness checks are performed.""" + self.calls = 0 + + def check_ready(self) -> None: + """Complete synchronously after recording the readiness check.""" + self.calls += 1 + + +class PeopleOperabilityConcurrencyTests(unittest.IsolatedAsyncioTestCase): + """Keep synchronous owned-dependency work off the ASGI event loop.""" + + async def test_ready_yields_event_loop_before_running_sync_probe(self) -> None: + """A readiness request must not monopolize the loop while DB-API work executes.""" + probe = ImmediateReadinessProbe() + app = PeopleOperabilityAsgiApp(probe) + loop = asyncio.get_running_loop() + loop_progress = asyncio.Event() + messages: list[dict[str, object]] = [] + + async def receive() -> dict[str, object]: + """Return an empty request frame; readiness never consumes a body.""" + return {"type": "http.request", "body": b"", "more_body": False} + + async def send(message: dict[str, object]) -> None: + """Capture the readiness response.""" + messages.append(message) + + # If the synchronous readiness probe runs directly on this event loop, + # this callback cannot run until the entire request has already returned. + loop.call_soon(loop_progress.set) + await app({"type": "http", "method": "GET", "path": "/ready"}, receive, send) + + self.assertTrue( + loop_progress.is_set(), + "readiness must yield the ASGI event loop before synchronous dependency work", + ) + self.assertEqual(probe.calls, 1) + self.assertEqual(messages[0]["status"], 200) + + +if __name__ == "__main__": + unittest.main() From ac7b947a15838453576d5d3f10c3bcd2ecde4da1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 10:04:48 -0700 Subject: [PATCH 16/18] fix(operability): offload readiness probe from event loop --- .../people-api/src/orgmetra_people_api/operability.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/people-api/src/orgmetra_people_api/operability.py b/services/people-api/src/orgmetra_people_api/operability.py index bda9356d7..327558969 100644 --- a/services/people-api/src/orgmetra_people_api/operability.py +++ b/services/people-api/src/orgmetra_people_api/operability.py @@ -9,6 +9,7 @@ from __future__ import annotations +import asyncio from contextlib import AbstractContextManager from dataclasses import dataclass import json @@ -65,9 +66,10 @@ class PeopleOperabilityAsgiApp: ``/health`` never invokes dependencies and therefore remains suitable for a liveness probe that should not create restart loops during a database outage. - ``/ready`` invokes the supplied owned-dependency probe and returns HTTP 503 - on any dependency failure. Neither route accepts credentials, reads HR data, - returns dependency details, or claims that foreign CWL services are healthy. + ``/ready`` offloads the supplied synchronous owned-dependency probe from the + ASGI event loop and returns HTTP 503 on any dependency failure. Neither route + accepts credentials, reads HR data, returns dependency details, or claims + that foreign CWL services are healthy. """ readiness_probe: ReadinessProbe @@ -101,7 +103,7 @@ async def __call__(self, scope: Mapping[str, object], receive: AsgiReceive, send return if path == "/ready": try: - self.readiness_probe.check_ready() + await asyncio.to_thread(self.readiness_probe.check_ready) except Exception: # noqa: BLE001 - readiness must normalize dependency details. await _send_json( send, From 471d55839329c63b1b551626dadb2c7185486359 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 10:05:45 -0700 Subject: [PATCH 17/18] test(operability): model blocking readiness concurrency --- .../tests/test_operability_concurrency.py | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/services/people-api/tests/test_operability_concurrency.py b/services/people-api/tests/test_operability_concurrency.py index fc63e47fa..ab32eec45 100644 --- a/services/people-api/tests/test_operability_concurrency.py +++ b/services/people-api/tests/test_operability_concurrency.py @@ -3,32 +3,41 @@ from __future__ import annotations import asyncio +import threading import unittest from orgmetra_people_api.operability import PeopleOperabilityAsgiApp -class ImmediateReadinessProbe: - """Model one synchronous DB-API readiness check without external dependencies.""" +class BlockingReadinessProbe: + """Model synchronous DB-API work that needs the ASGI event loop to stay live.""" def __init__(self) -> None: - """Track how many readiness checks are performed.""" + """Create one release signal and observable probe outcome.""" self.calls = 0 + self.release_signal = threading.Event() + self.released_during_check = False + + def release(self) -> None: + """Release the simulated dependency wait from the event-loop callback.""" + self.release_signal.set() def check_ready(self) -> None: - """Complete synchronously after recording the readiness check.""" + """Wait briefly for loop progress and fail if the loop is monopolized.""" self.calls += 1 + self.released_during_check = self.release_signal.wait(timeout=0.5) + if not self.released_during_check: + raise RuntimeError("ASGI event loop could not progress during readiness work") class PeopleOperabilityConcurrencyTests(unittest.IsolatedAsyncioTestCase): """Keep synchronous owned-dependency work off the ASGI event loop.""" - async def test_ready_yields_event_loop_before_running_sync_probe(self) -> None: - """A readiness request must not monopolize the loop while DB-API work executes.""" - probe = ImmediateReadinessProbe() + async def test_ready_keeps_event_loop_live_during_sync_probe(self) -> None: + """A blocking readiness check must execute away from the ASGI event loop.""" + probe = BlockingReadinessProbe() app = PeopleOperabilityAsgiApp(probe) loop = asyncio.get_running_loop() - loop_progress = asyncio.Event() messages: list[dict[str, object]] = [] async def receive() -> dict[str, object]: @@ -39,14 +48,14 @@ async def send(message: dict[str, object]) -> None: """Capture the readiness response.""" messages.append(message) - # If the synchronous readiness probe runs directly on this event loop, - # this callback cannot run until the entire request has already returned. - loop.call_soon(loop_progress.set) + # The callback can release the simulated DB wait only when the ASGI loop + # remains schedulable while synchronous readiness work is in progress. + loop.call_soon(probe.release) await app({"type": "http", "method": "GET", "path": "/ready"}, receive, send) self.assertTrue( - loop_progress.is_set(), - "readiness must yield the ASGI event loop before synchronous dependency work", + probe.released_during_check, + "readiness must keep the ASGI event loop live during synchronous dependency work", ) self.assertEqual(probe.calls, 1) self.assertEqual(messages[0]["status"], 200) From 7d75e683b5bca2881f70e4447e69b3de80babd00 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 10:06:07 -0700 Subject: [PATCH 18/18] docs(operability): trace readiness concurrency boundary --- docs/traceability/people-api-operability.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/traceability/people-api-operability.md b/docs/traceability/people-api-operability.md index 296aef052..50d03226f 100644 --- a/docs/traceability/people-api-operability.md +++ b/docs/traceability/people-api-operability.md @@ -10,6 +10,7 @@ This document describes the active PR that introduces executable People API prob |---|---|---|---| | Process liveness must not depend on PostgreSQL availability | `PeopleOperabilityAsgiApp` `GET /health` | `test_health_is_live_without_touching_owned_dependencies` | PostgreSQL failure cannot make liveness fail or trigger a dependency call. | | Traffic readiness must reflect the service-owned PostgreSQL dependency | `PeopleOperabilityAsgiApp` `GET /ready` + `PostgresReadinessProbe` | `test_ready_checks_owned_dependency_and_reports_success`, `test_postgres_probe_requires_callable_factory_and_a_result_row`, `test_postgres_probe_accepts_mapping_row_factory` | Readiness is 200 only after the reviewed read-only probe produces a row regardless of DB-API row factory. | +| Synchronous readiness work must not monopolize the ASGI event loop | `PeopleOperabilityAsgiApp` offloads `ReadinessProbe.check_ready` with `asyncio.to_thread` | `test_ready_keeps_event_loop_live_during_sync_probe` | The connection factory, cursor, transaction, and `SELECT 1` execute together on the worker thread while the ASGI loop remains schedulable. | | Backend failure details must not cross the HTTP boundary | bounded `503 not_ready` response | `test_ready_normalizes_dependency_failure_without_leaking_details` | Any owned-dependency exception becomes a stable 503 with a next operator action and no backend message. | | Probe routes must not become a hidden HR-data or identity integration path | no tenant context, credentials, HR table SQL, or foreign-service calls | source contract plus route tests | `/health` and `/ready` expose status only; Keyverse and other dedicated-writer services remain outside this boundary. | | Unsupported transport shapes must stay bounded | exact GET-only `/health` and `/ready` surface | `test_unknown_route_and_wrong_method_are_bounded_transport_errors`, `test_non_http_scope_is_rejected_as_a_programming_error` | Unknown route is 404, wrong method is 405 with `Allow: GET`, non-HTTP ASGI is rejected. | @@ -18,7 +19,7 @@ This document describes the active PR that introduces executable People API prob Kubernetes distinguishes liveness from readiness: liveness is used to decide when a container should be restarted, while readiness determines whether it should receive traffic. The official Kubernetes probe guidance specifically warns that an incorrect liveness dependency can cause cascading restarts and notes that a strict backend dependency can be checked by readiness while liveness continues to reflect the application itself. Orgmetra therefore keeps PostgreSQL out of `/health` and checks it only in `/ready`. -The PostgreSQL readiness query deliberately proves infrastructure availability only. It does not prove tenant authorization, HR-data correctness, migration compatibility, downstream integrations, or release acceptance. The constant `SELECT 1` contract requires a result row but does not prescribe whether a DB-API driver represents that row as a tuple, mapping, or another row-factory-owned shape. Those concerns remain separate evidence gates. +The PostgreSQL readiness query deliberately proves infrastructure availability only. It does not prove tenant authorization, HR-data correctness, migration compatibility, downstream integrations, or release acceptance. The constant `SELECT 1` contract requires a result row but does not prescribe whether a DB-API driver represents that row as a tuple, mapping, or another row-factory-owned shape. Because the DB-API probe is synchronous, the ASGI adapter offloads the complete probe call rather than moving a live connection or cursor between threads; this preserves connection-factory/context-manager ownership while preventing a slow readiness dependency from monopolizing unrelated event-loop work. Those concerns remain separate evidence gates. ## Ownership boundary