Skip to content

feat(core): persist bitemporal position reporting relationships - #106

Open
seonghobae wants to merge 30 commits into
feat/position-reporting-hierarchyfrom
feat/position-reporting-persistence
Open

feat(core): persist bitemporal position reporting relationships#106
seonghobae wants to merge 30 commits into
feat/position-reporting-hierarchyfrom
feat/position-reporting-persistence

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Buyer-visible gap

PR #94 defines governed Position→Position solid-line reporting snapshots but intentionally leaves durable persistence/mutation out of scope. This stacked Orgmetra-only lane adds normalized bitemporal persistence without deriving supervision from Person, Assignment, or organization-unit parentage.

Governed persistence contract

position_reporting_relationship_record owns stable (tenant, subordinate Position, solid_line) identity and position_reporting_relationship_version owns manager Position plus effective/business time and PostgreSQL-owned system-recorded time. Persisted versions require same-tenant Position references, distinct human reviewer/applying actor, immutable review/application audit+outbox correlation, staffable PositionVersion coverage for both endpoints across the full effective interval, cycle-free graph state, immutable history/TRUNCATE resistance, and FORCE RLS. Person/Assignment/worker identity, compensation, ratings, assessment output, and free-form HR text are absent. RLS is defense in depth and does not replace purpose-bound application authorization.

Repair sequence

  • Initial RED 78e092353be310f2bcefd30c1f7439b5ed1b235e established database-owned recorded time, one durable solid-line anchor per subordinate, same-tenant scope, self-report/cycle rejection, immutable history, audited human-reviewed application, and FORCE-RLS acceptance criteria.
  • Root implementation 209259120d10b1cd18ab1e6184297f5af4730ffc added the normalized relations and guards. 2e421d8927516dd4b67e469c25ef9cf12107ca34 made self-report/cycle regressions transaction-independent.
  • Review/application integrity was strengthened through 566b74570e479735bbc2fedae7c6b56eb35ff3d9, fc1fb92ad208abdc895b1f94a8d637531e126d2b, affa260b1e94e0040fdfcc59f7bcdad5117bd9a4, and cfef3e27258104b2de448580777271e8ac7d16bf: the audit event's orgmetraevidence must equal the reviewed-evidence digest and stored application evidence must equal the immutable audit envelope digest exactly.
  • Concurrency RED 69923053bb8f39fbe9ea876c33443c38b5ef4b63 demonstrated that concurrent opposite edges need serialization; df13397ded4dfa6b605b7ded7cfb7e4468d3f0fc added a transaction-scoped tenant graph advisory lock before graph reads.
  • Staffable endpoint RED 8f0a874761bcf1f84e7d0544fabf9394178773ba showed persistence accepted Position anchors with no staffable PositionVersion coverage. Root repair facfff9d91243fe06639d60a978cb5072cfec64f added position_reporting_has_staffable_coverage(...), using PostgreSQL 16 multirange containment so same-tenant active/open PositionVersion ranges must cover the relationship's entire effective interval.
  • Concurrency-fixture repair 61611908b49a5ff74f2b7f10a2883a2572070b54 added active PositionVersion fixtures for the X/Y race endpoints. Exact-head Position Reporting Persistence Quality run 32688382859, job 97317512146, completed GREEN and proved exact checkout, deterministic provenance, primary persistence regressions, immutable review binding, two-session cycle prevention, and a clean checkout.
  • Audit-semantics regression 2af25408d95ac9bb362f318d218af507c0861f8b added a valid immutable audit/outbox event whose purpose/actor/evidence/subject/result/time all matched the requested mutation but whose governed orgmetrareason was unrelated_change. The RED-head hosted run 32692438893 was cancelled after the branch advanced and is not claimed as terminal RED evidence.
  • Root repair ef8c7776b2520e6839c14da8122aacfe3f6ad1e4 now requires orgmetrareason = approved_reporting_line_change in the authoritative insert guard. ADR 0106 and traceability were aligned in 8ede14cc7430d83955c38c867d1521989a557ea7 and current head c35ad114edbce7a4ebafcea793748493f1346351.

Current exact state

Current exact head: c35ad114edbce7a4ebafcea793748493f1346351.
Parent base: #94 feat/position-reporting-hierarchy@3f67182bb3065f2fc8fd974bfdd75a390d8a8fdc.
GitHub reports this PR open, Draft, and mergeable against its stack base.

Exact-current-head Position Reporting Persistence Quality run 32692576503 is terminal GREEN. It is lane-local stack evidence only: #106 still lacks post-parent-integration Foundation/SAST/Security/Recovery/product evidence against fresh protected develop, and no predecessor #94 check or review is transferred. Fresh submitted-review and review-thread queries are empty.

