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
34 changes: 34 additions & 0 deletions .github/workflows/criterion-temporal-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
- main
paths:
- "database/migrations/0011_criterion_observation_scope.sql"
- "database/migrations/0014_criterion_observation_chronology.sql"
- "database/migrations/0015_criterion_observation_statement_default.sql"
- "tests/test_criterion_observation_scope_postgres.sh"
- ".github/workflows/criterion-temporal-quality.yml"
workflow_dispatch:
Expand Down Expand Up @@ -52,6 +54,38 @@ jobs:
run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA"
- name: Prove recorded-time and UTC boundaries
run: bash tests/test_criterion_observation_scope_postgres.sh
- name: Reject observations recorded before they occurred
env:
PGOPTIONS: -c orgmetra.tenant_record_id=10000000-0000-7000-8000-000000000101
run: |
set +e
output="$(psql "$DATABASE_URL" -v ON_ERROR_STOP=1 <<'SQL' 2>&1
INSERT INTO criterion_observation (
tenant_record_id, criterion_observation_id, criterion_blueprint_id,
performance_cycle_id, person_record_id, observed_value,
observed_at, recorded_from
) VALUES (
'10000000-0000-7000-8000-000000000101',
'10000000-0000-7000-8000-000000000134',
'10000000-0000-7000-8000-000000000110',
'10000000-0000-7000-8000-000000000109',
'10000000-0000-7000-8000-000000000102',
4.8,
TIMESTAMPTZ '2026-08-18 12:00:00+00',
TIMESTAMPTZ '2026-08-17 12:00:00+00'
);
SQL
)"
status=$?
set -e
if [[ $status -eq 0 ]]; then
echo "criterion observation accepted an observed_at after its system-recorded time" >&2
exit 1
fi
if [[ "$output" != *"criterion observation cannot be recorded before it was observed"* ]]; then
echo "future criterion observation failed for an unexpected reason: $output" >&2
exit 1
fi
- name: Require clean checkout
run: |
git diff --exit-code
Expand Down
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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 `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.
- Active performance-criterion scope hardening: migration `0014_criterion_observation_chronology.sql` upgrades the already-installed `0011` trigger function without rewriting its migration or replacing the trigger binding, and migration `0015_criterion_observation_statement_default.sql` changes omitted observation `recorded_from` values from transaction time to statement time for long-transaction correctness. `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, observations recorded before `observed_at`, future observations or recording timestamps relative to database statement time, 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, proves UTC midnight plus non-UTC session `TimeZone` boundaries, and accepts a valid observation inserted after a transaction has already begun.
- Bitemporal tenant-scoped organization hierarchy validation that rejects visible indirect parent cycles and reuses single-valued recorded-time reconstruction before graph traversal.
- Stacked governed job-analysis evidence contract via `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `TaskKSAOLink`, `FunctionalJobAnalysisProfile`, and `EvidenceSource`: tenant/Job-scoped observable tasks, explicit Task-to-KSAO linkage, importance/difficulty/proficiency ratings, source/version/retrieval/SHA-256 provenance, deterministic canonical snapshot bytes, current O*NET evidence support, and historical DOT Data/People/Things compatibility. Validated snapshots require accountable human review and complete non-LLM evidence; LLM-origin material remains `analysis_draft`, and the snapshot is evidence input rather than a hiring, promotion, termination, compensation, or other high-impact employment decision.
- Stacked governed audit/outbox slice via `AuditOutboxEvent`, `audit_event_record`, `outbox_delivery_record`, and `outbox_delivery_escalation_record`: CloudEvents 1.0-compatible PII-minimized metadata, exact canonical JSON bytes, database-verified SHA-256 digests, mandatory human confirmation for high-impact events, immutable audit evidence, tenant RLS, atomic audit/outbox insertion, guarded pending/leased/delivered/dead-lettered delivery state, tenant-safe `claim_outbox_delivery(...)` with deterministic due-work ordering, `FOR UPDATE ... SKIP LOCKED`, opaque worker identity, bounded future leases, immutable envelope return, and atomic takeover of genuinely expired leases only while retry attempts remain; owner-bound `complete_outbox_delivery(...)` and `retry_outbox_delivery(...)`; database-budget-governed `dead_letter_outbox_delivery(...)`; and a separately privileged `operator_dead_letter_expired_outbox_delivery(...)` recovery path for an exhausted final lease whose recorded worker identity is permanently unavailable. `maximum_attempt_count` is persisted on the delivery row, defaults to 5, is constrained to 1 through 100, and cannot be lowered by a dispatcher during finalization. Migration 0007 prevents retry or expired-lease takeover from creating attempt N+1; migration 0008 adds TRUNCATE guards, trusted function search paths, a concurrently built due-work partial index, session-independent immutable envelope validation, and operator recovery backed by separate NOLOGIN/NOBYPASSRLS owner/capability roles so the externally assignable operator role can invoke recovery without receiving direct transport-table read/write rights. Migration 0008 also rejects pre-existing reserved recovery-role names before project DDL, atomically contains the temporary schema-creation privilege used for function ownership handoff, and forces deferred escalation binding while the narrow SECURITY DEFINER owner is still active. Exponential/backoff policy selection, policy-specific producer configuration, and external delivery receipts remain subsequent work.
Expand Down Expand Up @@ -73,4 +73,4 @@ All notable changes to Orgmetra will be documented in this file.

### Notes

- Protected `develop` at `e7ddb7a78a5e1460410005d10f43ebf18c5e12e4` includes normalized validity-study and criterion integrity, bitemporal workforce composition, governed candidate-to-worker conversion, purpose-bound PII authorization, GET-only People reads, governed People mutation/idempotency API, and the accepted ADR 0001–0003 source expansion integrated by #37. Job Analysis persistence/API and the selection-review packet remain active-PR truth until their unchanged exact heads satisfy fresh gates and merge.
- Protected `develop` at `e7ddb7a78a5e1460410005d10f43ebf18c5e12e4` includes the baseline normalized validity-study and criterion integrity, bitemporal workforce composition, governed candidate-to-worker conversion, purpose-bound PII authorization, GET-only People reads, governed People mutation/idempotency API, and the accepted ADR 0001–0003 source expansion integrated by #37. The recorded-before-observed criterion chronology guard remains active-PR truth until its unchanged exact head satisfies fresh gates and merges. Job Analysis persistence/API and the selection-review packet likewise remain active-PR truth until their unchanged exact heads satisfy fresh gates and merge.
173 changes: 173 additions & 0 deletions database/migrations/0014_criterion_observation_chronology.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
-- Add chronology protection without rewriting the already released 0011 migration.
-- CREATE OR REPLACE preserves criterion_observation_scope_guard's binding while
-- upgrading databases that have already installed enforce_criterion_observation_scope().

CREATE OR REPLACE FUNCTION enforce_criterion_observation_scope()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = pg_catalog, public
Comment thread
seonghobae marked this conversation as resolved.
AS $$
DECLARE
observation_effective_date date;
criterion_job_profile_id uuid;
BEGIN
-- An immutable performance observation cannot truthfully enter system time
-- before the event it claims to have observed. Reject impossible chronology
-- before using the claimed observation instant for any scope lookup.
IF NEW.recorded_from < NEW.observed_at THEN
RAISE EXCEPTION 'criterion observation cannot be recorded before it was observed'
USING ERRCODE = '23514';
END IF;
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.

-- Effective periods in the current foundation are date-granular. Convert
-- the evidence instant through UTC explicitly so session TimeZone cannot
-- move an observation across a date boundary and bypass temporal checks.
observation_effective_date := (NEW.observed_at AT TIME ZONE 'UTC')::date;

SELECT blueprint.job_profile_id
INTO criterion_job_profile_id
FROM criterion_blueprint AS blueprint
WHERE blueprint.tenant_record_id = NEW.tenant_record_id
AND blueprint.criterion_blueprint_id = NEW.criterion_blueprint_id
AND blueprint.effective_from <= observation_effective_date
AND (
blueprint.effective_to IS NULL
OR observation_effective_date < blueprint.effective_to
)
AND blueprint.recorded_from <= statement_timestamp()
AND (
blueprint.recorded_to IS NULL
OR statement_timestamp() < blueprint.recorded_to
);

IF criterion_job_profile_id IS NULL THEN
RAISE EXCEPTION 'criterion observation references a criterion outside its effective or current-recorded period'
USING ERRCODE = '23514';
END IF;

IF NOT EXISTS (
SELECT 1
FROM performance_cycle AS cycle_record
WHERE cycle_record.tenant_record_id = NEW.tenant_record_id
AND cycle_record.performance_cycle_id = NEW.performance_cycle_id
AND cycle_record.effective_from <= observation_effective_date
AND (
cycle_record.effective_to IS NULL
OR observation_effective_date < cycle_record.effective_to
)
AND cycle_record.recorded_from <= statement_timestamp()
AND (
cycle_record.recorded_to IS NULL
OR statement_timestamp() < cycle_record.recorded_to
)
) THEN
RAISE EXCEPTION 'criterion observation is outside the performance cycle effective period'
USING ERRCODE = '23514';
END IF;

IF NOT EXISTS (
SELECT 1
FROM assignment_record AS assignment
JOIN position_record AS position
ON position.tenant_record_id = assignment.tenant_record_id
AND position.position_record_id = assignment.position_record_id
WHERE assignment.tenant_record_id = NEW.tenant_record_id
AND assignment.person_record_id = NEW.person_record_id
AND position.job_profile_id = criterion_job_profile_id
AND assignment.effective_from <= observation_effective_date
AND (
assignment.effective_to IS NULL
OR observation_effective_date < assignment.effective_to
)
AND assignment.recorded_from <= statement_timestamp()
AND (
assignment.recorded_to IS NULL
OR statement_timestamp() < assignment.recorded_to
)
AND position.recorded_from <= statement_timestamp()
AND (
position.recorded_to IS NULL
OR statement_timestamp() < position.recorded_to
)
) THEN
RAISE EXCEPTION 'criterion observation does not match an effective worker assignment for the criterion job'
USING ERRCODE = '23514';
END IF;

-- A stale assignment anchor must not make a terminated employment or a
-- closed/frozen/abolished seat look like valid performance context. Reuse
-- the same status semantics as the HRIS assignment kernel and require one
-- *single* matching assignment to have both eligible employment and a
-- staffable position at the observation coordinate.
IF NOT EXISTS (
SELECT 1
FROM assignment_record AS assignment
JOIN position_record AS position
ON position.tenant_record_id = assignment.tenant_record_id
AND position.position_record_id = assignment.position_record_id
JOIN employment_record_version AS employment_version
ON employment_version.tenant_record_id = assignment.tenant_record_id
AND employment_version.employment_record_id = assignment.employment_record_id
JOIN position_record_version AS position_version
ON position_version.tenant_record_id = assignment.tenant_record_id
AND position_version.position_record_id = assignment.position_record_id
WHERE assignment.tenant_record_id = NEW.tenant_record_id
AND assignment.person_record_id = NEW.person_record_id
AND position.job_profile_id = criterion_job_profile_id
AND assignment.effective_from <= observation_effective_date
AND (
assignment.effective_to IS NULL
OR observation_effective_date < assignment.effective_to
)
AND assignment.recorded_from <= statement_timestamp()
AND (
assignment.recorded_to IS NULL
OR statement_timestamp() < assignment.recorded_to
)
AND position.recorded_from <= statement_timestamp()
AND (
position.recorded_to IS NULL
OR statement_timestamp() < position.recorded_to
)
AND employment_version.employment_status_code IN ('active', 'leave')
AND employment_version.effective_from <= observation_effective_date
AND (
employment_version.effective_to IS NULL
OR observation_effective_date < employment_version.effective_to
)
AND employment_version.recorded_from <= statement_timestamp()
AND (
employment_version.recorded_to IS NULL
OR statement_timestamp() < employment_version.recorded_to
)
AND position_version.position_status_code IN ('active', 'open')
AND position_version.effective_from <= observation_effective_date
AND (
position_version.effective_to IS NULL
OR observation_effective_date < position_version.effective_to
)
AND position_version.recorded_from <= statement_timestamp()
AND (
position_version.recorded_to IS NULL
OR statement_timestamp() < position_version.recorded_to
)
) THEN
RAISE EXCEPTION 'criterion observation lacks an assignment with eligible employment and staffable position coverage'
USING ERRCODE = '23514';
END IF;

-- A caller may order both supplied timestamps after one another while still
-- claiming an event that has not happened at the database's current time.
IF NEW.observed_at > statement_timestamp() THEN
RAISE EXCEPTION 'criterion observation cannot be observed in the future'
USING ERRCODE = '23514';
END IF;
Comment thread
seonghobae marked this conversation as resolved.

IF NEW.recorded_from > statement_timestamp() THEN
RAISE EXCEPTION 'criterion observation cannot be recorded in the future'
USING ERRCODE = '23514';
END IF;
Comment thread
seonghobae marked this conversation as resolved.

RETURN NEW;
END;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Record omitted criterion-observation timestamps at statement time.
-- The transaction timestamp can precede a real observation in a long transaction.

ALTER TABLE criterion_observation
ALTER COLUMN recorded_from SET DEFAULT statement_timestamp();
Comment thread
seonghobae marked this conversation as resolved.
Loading
Loading