Skip to content
Draft
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
58 changes: 58 additions & 0 deletions .github/workflows/position-span-of-control-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Position Span of Control Quality

on:
pull_request:
branches:
- develop
- feat/position-reporting-hierarchy
paths:
- '.github/workflows/position-span-of-control-quality.yml'
- '.github/requirements/foundation-test.txt'
- 'packages/hris-kernel/**'
- 'docs/adr/0133-position-span-of-control-evidence.md'
- 'docs/doctoring/position-span-of-control-references.md'
- 'docs/traceability/position-span-of-control.md'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: position-span-of-control-quality-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
span-of-control:
name: Position span-of-control contract and HRIS kernel 100% coverage
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout exact candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Prove exact candidate checkout
env:
ORGMETRA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA"
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
check-latest: false
- name: Install reviewed test toolchain
run: |
python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt
python -m pip check
- name: Compile HRIS workforce boundary
run: python -m compileall -q packages/hris-kernel/src packages/hris-kernel/tests
- name: Test complete HRIS kernel with exact statement and branch coverage
env:
PYTHONPATH: packages/hris-kernel/src
COVERAGE_FILE: /tmp/orgmetra-position-span.coverage
run: python -m pytest -c packages/hris-kernel/pyproject.toml packages/hris-kernel/tests
- name: Require clean checkout
run: |
git diff --exit-code
test -z "$(git status --porcelain)"
39 changes: 39 additions & 0 deletions docs/adr/0133-position-span-of-control-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# ADR 0133: Position span-of-control evidence is structural, bitemporal, and non-authorizing

- Status: Proposed
- Scope: active PR #133 only; not protected-`develop` truth until integrated
- Parent owner: PR #94 Position-to-Position solid-line reporting hierarchy

## Context

Orgmetra can reconstruct a tenant-scoped solid-line Position reporting graph on PR #94, but buyers still need a defensible organization-design metric answering a narrower question: how many direct-report **Position seats** are attached to each manager Position at one business date and one system-knowledge cutoff?

Span of control is empirically consequential, but research does not support treating one universal number as optimal across all organizations or levels. Work complexity, functional diversity, hierarchy level and organizational goals change the relationship. Encoding a universal target would turn descriptive structure into an unsupported management or employment recommendation.

## Decision

Orgmetra will expose `PositionSpanOfControlSnapshot` as PII-minimized structural workforce evidence derived only from one exact governed `PositionReportingSnapshot`.

The boundary:

1. counts direct-report Position seats, never workers;
2. preserves the parent's explicit `tenant_record_id`, business `effective_on`, and UTC system `known_at` coordinate;
3. revalidates the parent runtime type, opaque identities, immutable edge shape, unique visible relationship identities, one manager per subordinate, self-edge rejection, and cycle rejection so direct dataclass construction cannot forge downstream evidence;
4. emits manager Position UUID plus positive direct-report Position count in deterministic UUID order;
5. binds canonical JSON with SHA-256 for audit correlation while excluding Person, Employment and Assignment identifiers;
6. labels the result `structural_workforce_evidence` and `not_authorized_for_employment_decision`; and
7. does **not** encode an ideal span, performance score, staffing recommendation, promotion signal, termination signal, or compensation implication.

## Consequences

This slice makes span-of-control structure inspectable without claiming that a wide or narrow span is good or bad. Any future recommendation layer must supply separately governed contextual evidence and human review rather than converting this descriptive count into a high-impact rule.

Persistence, authorized presentation, organization-design simulation and workflow-specific UI remain separate boundaries. PR #94 must integrate first; this child is not shipped truth and cannot inherit parent checks or reviews.

## References

Bell, G. D. (1967). Determinants of span of control. *American Journal of Sociology, 73*(1), 90–101. https://doi.org/10.1086/224439

Meier, K. J., & Bohte, J. (2000). Ode to Luther Gulick: Span of control and organizational performance. *Administration & Society, 32*(2), 115–137. https://doi.org/10.1177/00953990022019371