Stack and merge governance

This PR is intentionally stacked on #94. #94 must integrate first; its checks/reviews do not transfer. After #94 integrates, retarget #106 to fresh develop, reconcile migration/document ordering, refetch exact head/base/rules/reviews/threads, and rerun all applicable Foundation/SAST/Security/Recovery/product evidence on the resulting head before review readiness.

PR #95 remains the separate Orgmetra owner for the pre-mutation review packet; this branch consumes only reviewed-evidence digest plus immutable application audit correlation and does not rewrite #95. No dedicated-writer dependency repository or cross-service application table is modified.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 12a27864-575c-4c7a-8df7-35cc6d4ea66e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@seonghobae
seonghobae marked this pull request as ready for review August 25, 2026 23:30

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

Open in Devin Review

Comment on lines +206 to +218
BEGIN
-- Graph validity is a tenant-wide invariant. Concurrent opposite edge
-- inserts can otherwise both validate before either commits. A transaction
-- advisory lock serializes graph mutations for one tenant while still
-- allowing different tenants to progress independently. This trigger is
-- VOLATILE, so the queries after lock acquisition use current committed
-- state under PostgreSQL READ COMMITTED semantics.
PERFORM pg_catalog.pg_advisory_xact_lock(
pg_catalog.hashtextextended(
'orgmetra_position_reporting:' || NEW.tenant_record_id::text,
0
)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Cycle detection depends on VOLATILE snapshot behavior

Concurrency safety in enforce_position_reporting_scope rests on the trigger being VOLATILE, so the recursive graph query re-snapshots after the advisory lock and sees the committed opposite edge under READ COMMITTED. Correct as written, but fragile: changing the function to STABLE would silently break serialization.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +33 to +51
with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<SQL
INSERT INTO position_record (
tenant_record_id, position_record_id, organization_unit_id, job_profile_id
) VALUES
('${TENANT_ID}', '${POSITION_X}', '${ORG_ID}', '${JOB_ID}'),
('${TENANT_ID}', '${POSITION_Y}', '${ORG_ID}', '${JOB_ID}');
INSERT INTO position_record_version (
tenant_record_id, position_record_version_id, position_record_id,
position_status_code, effective_from
) VALUES
('${TENANT_ID}', '${POSITION_X_VERSION}', '${POSITION_X}', 'active', DATE '2026-01-01'),
('${TENANT_ID}', '${POSITION_Y_VERSION}', '${POSITION_Y}', 'active', DATE '2026-01-01');
INSERT INTO position_reporting_relationship_record (
tenant_record_id, position_reporting_relationship_record_id,
subordinate_position_record_id, relationship_type_code
) VALUES
('${TENANT_ID}', '${REL_X}', '${POSITION_X}', 'solid_line'),
('${TENANT_ID}', '${REL_Y}', '${POSITION_Y}', 'solid_line');
SQL

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Tests share DB state and must run in fixed order

test_position_reporting_review_binding_postgres.sh and test_position_reporting_concurrency_postgres.sh reuse tenant, org, job, and version rows inserted by test_position_reporting_persistence_postgres.sh on the shared Postgres service. They only pass in the workflow's fixed step order; run in isolation they fail on missing prerequisites.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +138 to +146
set +e
(with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c "${sql_x}" >"${log_x}" 2>&1) &
pid_x=$!
sleep 0.5
with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c "${sql_y}" >"${log_y}" 2>&1
status_y=$?
wait "${pid_x}"
status_x=$?
set -e

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Concurrency test winner is timing-dependent

The test assumes the backgrounded X transaction grabs the advisory lock within the 0.5s head start before Y runs. On a slow runner Y could win the lock first, flipping which transaction hits the cycle error and producing a spurious failure. The 2s pg_sleep makes this unlikely but not guaranteed.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


PR #95 owns the in-memory pre-mutation review packet. This persistence slice does not copy or modify that branch; it accepts the review digest plus immutable application audit evidence as the handoff boundary. A later authorized host adapter may translate a verified review packet into the database command, but direct cross-service SQL is out of scope.

Canonical `docs/DATA_MODEL.md` / `docs/ERD.md` are intentionally not edited in this stacked slice while independent active database PRs also own those high-conflict documents. Integration must reconcile the accepted relationship tables into canonical data-model documentation after dependency ordering is resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Canonical data-model docs not updated for new tables

The two new tables are not reflected in docs/DATA_MODEL.md or docs/ERD.md; the ADR defers this to integration to avoid conflicts with concurrent database PRs. Reviewer should confirm reconciliation happens at integration so the canonical data-model docs stay current with the schema.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant