Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
371a0f9
test(candidate-withdrawal): define governed withdrawal evidence boundary
seonghobae Aug 21, 2026
a9107d6
test(candidate-withdrawal): wire exact-head PostgreSQL regression
seonghobae Aug 21, 2026
c353166
feat(candidate-withdrawal): persist candidate-initiated immutable evi…
seonghobae Aug 21, 2026
b93f521
test(candidate-withdrawal): isolate governed happy-path evidence
seonghobae Aug 21, 2026
79c1fa4
test(candidate-withdrawal): prove anti-forgery and tenant isolation
seonghobae Aug 21, 2026
fb2005f
test(candidate-withdrawal): run focused security regression
seonghobae Aug 21, 2026
40c0da9
docs(candidate-withdrawal): record governed withdrawal decision
seonghobae Aug 21, 2026
ade9da8
docs(candidate-withdrawal): trace authority and audit evidence
seonghobae Aug 21, 2026
cf59b30
docs(candidate-withdrawal): record primary-source references
seonghobae Aug 21, 2026
79d1f62
docs(adr): index active decision records and reseal foundation manifest
seonghobae Aug 25, 2026
f2750f4
fix(withdrawal): bind audit provenance fields
seonghobae Aug 28, 2026
5ab9416
chore(withdrawal): sync normalized application base
seonghobae Aug 28, 2026
ccaabeb
test(withdrawal): align fixture with normalized application
seonghobae Aug 28, 2026
687231a
test(withdrawal): cover provenance artifacts
seonghobae Aug 28, 2026
18bf097
test(withdrawal): remove stale fixture values
seonghobae Aug 28, 2026
f0afa82
fix(candidate-withdrawal): migrate audit envelope validator forward
seonghobae Aug 28, 2026
b855af9
fix(workflow): trigger withdrawal migration upgrade checks
seonghobae Aug 29, 2026
4674809
Merge remote-tracking branch 'origin/feat/normalized-candidate-applic…
seonghobae Aug 29, 2026
65d8627
docs(adr): align normalized application maturity
seonghobae Aug 29, 2026
ee5a56d
fix(withdrawal): own recorded evidence time
seonghobae Aug 29, 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
107 changes: 107 additions & 0 deletions .github/workflows/candidate-withdrawal-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Candidate Withdrawal Quality

on:
pull_request:
branches:
- develop
- feat/normalized-candidate-application
paths:
- "database/migrations/**"
- "tests/test_candidate_withdrawal_postgres.sh"
- "tests/test_candidate_withdrawal_security_postgres.sh"
- "tests/test_candidate_withdrawal_migration_upgrade_postgres.sh"
- ".github/workflows/candidate-withdrawal-quality.yml"
- "docs/adr/0027-governed-candidate-withdrawal.md"
- "docs/traceability/candidate-withdrawal.md"
- "docs/doctoring/candidate-withdrawal-references.md"
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
workflow_dispatch:

permissions:
contents: read

concurrency:
group: candidate-withdrawal-quality-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
postgres_contract:
name: Candidate withdrawal PostgreSQL contract
runs-on: ubuntu-latest
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 test syntax
run: >-
bash -n
tests/test_candidate_withdrawal_postgres.sh
tests/test_candidate_withdrawal_security_postgres.sh
tests/test_candidate_withdrawal_migration_upgrade_postgres.sh
- name: Prove governed candidate withdrawal persistence
run: bash tests/test_candidate_withdrawal_postgres.sh
- name: Prove anti-forgery and tenant isolation
run: bash tests/test_candidate_withdrawal_security_postgres.sh
- name: Prove validation is read-only
run: |
git diff --exit-code
test -z "$(git status --porcelain)"

postgres_migration_upgrade_contract:
name: Candidate withdrawal migration upgrade contract
runs-on: ubuntu-latest
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 migration upgrade path
run: bash tests/test_candidate_withdrawal_migration_upgrade_postgres.sh
- name: Prove validation is read-only
run: |
git diff --exit-code
test -z "$(git status --porcelain)"
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ All notable changes to Orgmetra will be documented in this file.

