Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
2612aac
test(hris): define assignment category supersession contract
seonghobae Sep 2, 2026
761c89d
feat(hris): build assignment category supersession facts
seonghobae Sep 2, 2026
3e17132
fix(hris): scope category correction to explicit facts
seonghobae Sep 2, 2026
937502c
test(hris): reject legacy category correction predecessor
seonghobae Sep 2, 2026
d00b61d
feat(hris): export assignment category correction contract
seonghobae Sep 2, 2026
6ee89e5
test(hris): reject forged correction identities
seonghobae Sep 2, 2026
e77dacd
fix(hris): harden assignment correction identities
seonghobae Sep 2, 2026
431390f
test(hris): reject malformed correction timestamps
seonghobae Sep 2, 2026
9120df7
fix(hris): validate correction recorded time
seonghobae Sep 2, 2026
71f336b
test(people): define assignment supersession persistence contract
seonghobae Sep 2, 2026
5b817ce
feat(people): persist assignment supersession provenance
seonghobae Sep 2, 2026
5e252bd
ci(people): verify assignment correction provenance
seonghobae Sep 2, 2026
6449983
docs(people): trace assignment correction provenance
seonghobae Sep 2, 2026
c574d67
ci(people): keep correction traceability in exact-head proof
seonghobae Sep 2, 2026
7cc6528
test(people): harden correction provenance persistence
seonghobae Sep 2, 2026
02effbb
test(people): define governed assignment correction command
seonghobae Sep 2, 2026
d11df86
feat(people): govern assignment correction command
seonghobae Sep 2, 2026
e8fddad
ci(people): prove governed correction command
seonghobae Sep 2, 2026
08ec680
docs(people): align correction command traceability
seonghobae Sep 2, 2026
17a3879
test(people): define PostgreSQL assignment correction contract
seonghobae Sep 2, 2026
6c4c630
test(people): require correction idempotency route
seonghobae Sep 2, 2026
a93b6a4
test(ci): execute assignment correction adapter contracts
seonghobae Sep 2, 2026
d98f7c8
test(people): bind correction replay to migration 0019
seonghobae Sep 2, 2026
86f22b5
feat(people): persist assignment correction replay route
seonghobae Sep 2, 2026
ff9f74d
feat(people): persist assignment category corrections atomically
seonghobae Sep 2, 2026
3ee5649
fix(ci): include correction replay migration in quality gate
seonghobae Sep 2, 2026
61c629e
docs(people): trace correction adapter and replay boundary
seonghobae Sep 2, 2026
b2d61d0
test(people): define assignment correction HTTP contract
seonghobae Sep 2, 2026
cb1a655
feat(people): expose assignment category correction HTTP API
seonghobae Sep 2, 2026
ce19816
test(people): cover correction HTTP failure boundaries
seonghobae Sep 2, 2026
574a4ba
ci(people): enforce correction HTTP coverage on child stack
seonghobae Sep 2, 2026
a703203
docs(people): document assignment correction product surface
seonghobae Sep 2, 2026
9b7c23f
fix(test): construct kernel correction failure correctly
seonghobae Sep 2, 2026
bbea381
test(people): require OpenAPI error envelope parity
seonghobae Sep 2, 2026
9c75607
fix(people): align correction OpenAPI error envelope
seonghobae Sep 2, 2026
b23d032
fix(test): align correction error schema with shared mutation envelope
seonghobae Sep 2, 2026
ad5415d
fix(people): restore closed correction error contract
seonghobae Sep 2, 2026
a94c9c3
docs(people): detail correction architecture and release handoff
seonghobae Sep 2, 2026
e086eaf
test(people): bound assignment correction evidence metadata
seonghobae Sep 2, 2026
06553c2
fix(people): bound assignment correction evidence metadata
seonghobae Sep 2, 2026
3fd21c9
test(people): pin correction metadata bounds in OpenAPI
seonghobae Sep 2, 2026
2cce7ed
fix(people): publish correction evidence metadata bounds
seonghobae Sep 2, 2026
14875b7
docs(people): trace bounded correction evidence metadata
seonghobae Sep 2, 2026
da7ccd9
test(people): prove correction metadata boundary values
seonghobae Sep 2, 2026
05ba1a0
test(people): require post-portfolio correction clock
seonghobae Sep 3, 2026
db47c5b
fix(people): timestamp correction after portfolio locks
seonghobae Sep 3, 2026
a80c379
docs(people): record post-portfolio correction clock
seonghobae Sep 3, 2026
4eb8f78
test(people): require deadlock-safe correction lock order
seonghobae Sep 3, 2026
b8f0262
fix(people): make correction lock order deadlock-safe
seonghobae Sep 3, 2026
55bf6ed
docs(people): record deterministic correction locks
seonghobae Sep 3, 2026
5d921db
test(people): reject correction command subtypes
seonghobae Sep 3, 2026
399f1dd
fix(people): reject correction command subtypes
seonghobae Sep 3, 2026
1368d6f
test(people): document correction HTTP contracts
seonghobae Sep 3, 2026
3fb8ba3
docs(people): trace exact correction command boundary
seonghobae Sep 3, 2026
e388643
test(people): scope correction OpenAPI contract to POST
seonghobae Sep 3, 2026
14417af
test(people): align scoped OpenAPI security indentation
seonghobae Sep 3, 2026
cac9dc8
test(people): reject substituted correction auth scheme
seonghobae Sep 3, 2026
84fb54f
fix(people): bind correction scope to Keyverse scheme
seonghobae Sep 3, 2026
a7c1ae4
merge(parent): restack correction provenance non-destructively
seonghobae Sep 3, 2026
21c5172
test(people): reject correction adapter runtime subtypes
seonghobae Sep 3, 2026
e5a3b10
fix(people): harden correction digest runtime types
seonghobae Sep 3, 2026
29f2144
fix(people): reject correction persistence runtime subtypes
seonghobae Sep 3, 2026
2ff9ad5
docs(people): trace correction runtime evidence hardening
seonghobae Sep 3, 2026
15cfca8
test(people): reject correction result runtime subtypes
seonghobae Sep 3, 2026
9959942
fix(people): require exact correction result runtime type
seonghobae Sep 3, 2026
0817104
docs(people): trace exact correction result boundary
seonghobae Sep 3, 2026
adc0964
merge: adopt current assignment category prerequisite
seonghobae Sep 3, 2026
b01d387
test(hris): expose correction recorded-time timezone trust gap
seonghobae Sep 3, 2026
8a8a2af
fix(hris): detach correction recorded-time timezone behavior
seonghobae Sep 3, 2026
6aa7b91
ci(people): execute correction recorded-time integrity regression
seonghobae Sep 3, 2026
6a71469
docs(people): trace correction recorded-time detachment
seonghobae Sep 3, 2026
ffca885
refactor(hris): keep recorded-time detachment fully coverable
seonghobae Sep 3, 2026
80be07b
test(hris): cover correction timezone offset edge cases
seonghobae Sep 3, 2026
5527979
style(hris): format recorded-time integrity regressions
seonghobae Sep 3, 2026
b61491e
merge(parent): adopt restored documentation contract checks
seonghobae Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions .github/workflows/assignment-correction-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: Assignment Correction Quality