Theobald, N. A., & Nicholson-Crotty, S. (2005). The many faces of span of control: Organizational structure across multiple goals. *Administration & Society, 36*(6), 648–660. https://doi.org/10.1177/0095399704270585
23 changes: 23 additions & 0 deletions docs/doctoring/position-span-of-control-references.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Position span-of-control research notes

Retrieved for PR #133 on 2026-08-28. These references support a **descriptive structural metric**, not a universal managerial target.

## Evidence used

Bell (1967) examined supervisors and employees in a community hospital and reported that subordinate and supervisor job complexity were associated with narrower spans. Orgmetra therefore does not translate a direct-report count into a context-free judgement.

Meier and Bohte (2000) tested span-of-control relationships using 678 Texas school districts over four years and found meaningful associations with organizational performance. The finding supports preserving the structural measure as evidence while not treating it as causal or universally optimal outside that design.

Theobald and Nicholson-Crotty (2005) analyzed nearly 600 public bureaucracies and showed that structural choices can have different consequences across multiple organizational goals. Orgmetra therefore records the direct-report Position count and leaves goal-dependent interpretation to a separately governed, human-reviewed decision process.

## Implementation constraint

`PositionSpanOfControlSnapshot` counts subordinate **Position seats** under a manager Position at one tenant/business/system coordinate. It excludes worker identity and does not recommend a target span, employment action, compensation action, performance rating or organization change.

## APA 7 references

Bell, G. D. (1967). Determinants of span of control. *American Journal of Sociology, 73*(1), 90–101. https://doi.org/10.1086/224439

Meier, K. J., & Bohte, J. (2000). Ode to Luther Gulick: Span of control and organizational performance. *Administration & Society, 32*(2), 115–137. https://doi.org/10.1177/00953990022019371

Theobald, N. A., & Nicholson-Crotty, S. (2005). The many faces of span of control: Organizational structure across multiple goals. *Administration & Society, 36*(6), 648–660. https://doi.org/10.1177/0095399704270585
27 changes: 27 additions & 0 deletions docs/traceability/position-span-of-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Position span-of-control traceability

## State classification

- Protected `develop`: does **not** contain Position-to-Position reporting or span-of-control evidence at `9e3e4847510e1e612b48474ba42b177b8ed824df`.
- Parent active PR #94: owns governed bitemporal Position reporting at `3f67182bb3065f2fc8fd974bfdd75a390d8a8fdc`.
- Active child PR #133: owns descriptive Position span-of-control evidence. It is dependency-first active-PR truth only.
- Planned/out of scope here: persistence, authorized buyer presentation, organization-design simulation, target-span recommendations, staffing actions and employment decisions.

## Requirements to evidence

| Requirement | Implementation | Regression / evidence |
|---|---|---|
| Count Position seats, not workers | `build_position_span_of_control_snapshot()` consumes only `PositionReportingSnapshot` | `test_span_snapshot_counts_direct_reporting_positions_only` and canonical JSON PII assertions |
| Preserve bitemporal coordinate | tenant/effective/system fields copied from exact parent snapshot | noncanonical parent-coordinate regressions |
| Resist forged direct parent construction | exact runtime, UUID, tuple, uniqueness, self-edge and cycle revalidation | hostile graph/container/runtime regressions |
| Deterministic structural evidence | UUID-sorted `span_by_manager`, exact positive counts, canonical JSON + SHA-256 | direct output and digest regressions |
| No universal target / no high-impact authority | fixed evidence state and decision-authority labels; no recommendation code path | public contract + ADR 0133 |
| Exact owned coverage | dedicated `Position Span of Control Quality` workflow runs the complete HRIS-kernel suite with existing exact 100% statement/branch thresholds | `.github/workflows/position-span-of-control-quality.yml` |

## Dependency and integration rule

PR #94 must integrate first. A focused GREEN result on #133 is stack-local only. After parent integration, retarget #133 to fresh `develop`, reconcile any parent changes, then rerun every applicable Workforce/People/Job-Analysis/Foundation/Recovery/SAST/Security and central required workflow on one new exact head. Parent checks, reviews and statuses never transfer.

## Scientific interpretation guard