- Accepted ADRs 0001–0003 now include buyer-facing Context, Decision, and Consequences grounded in verified ISO 30400:2022, ISO 30414:2025, Uniform Guidelines (29 C.F.R. Part 1607), SIOP (2018), OpenAPI Specification v3.2.0, OpenID Connect Core 1.0 errata set 2, CloudEvents v1.0.2, Jensen and Snodgrass (1999), Snodgrass (1999), and Allen (1983) records already listed in `docs/doctoring/REFERENCES.md`. ADRs 0004 and 0005 gained APA 7th References pointers to that same bibliography without changing their Decision bodies.
- Active-PR governed Job Analysis persistence/API on the canonical `JobAnalysisSnapshot` model: migration `0013_job_analysis_snapshot.sql` stores immutable tenant-scoped snapshot, Task, KSAO, Task–KSAO, FJA and write-command evidence; `POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots` and matching GET enforce purpose-bound Keyverse scope, authenticated-principal actor authority, bounded/strict JSON handling, transactional Idempotency-Key serialization, parent-scope fail-closed integrity, forced RLS, and atomic audit/outbox evidence. ADR 0014 records the persistence decision while ADR 0007 remains the domain/evidence authority; validated evidence still requires accountable human review and non-LLM provenance, and the service does not make a high-impact employment decision.
- Active-PR candidate-withdrawal persistence now binds identity-resolution reference/digest, withdrawal evidence reference/digest, and evidence version into the candidate-specific immutable audit envelope as well as the append-only withdrawal fact; generic audit envelopes retain their exact existing data contract.
- Active-PR candidate-withdrawal persistence now delivers its audit-envelope extension through forward-only migration `0016_candidate_withdrawal_audit_envelope.sql`, preserving already-applied migration `0008` while proving the legacy-to-upgraded validator path in a disposable PostgreSQL contract.
- Active-PR candidate-withdrawal persistence now assigns `recorded_at` from PostgreSQL transaction time at the insert trigger, rejecting caller-supplied backdated or future system-recorded evidence while preserving candidate-declared `withdrawn_at` chronology.
- Active-PR `orgmetra_selection_review` packet for PII-minimized, evidence-bound human selection review: canonical operational tenant identity, UUID-backed opaque candidate/Job/sealed-evidence/reviewer references, explicit purpose/reason/evidence version, deterministic canonical JSON and SHA-256 correlation, mandatory human decision state, redacted packet repr, and provenance-paired model evidence that remains `untrusted_draft`, with exact 100% owned statement and branch coverage required by its quality gate.
- Active performance-criterion scope hardening: `criterion_observation_scope_guard` rejects criterion outcomes for a Job the worker did not effectively hold at the observation date, observations before the relevant assignment, and observations outside the referenced performance cycle while preserving valid multiple-assignment cases and existing bitemporal correction semantics. The guard evaluates current-recorded facts, derives the date coordinate from `observed_at` in UTC so session `TimeZone` cannot alter the result, uses a trusted function search path, and adds no PII or automated employment decision authority. The Foundation PostgreSQL contract also rejects a closed `recorded_to` on each time-coordinate lookup and proves UTC midnight plus non-UTC session `TimeZone` boundaries.
- Bitemporal tenant-scoped organization hierarchy validation that rejects visible indirect parent cycles and reuses single-valued recorded-time reconstruction before graph traversal.
Expand Down
4 changes: 4 additions & 0 deletions database/migrations/0014_candidate_application_core.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ ALTER TABLE public.position_record
ADD CONSTRAINT position_record_tenant_position_job_unique
UNIQUE (tenant_record_id, position_record_id, job_profile_id);

-- Table creation stays search-path-qualified rather than schema-qualified because
-- the repository foundation validator applies the same descriptive two-word
-- snake_case rule to an explicit schema token. SET LOCAL search_path above keeps
-- these objects owned by public without weakening that repository-wide check.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
-- The durable application identity is immutable. The candidate/requisition pair
-- identifies the application; correctable Job/Position scope belongs to the
-- bitemporal version relation below so stage history never has to change anchor.
Expand Down
195 changes: 195 additions & 0 deletions database/migrations/0015_candidate_withdrawal_governance.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
-- Govern candidate-initiated application withdrawal without reusing raw workflow
-- stage codes for a terminal action. The application layer remains responsible
-- for authenticating the candidate; this persistence boundary requires the
-- resulting identity-resolution evidence, candidate actor, withdrawal evidence,
-- and exact immutable audit/outbox correlation to agree before accepting a row.