on:
pull_request:
branches:
- feat/explicit-assignment-category
- bootstrap
- develop
- main
paths:
- "database/migrations/0018_assignment_category_supersession.sql"
- "database/migrations/0019_assignment_correction_idempotency_route.sql"
- "docs/traceability/assignment-category-correction-provenance.md"
- "packages/hris-kernel/**"
- "services/people-api/**"
- "tests/test_assignment_category_correction_postgres.sh"
- "tests/test_assignment_correction_idempotency_postgres.sh"
- ".github/workflows/assignment-correction-quality.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: assignment-correction-quality-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
kernel:
name: Assignment correction application and 100% People coverage
runs-on: ubuntu-24.04
timeout-minutes: 10
env:
PYTHONPATH: packages/hris-kernel/src:packages/keyverse-adapter/src:services/people-api/src
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 locked test dependencies
run: |
python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt
python -m pip check
- name: Prove HRIS correction domain contract
run: >-
python -m pytest
packages/hris-kernel/tests/test_assignment_category_correction.py
packages/hris-kernel/tests/test_assignment_category_correction_recorded_time_integrity.py
- name: Prove the full People API at exact statement and branch coverage
env:
COVERAGE_FILE: /tmp/orgmetra-assignment-correction.coverage
run: python -m pytest -c services/people-api/pyproject.toml services/people-api/tests
- name: Require clean checkout
run: |
git diff --exit-code
test -z "$(git status --porcelain)"

postgres:
name: Assignment correction PostgreSQL contract
runs-on: ubuntu-24.04
timeout-minutes: 10
services:
postgres:
image: postgres:16.14@sha256:33f923b05f64ca54ac4401c01126a6b92afe839a0aa0a52bc5aeb5cc958e5f20
env:
POSTGRES_USER: orgmetra
POSTGRES_PASSWORD: orgmetra
POSTGRES_DB: orgmetra
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U orgmetra -d orgmetra"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql://orgmetra:orgmetra@localhost:5432/orgmetra
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: Prove normalized supersession persistence
run: bash tests/test_assignment_category_correction_postgres.sh
- name: Require clean checkout
run: |
git diff --exit-code
test -z "$(git status --porcelain)"