Primary research shows span of control can matter but varies with work complexity, hierarchy and organizational goals. Orgmetra therefore exposes direct-report Position counts as inspectable structural evidence only. Any later evaluative or prescriptive use requires separate contextual evidence and accountable human review.
7 changes: 5 additions & 2 deletions packages/hris-kernel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ Use this package to:
7. Build a deterministic `JobAnalysisSnapshot` that links observable Tasks to explicit KSAO requirements, retains source/version/digest provenance, and optionally carries historical Functional Job Analysis Data/People/Things codes without confusing Job with Position or Assignment.
8. Build a deterministic `WorkforceCompositionSnapshot` for one tenant, effective day, and recorded-time cutoff with distinct-person headcount, reportable employment count, staffed assignment count/FTE, unassigned-person count, and status counts without serializing row-level person, employment, position, or assignment identifiers.
9. Build a deterministic solid-line `PositionReportingSnapshot` for one tenant, effective day, and system-knowledge cutoff without deriving supervision from Person, Assignment, or organization-unit parentage.
10. Build a deterministic `PositionSpanOfControlSnapshot` from one governed Position reporting snapshot to count direct-report Position seats per manager Position without counting workers or recommending an ideal span.

Every historical reconstruction and portfolio/capacity decision requires an explicit `tenant_record_id`. A colliding durable identifier from another tenant is ignored rather than treated as local employment truth.

`PositionReportingRelationship` represents managerial structure between durable Position seats. Both endpoints must resolve to exactly one same-tenant `active` or `open` `PositionVersion` at the requested business/system coordinate. One subordinate can have only one visible solid-line manager, self-reporting and cycles fail closed, and caller-defined runtime subclasses cannot control identity or temporal comparisons. `build_position_reporting_snapshot(...)` returns deterministic subordinate-to-manager UUID pairs while its routine `repr` redacts those correlation identifiers. This is descriptive organizational evidence only: it neither identifies the worker occupying a seat nor grants employment-decision authority. Persistence and reporting-line mutation remain separate authoritative write-boundary work.
`PositionReportingRelationship` represents managerial structure between durable Position seats. Both endpoints must resolve to exactly one same-tenant `active` or `open` `PositionVersion` at the requested business/system coordinate. One visible relationship identity and one visible solid-line manager are allowed per subordinate; duplicate relationship identities, self-reporting and cycles fail closed, and caller-defined runtime subclasses cannot control identity or temporal comparisons. `build_position_reporting_snapshot(...)` returns deterministic subordinate-to-manager UUID pairs while its routine `repr` redacts those correlation identifiers. This is descriptive organizational evidence only: it neither identifies the worker occupying a seat nor grants employment-decision authority. Persistence and reporting-line mutation remain separate authoritative write-boundary work.

`PositionSpanOfControlSnapshot` is descriptive organization-design evidence derived only from an exact governed `PositionReportingSnapshot`. It preserves the tenant/business/system coordinate, revalidates the parent graph against forged direct construction, sorts manager Position evidence deterministically, and reports positive direct-report **Position-seat** counts without Person, Employment, or Assignment identifiers. Its canonical evidence is explicitly `structural_workforce_evidence` and `not_authorized_for_employment_decision`. Orgmetra does not encode a universal “good” span: research shows span effects depend on work complexity, hierarchy and organizational goals, so prescriptive interpretation requires separate contextual evidence and accountable human review.

`WorkforceCompositionSnapshot` is descriptive reporting evidence, not an employment recommendation or decision. It derives `active` and `leave` workforce composition from the same authoritative bitemporal facts and assignment-integrity rules used by the HRIS kernel. Concurrent employments count one person once for headcount while employment count and staffed FTE retain the actual portfolio shape. Contradictory visible versions, duplicate assignment identities, invalid coverage, and over-allocation fail closed instead of becoming plausible-looking metrics. Canonical snapshot JSON carries only the opaque tenant identifier, report coordinate, aggregate metrics, and schema version; `content_digest()` addresses those exact UTF-8 bytes with SHA-256.

Expand All @@ -28,4 +31,4 @@ Every historical reconstruction and portfolio/capacity decision requires an expl