BEGIN;

SET LOCAL search_path = public, pg_catalog;

CREATE TABLE candidate_withdrawal_record (
tenant_record_id uuid NOT NULL REFERENCES public.tenant_record(tenant_record_id),
candidate_withdrawal_record_id uuid PRIMARY KEY,
candidate_application_record_id uuid NOT NULL,
initiating_actor_reference text NOT NULL,
identity_resolution_reference text NOT NULL,
identity_resolution_digest text NOT NULL,
withdrawal_evidence_reference text NOT NULL,
withdrawal_evidence_digest text NOT NULL,
evidence_version integer NOT NULL,
withdrawn_at timestamptz NOT NULL,
audit_event_record_id uuid NOT NULL,
recorded_at timestamptz NOT NULL DEFAULT pg_catalog.transaction_timestamp(),
CONSTRAINT candidate_withdrawal_record_id_operational_check
CHECK (
candidate_withdrawal_record_id <> '00000000-0000-0000-0000-000000000000'::uuid
AND candidate_withdrawal_record_id <> 'ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid
),
CONSTRAINT candidate_withdrawal_application_tenant_fk
FOREIGN KEY (tenant_record_id, candidate_application_record_id)
REFERENCES public.candidate_application_record(
tenant_record_id, candidate_application_record_id
),
CONSTRAINT candidate_withdrawal_audit_tenant_fk
FOREIGN KEY (tenant_record_id, audit_event_record_id)
REFERENCES public.audit_event_record(tenant_record_id, audit_event_record_id),
CONSTRAINT candidate_withdrawal_actor_reference_check
CHECK (
initiating_actor_reference ~
'^candidate:[A-Za-z0-9][A-Za-z0-9._~-]*$'
),
CONSTRAINT candidate_withdrawal_identity_reference_check
CHECK (
identity_resolution_reference ~
'^identity_resolution:[A-Za-z0-9][A-Za-z0-9._~-]*$'
),
CONSTRAINT candidate_withdrawal_identity_digest_check
CHECK (identity_resolution_digest ~ '^[0-9a-f]{64}$'),
CONSTRAINT candidate_withdrawal_evidence_reference_check
CHECK (
withdrawal_evidence_reference ~
'^candidate_withdrawal_evidence:[A-Za-z0-9][A-Za-z0-9._~-]*$'
),
CONSTRAINT candidate_withdrawal_evidence_digest_check
CHECK (withdrawal_evidence_digest ~ '^[0-9a-f]{64}$'),
CONSTRAINT candidate_withdrawal_evidence_version_check
CHECK (evidence_version >= 1 AND evidence_version <= 1000000),
CONSTRAINT candidate_withdrawal_recorded_order_check
CHECK (withdrawn_at <= recorded_at),
CONSTRAINT candidate_withdrawal_tenant_identity_unique
UNIQUE (tenant_record_id, candidate_withdrawal_record_id),
CONSTRAINT candidate_withdrawal_application_unique
UNIQUE (tenant_record_id, candidate_application_record_id),
CONSTRAINT candidate_withdrawal_audit_identity_unique
UNIQUE (tenant_record_id, audit_event_record_id)
);

CREATE FUNCTION public.validate_candidate_withdrawal_evidence()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = pg_catalog, public, pg_temp
AS $$
DECLARE
application_submitted_at timestamptz;
audit_event_envelope jsonb;
audit_event_time timestamptz;
BEGIN
-- recorded_at is database-owned evidence, not a caller-provided timestamp.
NEW.recorded_at := pg_catalog.transaction_timestamp();

SELECT application_record.submitted_at
INTO application_submitted_at
FROM public.candidate_application_record AS application_record
WHERE application_record.tenant_record_id = NEW.tenant_record_id
AND application_record.candidate_application_record_id =
NEW.candidate_application_record_id;

IF NOT FOUND THEN
RAISE EXCEPTION 'candidate withdrawal requires a tenant-local application'
USING ERRCODE = '23503';
END IF;

IF NEW.withdrawn_at < application_submitted_at THEN
RAISE EXCEPTION 'candidate withdrawal cannot predate application submission'
USING ERRCODE = '23514';
END IF;