idempotency:
name: Assignment correction idempotency contract
runs-on: ubuntu-24.04
timeout-minutes: 10
services:
postgres:
image: postgres:16.14@sha256:33f923b05f64ca54ac4401c01126a6b92afe839a0aa0a52bc5aeb5cc958e5f20
env:
POSTGRES_USER: orgmetra
POSTGRES_PASSWORD: orgmetra
POSTGRES_DB: orgmetra
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U orgmetra -d orgmetra"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql://orgmetra:orgmetra@localhost:5432/orgmetra
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: Prove correction replay route is durable and closed
run: bash tests/test_assignment_correction_idempotency_postgres.sh
- name: Require clean checkout
run: |
git diff --exit-code
test -z "$(git status --porcelain)"
154 changes: 154 additions & 0 deletions database/migrations/0018_assignment_category_supersession.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
-- Persist immutable Assignment category-correction lineage as Orgmetra HRIS truth.
--
-- An Assignment correction is a system-time replacement, never an in-place
-- business rewrite. The predecessor must already be closed at the correction
-- timestamp, the replacement must start at that same timestamp, all business
-- truth except category must be identical, and the two explicit categories must
-- differ. The normalized one-to-one edge prevents unlinked duplicates and forks.

BEGIN;

SET LOCAL search_path = public, pg_catalog;

CREATE TABLE public.assignment_supersession_record (
tenant_record_id uuid NOT NULL REFERENCES public.tenant_record(tenant_record_id),
assignment_supersession_record_id uuid PRIMARY KEY,
predecessor_assignment_record_id uuid NOT NULL,
replacement_assignment_record_id uuid NOT NULL,
recorded_at timestamptz NOT NULL,
CONSTRAINT assignment_supersession_record_id_operational_check
CHECK (public.is_operational_uuid(assignment_supersession_record_id)),
CONSTRAINT assignment_supersession_predecessor_id_operational_check
CHECK (public.is_operational_uuid(predecessor_assignment_record_id)),
CONSTRAINT assignment_supersession_replacement_id_operational_check
CHECK (public.is_operational_uuid(replacement_assignment_record_id)),
CONSTRAINT assignment_supersession_distinct_assignment_check
CHECK (predecessor_assignment_record_id <> replacement_assignment_record_id),
CONSTRAINT assignment_supersession_predecessor_tenant_fk
FOREIGN KEY (tenant_record_id, predecessor_assignment_record_id)
REFERENCES public.assignment_record(tenant_record_id, assignment_record_id),
CONSTRAINT assignment_supersession_replacement_tenant_fk
FOREIGN KEY (tenant_record_id, replacement_assignment_record_id)
REFERENCES public.assignment_record(tenant_record_id, assignment_record_id),
CONSTRAINT assignment_supersession_tenant_identity_unique
UNIQUE (tenant_record_id, assignment_supersession_record_id),
CONSTRAINT assignment_supersession_predecessor_unique
UNIQUE (tenant_record_id, predecessor_assignment_record_id),
CONSTRAINT assignment_supersession_replacement_unique
UNIQUE (tenant_record_id, replacement_assignment_record_id)
);

CREATE FUNCTION public.enforce_assignment_supersession_link()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = pg_catalog, public
AS $$
DECLARE
predecessor_record public.assignment_record%ROWTYPE;
replacement_record public.assignment_record%ROWTYPE;
BEGIN
SELECT assignment.*
INTO predecessor_record
FROM public.assignment_record AS assignment
WHERE assignment.tenant_record_id = NEW.tenant_record_id
AND assignment.assignment_record_id = NEW.predecessor_assignment_record_id
FOR SHARE;

IF NOT FOUND THEN
RAISE EXCEPTION 'assignment supersession predecessor does not exist in tenant scope'
USING ERRCODE = 'foreign_key_violation',
CONSTRAINT = 'assignment_supersession_predecessor_tenant_fk',
TABLE = 'assignment_supersession_record',
SCHEMA = 'public';
END IF;

SELECT assignment.*
INTO replacement_record
FROM public.assignment_record AS assignment
WHERE assignment.tenant_record_id = NEW.tenant_record_id
AND assignment.assignment_record_id = NEW.replacement_assignment_record_id
FOR SHARE;

IF NOT FOUND THEN
RAISE EXCEPTION 'assignment supersession replacement does not exist in tenant scope'
USING ERRCODE = 'foreign_key_violation',
CONSTRAINT = 'assignment_supersession_replacement_tenant_fk',
TABLE = 'assignment_supersession_record',
SCHEMA = 'public';
END IF;