`AuditOutboxEvent.canonical_json()` is the exact deterministic JSON text that the owning service persists. `AuditOutboxEvent.content_digest()` is SHA-256 over the UTF-8 bytes of that exact text. Callers must not independently serialize `to_cloudevent()` with library defaults and then assume the digest still addresses the stored representation. The Orgmetra PostgreSQL persistence boundary in migration `0003_audit_outbox_persistence.sql` reparses and allowlists that envelope, verifies tenant/event identity and high-impact confirmation, recomputes the digest over the supplied bytes, writes immutable `audit_event_record` evidence, and creates separate `outbox_delivery_record` transport state. The owning service calls `record_audit_outbox_event(...)` inside the same transaction as its business mutation.

This kernel itself does not talk to PostgreSQL, Keyverse, O*NET, or any other service. Persistence, authorization, source retrieval, and UI stay at their adapter boundaries. Production dispatcher claiming, retry scheduling, lease-expiry recovery, retention/export, external delivery receipts, job-analysis persistence, SME workflow, workforce-report authorization/presentation, position-reporting persistence/mutation, and selection-validity computation remain separately proven integration/operability work.
This kernel itself does not talk to PostgreSQL, Keyverse, O*NET, or any other service. Persistence, authorization, source retrieval, and UI stay at their adapter boundaries. Production dispatcher claiming, retry scheduling, lease-expiry recovery, retention/export, external delivery receipts, job-analysis persistence, SME workflow, workforce-report authorization/presentation, position-reporting persistence/mutation, span-of-control persistence/presentation, and selection-validity computation remain separately proven integration/operability work.
8 changes: 8 additions & 0 deletions packages/hris-kernel/src/orgmetra_hris_kernel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
build_position_reporting_snapshot,
)
from orgmetra_hris_kernel.resolution import resolve_bitemporal_facts, resolve_single_valued_fact
from orgmetra_hris_kernel.span_of_control import (
PositionSpanOfControlError,
PositionSpanOfControlSnapshot,
build_position_span_of_control_snapshot,
)
from orgmetra_hris_kernel.workforce import (
WorkforceCompositionSnapshot,
build_workforce_composition_snapshot,
Expand Down Expand Up @@ -80,13 +85,16 @@
"PositionReportingRelationship",
"PositionReportingSnapshot",
"PositionSeatError",
"PositionSpanOfControlError",
"PositionSpanOfControlSnapshot",
"PositionVersion",
"RecordedInterval",
"SingleValuedFactError",
"TaskEvidence",
"TaskKSAOLink",
"WorkforceCompositionSnapshot",
"build_position_reporting_snapshot",
"build_position_span_of_control_snapshot",
"build_workforce_composition_snapshot",
"close_recorded_interval",
"resolve_bitemporal_facts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,8 @@ def _freeze_known_at(value: datetime) -> datetime:
return wall_time - offset
except (OverflowError, ValueError) as exc:
raise PositionReportingHierarchyError(
"known_at cannot be represented as a UTC datetime.",
next_action=(
"Use a representable UTC knowledge cutoff, then rebuild the reporting chart."
),
"known_at UTC instant is outside the supported datetime range.",
next_action="Use a representable authoritative UTC system-knowledge timestamp, then rebuild the chart.",
) from exc


Expand Down Expand Up @@ -212,8 +210,16 @@ def build_position_reporting_snapshot(
]

manager_by_subordinate: dict[UUID, UUID] = {}
seen_relationship_ids: set[UUID] = set()
verified_positions: set[UUID] = set()
for relationship in visible:
relationship_id = relationship.position_reporting_relationship_id
if relationship_id in seen_relationship_ids:
raise PositionReportingHierarchyError(
"A visible position reporting relationship identity appears more than once.",
next_action="Resolve the duplicate reporting relationship identity, then rebuild the chart.",
)
seen_relationship_ids.add(relationship_id)
subordinate = relationship.subordinate_position_record_id
manager = relationship.manager_position_record_id
if subordinate in manager_by_subordinate:
Expand Down
Loading