SELECT audit_record.canonical_event_json::jsonb
INTO audit_event_envelope
FROM public.audit_event_record AS audit_record
WHERE audit_record.tenant_record_id = NEW.tenant_record_id
AND audit_record.audit_event_record_id = NEW.audit_event_record_id;

IF NOT FOUND THEN
RAISE EXCEPTION 'candidate withdrawal requires a tenant-local immutable audit event'
USING ERRCODE = '23503';
END IF;

audit_event_time := (audit_event_envelope ->> 'time')::timestamptz;

IF audit_event_envelope ->> 'source' <> 'urn:orgmetra:talent_acquisition'
OR audit_event_envelope ->> 'type' <> 'orgmetra.candidate.application_withdrawn'
OR audit_event_envelope ->> 'subject'
<> 'candidate_withdrawal_record:' || NEW.candidate_withdrawal_record_id::text
OR audit_event_envelope ->> 'orgmetraactor' <> NEW.initiating_actor_reference
OR audit_event_envelope ->> 'orgmetrapurpose' <> 'candidate_withdrawal'
OR audit_event_envelope ->> 'orgmetrareason' <> 'candidate_requested'
OR audit_event_envelope ->> 'orgmetraevidence' <> NEW.withdrawal_evidence_reference
OR audit_event_envelope #>> '{data,identity_resolution_reference}'
<> NEW.identity_resolution_reference
OR audit_event_envelope #>> '{data,identity_resolution_digest}'
<> NEW.identity_resolution_digest
OR audit_event_envelope #>> '{data,withdrawal_evidence_digest}'
<> NEW.withdrawal_evidence_digest
OR audit_event_envelope #>> '{data,evidence_version}'
<> NEW.evidence_version::text
OR audit_event_envelope ? 'orgmetraconfirmation'
OR (audit_event_envelope #>> '{data,high_impact}')::boolean IS NOT FALSE
OR audit_event_envelope #>> '{data,result_code}' <> 'application_withdrawn'
OR audit_event_time <> NEW.withdrawn_at
OR audit_event_time > NEW.recorded_at THEN
RAISE EXCEPTION 'candidate withdrawal audit envelope does not bind exact candidate provenance'
USING ERRCODE = '23514';
END IF;
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

IF NOT EXISTS (
SELECT 1
FROM public.outbox_delivery_record AS delivery_record
WHERE delivery_record.tenant_record_id = NEW.tenant_record_id
AND delivery_record.audit_event_record_id = NEW.audit_event_record_id
) THEN
RAISE EXCEPTION 'candidate withdrawal audit event requires transactional outbox delivery evidence'
USING ERRCODE = '23514';
END IF;

RETURN NEW;
END;
$$;

CREATE TRIGGER candidate_withdrawal_governance_guard
BEFORE INSERT ON candidate_withdrawal_record
FOR EACH ROW
EXECUTE FUNCTION public.validate_candidate_withdrawal_evidence();
Comment thread
seonghobae marked this conversation as resolved.

CREATE FUNCTION public.reject_candidate_withdrawal_mutation()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = pg_catalog, public, pg_temp
AS $$
BEGIN
RAISE EXCEPTION 'candidate withdrawal evidence is append-only'
USING ERRCODE = '55000';
END;
$$;

CREATE TRIGGER candidate_withdrawal_append_only_guard
BEFORE UPDATE OR DELETE ON candidate_withdrawal_record
FOR EACH ROW
EXECUTE FUNCTION public.reject_candidate_withdrawal_mutation();

CREATE FUNCTION public.reject_candidate_withdrawal_truncate()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = pg_catalog, public, pg_temp
AS $$
BEGIN
RAISE EXCEPTION 'candidate withdrawal evidence cannot be truncated'
USING ERRCODE = '55000';
END;
$$;

CREATE TRIGGER candidate_withdrawal_truncate_guard
BEFORE TRUNCATE ON candidate_withdrawal_record
FOR EACH STATEMENT
EXECUTE FUNCTION public.reject_candidate_withdrawal_truncate();

REVOKE TRUNCATE ON candidate_withdrawal_record FROM PUBLIC;

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

COMMIT;
Loading