IF predecessor_record.recorded_to IS DISTINCT FROM NEW.recorded_at
OR replacement_record.recorded_from IS DISTINCT FROM NEW.recorded_at
OR replacement_record.recorded_to IS NOT NULL THEN
RAISE EXCEPTION 'assignment supersession recorded timestamp must equal predecessor close and replacement start'
USING ERRCODE = 'check_violation',
CONSTRAINT = 'assignment_supersession_recorded_time_check',
TABLE = 'assignment_supersession_record',
SCHEMA = 'public';
END IF;

IF predecessor_record.employment_record_id IS DISTINCT FROM replacement_record.employment_record_id
OR predecessor_record.person_record_id IS DISTINCT FROM replacement_record.person_record_id
OR predecessor_record.position_record_id IS DISTINCT FROM replacement_record.position_record_id
OR predecessor_record.allocation_ratio IS DISTINCT FROM replacement_record.allocation_ratio
OR predecessor_record.effective_from IS DISTINCT FROM replacement_record.effective_from
OR predecessor_record.effective_to IS DISTINCT FROM replacement_record.effective_to THEN
RAISE EXCEPTION 'assignment supersession replacement changed business truth outside category'
USING ERRCODE = 'check_violation',
CONSTRAINT = 'assignment_supersession_business_truth_check',
TABLE = 'assignment_supersession_record',
SCHEMA = 'public';
END IF;

IF predecessor_record.assignment_category_code NOT IN ('primary', 'concurrent_secondary')
OR replacement_record.assignment_category_code NOT IN ('primary', 'concurrent_secondary')
OR predecessor_record.assignment_category_code = replacement_record.assignment_category_code THEN
RAISE EXCEPTION 'assignment supersession must change one explicit assignment category to the other'
USING ERRCODE = 'check_violation',
CONSTRAINT = 'assignment_supersession_category_change_check',
TABLE = 'assignment_supersession_record',
SCHEMA = 'public';
END IF;

RETURN NEW;
END;
$$;

COMMENT ON FUNCTION public.enforce_assignment_supersession_link() IS
'Validates close-to-replacement Assignment category lineage against locked tenant-local HRIS facts.';

CREATE TRIGGER assignment_supersession_link_guard
BEFORE INSERT ON public.assignment_supersession_record
FOR EACH ROW
EXECUTE FUNCTION public.enforce_assignment_supersession_link();

CREATE TRIGGER assignment_supersession_append_only_guard
BEFORE UPDATE OR DELETE ON public.assignment_supersession_record
FOR EACH ROW
EXECUTE FUNCTION public.reject_append_only_mutation();

CREATE FUNCTION public.reject_assignment_supersession_truncate()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = pg_catalog, public, pg_temp
AS $$
BEGIN
RAISE EXCEPTION 'assignment supersession records cannot be truncated'
USING ERRCODE = '55000';
END;
$$;

CREATE TRIGGER assignment_supersession_truncate_guard
BEFORE TRUNCATE ON public.assignment_supersession_record
FOR EACH STATEMENT
EXECUTE FUNCTION public.reject_assignment_supersession_truncate();

REVOKE TRUNCATE ON public.assignment_supersession_record FROM PUBLIC;

ALTER TABLE public.assignment_supersession_record ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.assignment_supersession_record FORCE ROW LEVEL SECURITY;
CREATE POLICY assignment_supersession_scope_policy ON public.assignment_supersession_record
USING (tenant_record_id = public.current_tenant_record_id())
WITH CHECK (tenant_record_id = public.current_tenant_record_id());

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- Extend the durable People mutation replay vocabulary for Assignment category corrections.
--
-- Correction retries reuse the existing tenant-scoped idempotency ledger. The
-- semantic digest is bound to the predecessor and reviewed correction meaning,
-- while created_record_id stores the first committed replacement Assignment.

BEGIN;

SET LOCAL search_path = public, pg_catalog;

ALTER TABLE public.people_mutation_idempotency_record
DROP CONSTRAINT people_mutation_idempotency_route_check;

ALTER TABLE public.people_mutation_idempotency_record
ADD CONSTRAINT people_mutation_idempotency_route_check
CHECK (
command_route IN (
'candidate-worker-conversions',
'employment-records',
'position-records',
'assignment-records',
'assignment-category-corrections'
)
) NOT VALID;

ALTER TABLE public.people_mutation_idempotency_record
VALIDATE CONSTRAINT people_mutation_idempotency_route_check;

COMMIT;
Loading
Loading