MDX Limo
PCRecruiter role sync (PR #3): review and merge plan

PCRecruiter role sync (PR #3): review and merge plan

Reviewed 2026-10-06 against origin/feat/pcrecruiter-order-sync at 943ec6d (12 commits on top of 4198538, +6,014 / −12 lines, 59 files), the draft PR https://github.com/job-com-org/job-com/pull/3 by Max (Molret), the Supabase preview branch pcr-order-sync (project ref foolqzjouuwgleuvamdw, linked to the PR) and production (pjupiksxikbttgqnipaa). Max's message of 7:30 PM is decoded in §3.

1. Verdict

Not mergeable today. Mergeable after the seven blockers in §5.1, which are small, plus the production adoption steps in §8. The engineering is strong: the worker is read-only against PCR, every write goes through one transactional SQL command with per-row quarantine, identity is PCR JobId (not the reusable portal requisition id), closures need two direct 404s, and the branch ships a reproducible database chain with CI that replays it. The preview reconciles all 4,690 current PCR orders with zero diff on replay.

What is missing is the last mile into production and into the Desk UI:

  • The CI "Workspace checks" job is red (lockfile formatting), so lint, types, tests and builds never ran in CI. I ran them locally; they pass (§4).
  • The worker and the adoption tooling hard-refuse the production project, so as written the feature cannot run in production at all.
  • The sync never writes posted_at or start_date, so every synced role shows no Posted/Age/Start in Desk, and Today's "uncovered" ordering degrades. The timezone question that justified this is now settled (§5.1, B4).
  • Production already holds 4,840 open roles. The sync links the ones PCR still lists and silently leaves the rest open forever, because the branch also blocks the order-report import that used to close them (§5.1, B5).
  • Desk has no surface for any of it: the Today strip, the Roles "Order report" filter and the role panel still speak only about the spreadsheet import (§6).

2. What the branch does

AreaWhat landed
apps/pcr-syncA Vercel cron worker (api/tick.ts, every 15 min, CRON_SECRET Bearer check with timingSafeEqual) and a CLI (src/main.ts --dry-run --force). client.ts is a GET-only PCR client (POST allowed only for /access-token; endpoint allowlist positionsV2, companiesV2; 30 s timeout; one retry on 429/5xx honouring Retry-After up to 30 s; two auth attempts 11 s apart). api-source.ts pages positionsV2 by JobId keyset, 500 per page, statuses Available/Hold/Pending. run.ts reconciles; store.ts calls one RPC.
Migration …_desk_pcr_bridgeTables pcr_bridge_settings/runs/requests/companies/orders/changes/errors (RLS on, service_role only). public.pcr_bridge_command(p_action, p) does claim/lease, quota reserve, request audit, company mapping, apply/deleted with revision-safe field ownership (P1001–P1007 codes), history and activities. role_rows_for and match_role_source hide end_date for managed roles. apply_role_import refuses to run while the bridge is enabled on the API transport. pcr_bridge_role_history (staff) and pcr_bridge_health (manager).
Migration 20261001000000_desk_environment_baselineA new first migration that creates the extensions, private, pcr and bullhorn schemas, five archive tables, and the six private.* helpers Desk functions depend on. Fresh databases (CI, preview) can now replay the whole chain. Production already has all of it, so production must adopt the version by metadata only (§8).
CI and lifecycle.github/workflows/ci.yml (format, lint, types, tests, builds; a second job replays migrations on a disposable local Supabase, runs the ten SQL suites and compares generated types byte-for-byte). scripts/database/* verify/plan/adopt the baseline with a derived contract; supabase/ci/seed.sql synthetic fixtures; [db.seed] enabled = false in config.toml.
Desksrc/lib/order-sync/* and src/lib/pcrecruiter/* (a planning model and normalizers that nothing imports, see S9), regenerated database.types.ts, and parse-report.ts now imports ./report-date.ts with an extension so the worker can load it under Node type stripping.
Docsdocs/product/live-orders-bridge.md describes the implemented behaviour.

3. Max's message, decoded

"Everything is working in Preview… reconciles 4690/4690 roles… CI/database lifecycle work… supabase review is passing."

True, with one caveat: the Supabase preview checks pass, but the GitHub "Workspace checks" job fails at the first step (§4). The preview run log confirms the rest (§4, table 2).

"For the Oct 1 verification, do we have a PCR snapshot/history from around Oct 1 with JobId + status?"

No, and none can be produced. PCR's API has no history endpoint; the pcr archive in Supabase is frozen at 2026-08-21; the only Oct 1 artefact is the Oct 1 Rollup Report (import #18, applied in production, plus its staged rows in private.order_report_2026_09_30), which carries the portal "Company Job ID", not the PCR JobId. What we do know from the Oct 6 check of the report against the live API: the report is a snapshot of PCR positions with Status = Available at about 8 AM ET; the 1,649 job ids that left the report between Oct 1 and Oct 6 really are closed in PCR (1,639 Closed, 1,424 of them in one bulk event on Oct 2 at 19:00). The closest possible "verification" is report job key → pcr_bridge_orders.applied->>'vms_job_id' after the first production run; it is a historical curiosity, not a merge blocker. Answer: drop it.

"And for the ~105 missing orders, do you or Jason have the list/report that number came from?"

The number is not in the repository, the memory notes, or the PR. It came from Jason verbally. Max is right not to guess. Ask Jason for the requisition list (MSP, requisition id, date). Once it exists, the check is one query per row: GET positionsV2?Query=EXISTS (CustomFields.FieldName eq 'Company Job ID' AND CustomFields.Value eq '<id>'), or locally select * from public.pcr_bridge_orders where applied->>'vms_job_id' = '<id>'. Not a merge blocker.

"Production already has the required schema, but we'll need to coordinate a migration-history adoption step before merging/deploying the new baseline."

Correct and important. §8 gives the exact steps. His tooling refuses to do it against production by design, so the adoption is a two-statement SQL run by you through the Supabase MCP.

4. What I verified

Table 1. Branch checks (run locally in a worktree at 943ec6d).

CheckResult
pnpm install --frozen-lockfileok, lockfile passes supply-chain policies
pnpm --filter pcr-sync --filter desk lintok
pnpm --filter pcr-sync --filter desk check-typesok (TypeScript 7.0.2; Desk next typegen ok)
pnpm --filter pcr-sync --filter desk testpcr-sync 64/64, desk 1,647/1,647
node --test scripts/database/*.test.mjs12/12
pnpm --filter desk buildok (Next production build, all routes)
pnpm format:check (GitHub CI, run 37562095834)fails: pnpm-lock.yaml is not Prettier-formatted
Same with pnpm-lock.yaml in .prettierignorepasses (2,017 files)
GitHub "Supabase migration and SQL checks" jobpass (3 m 41 s)
Supabase preview branch deploy (foolqzjouuwgleuvamdw)pass; migration history = main's 44 + baseline + bridge
Vercel checks on the PRblocked: "Git author Molret must have access to the project" (§5.2 S12)

Table 2. Preview database (foolqzjouuwgleuvamdw), read 2026-10-07 02:50 UTC.

FactValue
Settingsenabled, transport api, interval 1,440 min, monthly budget 40,000, next run 2026-10-08 00:11 UTC
Runs7: one lease_expired failure (first attempt), one dry run (4,568 created / 125 updated planned), one partial live run (2 quarantined), a 2-row fix-up, then 2,665 created + 2,025 unchanged, then two clean runs with 4,690 unchanged and 0 errors
Quarantine2 errors, both identity_already_linked (two PCR JobIds reusing a portal requisition), resolved by the next run
Orders linked4,690 (4,567 Available → open, 121 Hold + 2 Pending → on_hold); every JobId above 2^31, all within roles_id_check
Companies mapped738
PCR requests161 total: 4 auth, 40 positionsV2, 34 companiesV2 pages, 83 single-company reads
Roles data qualitydescription missing or trivial 56; vms_job_id null 63; bill_rate null 85; state null 81; discipline null 251; posted_at null 4,690; start_date null 4,690
MSP normalizationconfirmed 4,343, unconfirmed 273 (Wand, ShiftWise, StatStaff, FieldGlass, PeopleFluent, MedicalSolutions, "Adams 12 Five Star Schools - MSP"), empty 74
Security advisorsINFO only for the new tables (RLS with no policy = service_role only, as intended); two new SECURITY DEFINER RPCs callable by authenticated, consistent with the existing 33

Table 3. Production (pjupiksxikbttgqnipaa), read the same hour.

FactValue
Migration history44 versions, names equal to the files; no baseline, no bridge, no temporary desk_branch_api_schemas row
Baseline contract (scripts/database/lifecycle.mjs sql)[]: production satisfies every table, column, primary key, RLS/grant, helper-function hash and extension the baseline declares
roles92,231: open 4,840 (3,030 source='pcr', 1,810 source='report'), closed 87,391 (86,705 "archive", 686 "Not on the order report")
Role idssource='pcr' roles use the PCR JobId as id (3,030 of 3,030 open ones match pcr.positions.job_id); report roles use the sequence
Companies17,882, all source='pcr', ids equal to pcr.companies.company_id
Current PCR set (4,690 ids from the preview) vs productionBy PCR JobId: 1,555 are open in production already; 1,040 are closed (1,032 "archive", 8 "Not on the order report") and will be reopened by the sync; 2,095 have no role with that id (created in PCR after the Aug 21 archive freeze; the 1,810 report-sourced roles are their likely matches by requisition id). The other way round: 3,285 production open roles are not in the current PCR set (1,475 source='pcr', 1,810 source='report'); 962 of the pcr ones are already non-Available in the archive; 30 of the 3,285 have pipeline rows or assignments.
PCR timezonepcr.positions.date_posted for job 999065291722174 is 2025-11-14T10:03:22.08; the API returned the same wall-clock DatePosted and LastModified for the same job. PCR returns database-local time, and the archive loader already treats that as America/New_York (private.archive_ts).
Latest reportimport #76 "Rollup Report (10.6.2026).xlsx", 4,556 rows, previewed, not applied
Open roles with work on them70 have pipeline rows, 8 are assigned, 1,794 non-system activities

5. Findings

5.1 Blockers (fix on the branch before merge)

B1. CI is red on formatting. scripts/ci/format-check.mjs keeps pnpm-lock.yaml in its "existing debt" list but checks any debt file the PR touched, and the PR touched the lockfile. Prettier and pnpm disagree on YAML style, so formatting the lockfile would be undone by the next install. Fix: add a root .prettierignore containing pnpm-lock.yaml and remove it from baselineDebt. Verified: the check then passes. (Prettier reads .prettierignore from the project root.)

B2. Production refusal is hard-coded in two places. apps/pcr-sync/src/store.ts assertBranch throws for pjupiksxikbttgqnipaa; scripts/database/contract.mjs PRODUCTION makes lifecycle.mjs adopt refuse it. This was a sensible guard while building, but the deliverable is a production sync. Replace the hard-coded refusal with an explicit opt-in that keeps the safety: the worker requires PCR_SYNC_TARGET_REF to equal the ref parsed from SUPABASE_URL and additionally PCR_SYNC_ALLOW_PRODUCTION=true when that ref is production; lifecycle.mjs adopt takes --allow-production. Keep the tests, inverted. The production rollout in §8 still does not use adopt (no DB password on this machine; the metadata insert runs through MCP), so the worker change is the one that matters.

B3. First-run duration versus the cron deadline. runTick defaults to a 240 s deadline inside a 300 s function. The preview's first live run (4,690 creates in batches of 25, plus 117 company requests) took 2 m 35 s with an empty database and 5 min in dry-run mode; production will also link 3,000+ legacy rows and classify discipline/specialty per insert. A deadline failure marks the run failed and, by design since ac5dd63, pushes next_run_at a full interval out, so a slow first run costs a day. Fix two things: raise maxDuration to 600 and the default deadline to 540 s (Pro plan allows 800 s with Fluid compute), and run the first production sync from a laptop with pnpm --filter pcr-sync start -- --dry-run --force then --force (the CLI uses a 1 h deadline). Also make a deadline/failed run schedule a retry at now() + 15 min rather than a full interval; only succeeded/partial runs should honour the cadence.

B4. Dates are never written to roles. pcr_bridge_command stores DatePosted, BeginDate, EndDate, LastModified only in pcr_bridge_orders.raw_dates "until PCR's timezone is confirmed". It is confirmed (Table 3): the API returns the same naive wall-clock values as the SQL Server export that private.archive_ts already converts as America/New_York. Consequences today: roles.posted_at and start_date are null for every synced role, so the Roles grid's Posted/Age/Start columns are empty, today_uncovered_roles (ordered by priority desc, posted_at asc nulls last) sorts synced roles last, and report_role_aging falls back to created_at, which makes every role look brand new on the day of the first run. Fix in apply: posted_at = private.archive_ts((p->'raw_dates'->>'DatePosted')::timestamp), start_date = (p->'raw_dates'->>'BeginDate')::date. Leave end_date as it is (masked for managed open roles by role_rows_for, never a closure signal, per Julia), and keep the raw values in raw_dates. Add posted_at and start_date to the owned-field list, the "unchanged" comparison and the desk_pcr_bridge.sql test (posted_at must equal '2026-10-06T10:11:12.227'::timestamp at time zone 'America/New_York').

B5. Cutover: the legacy open set. Once pcr_bridge_settings.enabled is true on the API transport, apply_role_import raises P0409 ("PCRecruiter sync is authoritative"). That removes the only mechanism that closed roles the report no longer listed. The sync closes a linked role only when PCR says so, and it never looks at open roles it has not linked. So every production open role that PCR no longer lists stays open forever. Table 3 quantifies the set: 3,285 open roles are not in PCR's current set, and 2,095 current orders have no role by id, so without the linking fix below the first run creates about 2,000 new roles while the 1,810 report-sourced roles they duplicate stay open. Two fixes:

  1. Linking: apply links a legacy row by roles.id = JobId and source='pcr', else by vms_job_id and the same company_id. Report-sourced roles got their company_id by name match against the PCR company list, so a company-id mismatch produces a duplicate (new role created, old one left open). Add a third tier before creating: exactly one unlinked open role with the same vms_job_id and the same normalized company name (private.norm_name), else quarantine as P1001. This mirrors private.role_import_plan's "job+company" tier.
  2. A one-time, reviewed cutover after the first clean production run: close open roles with source in ('pcr','report') that are not in pcr_bridge_orders, with closed_reason = 'Not in PCRecruiter at sync cutover', moving active pipeline rows to job_closed via private.do_move, exactly as apply_role_import does. Roles with a pipeline row or an assignment (Table 3) get a manual look first. Add the new reason to the P1004 allowlist so PCR can reopen them later. This is a data step, not a migration (§8 step 9).

B6. Dead code in Desk. apps/desk/src/lib/order-sync/{model,plan}.ts and apps/desk/src/lib/pcrecruiter/{normalize,schemas,export}.ts (plus their tests, 459 test lines) are an earlier design (provider/scope identities, sourceUpdatedAt, allowSourceReopen) that neither Desk nor the worker imports; the worker has its own normalize.ts and run.ts. They would be maintained forever by whoever touches roles. Delete them, or move anything worth keeping into the worker. The single import the worker really needs from Desk is parse-report.ts (+ report-date.ts), and it reaches it with ../../desk/src/lib/role-imports/parse-report.ts. Prefer a tiny workspace package packages/order-report (parser + date helper, no React) that both apps import; it also removes the .ts-extension import that the branch added to Desk only for Node's type stripping (TypeScript requires allowImportingTsExtensions, Node requires the extension).

B7. .gitignore now ignores .env.*, which also ignores any future .env.example. Add !.env.example and commit apps/pcr-sync/.env.example listing the variables in environment.ts.

5.2 Should fix before enabling in production

S1. The budget model has the wrong unit. PCRecruiter's plans are per day: Free up to 5,000 calls/day, Premium ($100/month) up to 50,000 calls/day; logins count. The branch budgets 40,000 per month and defaults the interval to 24 h, which is why the docs say "40,000-call monthly operating budget under the 50,000-call plan". Keep the monthly cap as a cost guard but add a daily cap (daily_budget default 4,000 under Free, or 40,000 under Premium) in reserve, and raise the cadence: at 60 minutes a run costs about 13 calls (1 auth + 10 position pages + 1–2 company pages) plus two probes per closure, roughly 500 calls/day. The order report arrives once a day at ~8 AM ET; an hourly sync is strictly better and still under the Free tier. Also, PCR rejects more than three /access-token requests in a 10 s window with 400; the client's 11 s back-off is correct, keep it.

S2. Status map gaps. normalize.ts maps Available, Hold, Pending, Filled, OfferAccepted, Closed, Expired, NotAvailable. PCR's documented set is A/F/I/P/H/C (Internal Only exists); extra values are instance-specific. A linked open role whose PCR status becomes InternalOnly (or anything unmapped) fails normalization on every direct probe, which makes every run partial and never closes the role. For a linked role map unknown non-current statuses to closed; keep refusing to create from them. Confirm with Julia what Internal Only and Pending mean at Job.com.

S3. "Order report" semantics are now wrong. The Roles filter/view "Not on the latest report", the grid column, the role panel's Source line and role_detail.onLatestReport all compare roles.source_verified_at with private.latest_report_at() (the newest applied report). The bridge never sets source_verified_at, and no report will be applied again, so every synced role reads "never on a report" and the view fills with all of them. Fix in SQL without touching role_row: define private.latest_source_check_at() = greatest(latest_report_at(), max(pcr_bridge_runs.started_at where status in ('succeeded','partial'))) and, in role_rows_for and role_filter_sql, use greatest(r.source_verified_at, o.last_seen_at) from a left join on pcr_bridge_orders. Then "verified" means "confirmed by the latest source check", which is what recruiters want. Desk copy changes in §6.

S4. Recruiter edits versus source ownership. update_role lets any assigned recruiter edit title, city, state, zip, bill/pay rate, openings, description, start/end date; all but the dates are source-owned fields in the bridge. A later PCR change to the same field raises P1005 and the order is quarantined on every run until someone edits the field back; there is no UI for it. Decide one of: (a) Desk locks source-owned facts on managed roles (§6, recommended; the job-section editors already take a muted/read-only path), or (b) source wins and the bridge logs the overwrite as an activity. Keep P1005 as the safety net either way.

S5. pcr_bridge_health(p_scope) needs the scope hash (sha256 of the PCR DatabaseId), and pcr_bridge_settings is unreadable to authenticated, so Desk cannot discover it. Make p_scope optional (null → every scope) or add pcr_bridge_scopes() for managers.

S6. Quarantine growth. record_error is unique per (run_id, external_id, stage, error_code), so a persistent row error adds one row per run. Add a weekly pg_cron job that deletes resolved errors older than 90 days and pcr_bridge_requests older than 13 months.

S7. supabase/config.toml [db.seed] enabled = false also changes local development for apps/app, whose generated supabase/seed.sql was loaded by supabase db reset. Either keep enabled = true for local and disable it per remote with [remotes.<branch>.db.seed], or tell Alton that db reset no longer seeds. Not a correctness issue for Desk.

S8. Branching runs each migration in a single transaction. Nothing in the two new files needs concurrently, so this is fine, but note it for future index migrations: they will pass db push and fail in branching.

S9. On-hold roles are invisible by default. 123 Hold/Pending orders map to on_hold, and the Roles "Open" view defaults to statuses: ["open"] with the Status column hidden. Add on_hold to the default view or show a status chip count. Small, but recruiters will ask where those went.

S10. Unconfirmed MSP aliases. 273 orders carry MSP names not in the alias map (Table 2). The map is code (normalize.ts). Approve the list or move it to a pcr_bridge_msp_aliases table a manager can edit.

S11. Worker secrets and deployment. No Vercel project exists for apps/pcr-sync yet. Create job-com-pcr-sync with Root Directory apps/pcr-sync, Framework "Other", Node 24, Pro cron (*/15 needs Pro; Hobby allows once a day), environment variables from environment.ts, and CRON_SECRET (≥32 chars). job-com-matching is the template: it runs the same api/tick.ts web-handler pattern with framework: null in production today, so the pattern is proven. Vercel invokes the production deployment only, never retries a failed invocation, and may deliver duplicates; the lease in claim handles duplicates. Keep "Include source files outside of the Root Directory" on (default) until B6 moves the parser into a package.

S12. Vercel blocks Max's deployments. "Git author must have access to the project" is a documented security boundary with no off switch: add Max to the Goldkey Pro team (the PR comment carries the invite link) or accept that PR deploy previews stay blocked and only merges by a team member deploy.

S13. Vendor terms. The PCRecruiter Vendor API Access Terms (v1.0, 2026-07-22) require prior written approval for "database replication or data synchronization functions" and for AI use. The memory note from the planning session records this as blocker B2 with no resolution. The preview has already made 161 live calls. Confirm the approval status before the production cron is enabled; this is a business gate, not a code change.

5.3 Fine as built (noted so nobody re-reviews them)

  • Identity on JobId, with portal-id matching only for unlinked legacy rows and P1001 for ambiguity; regression-tested in desk_pcr_bridge_identity.sql.
  • Closure needs two consecutive direct 404s; file transport can never close.
  • Field ownership: the bridge records what it applied and refuses to overwrite a recruiter's edit (P1005); staff closures are protected (P1004); filled is protected (P1003).
  • Dry run returns outcomes without writing roles, and still records the run.
  • Diagnostics redact row values and constraint failures; tests prove it.
  • The lease (15 min, renewed per command) plus the per-account advisory lock make overlapping ticks safe; claim returns null when not due, so an idle tick makes zero PCR requests.
  • P0411 stops the run when the budget is exhausted; retries and auth count.
  • The baseline/adoption tooling derives its contract from the committed SQL, verifies read-only, and refuses when history or content drifted; CI proves the adoption transition on a disposable database.

6. Desk changes needed (apps/desk)

Nothing in the Desk UI knows about the sync. These are the concrete changes, all following AGENTS.md conventions (store in lib/<feature>/store.ts, copy in copy.ts, RPC calls through the typed client, route handlers gated).

  1. Copy (src/lib/roles/copy.ts): sources.pcr "PCR archive" → "PCRecruiter"; add panel.values.syncedAt(when) "Synced from PCRecruiter · {when}"; filters.verified "Order report" → "Source check", verifiedOn "Confirmed by the latest check", verifiedOff "Not confirmed by the latest check"; views.unverified "Not confirmed"; columns.verified "Last confirmed". src/lib/activities/model.ts: labels for pcr_created "Added from PCRecruiter", pcr_updated "Updated from PCRecruiter", pcr_closed "Closed in PCRecruiter". copy.test.ts lints them.
  2. Today (components/today/manager/report-strip.tsx, lib/today/store/manager.ts): when the bridge is enabled, replace the "Today's order report is not imported" banner with a sync line: "PCRecruiter sync · last run 8:14 AM · 4,690 current · 12 new since yesterday" from a new lib/pcr-sync/store.ts reading pcr_bridge_health. Keep the import banner only when the bridge is disabled or on file transport.
  3. Order Reports pages (components/role-imports/*): map the new P0409 message to copy ("PCRecruiter sync is on; reports are for checking only") instead of the generic "stale"; better, give the SQL guard its own SQLSTATE P0412 and add sync_authoritative to IMPORT_SQLSTATES. Hide the Upload action and the sidebar "Import report" entry while the bridge is on; keep the history pages.
  4. Role panel (components/roles/panel/source-section.tsx): for managed roles show "Synced from PCRecruiter · {RelativeTime last_seen_at}", the PCR PositionId, and the raw MSP value when it was normalized; load through GET /api/roles/[id]/pcr → pcr_bridge_role_history. job-section.tsx: render source-owned facts read-only on managed roles (S4a) with a lock hint; discipline, specialty, job type and priority stay editable.
  5. Settings → System → "PCRecruiter sync" (manager): health card (last run, next run, cadence, dry-run flag, enabled), request count today versus the daily budget, the last 20 runs with counts, and the quarantine list (each row linking to the role or showing the PCR id). Pattern: components/settings/match-runs-section.tsx. Needs S5. "Sync now" is out of scope until an authenticated trigger exists (the cron route is secret-gated; a pcr_bridge_command('configure', {next_run_at: now()}) wrapper for managers would do).
  6. Grid: no role_row change; with S3 the existing "verified" column and view keep working under new names. Consider showing on_hold in the Open view (S9).
  7. Mock (mock/server.mjs): stub pcr_bridge_health and pcr_bridge_role_history so pnpm test:e2e:mock covers the new surfaces (run from a separate worktree; it refuses a checkout with a running dev server).

Estimated size: 2–3 days, one person, after the branch fixes land.

7. Implementation plan

Ordered. Each step has an owner and a check. Steps 1–8 are branch commits; 9–16 are production work that happens around the merge.

#StepOwnerDone when
1.prettierignore with pnpm-lock.yaml; drop it from baselineDebt (B1)Max"Workspace checks" green on the PR
2Production opt-in in store.ts and lifecycle.mjs (B2); tests invertedMaxpnpm --filter pcr-sync test and pnpm test:database-lifecycle green
3maxDuration 600, deadline 540 s, failed/deadline runs retry in 15 min (B3)Maxtick.test.ts covers the retry schedule; desk_pcr_bridge.sql asserts next_run_at on failed
4Write posted_at and start_date from raw_dates with private.archive_ts (B4)MaxTest added; preview re-run shows 0 null posted_at
5Third linking tier by vms_job_id + normalized company name; new closed reason in the P1004 allowlist (B5.1)Maxdesk_pcr_bridge_identity.sql case for a report role with a different company id
6Delete lib/order-sync and lib/pcrecruiter from Desk; move the parser to packages/order-report and import it from both apps (B6); .gitignore and .env.example (B7)MaxDesk and worker typecheck; Vercel preview of the worker builds
7Daily budget + hourly default cadence + docs correction (S1); unknown statuses close linked roles (S2); pcr_bridge_health without scope (S5); cleanup cron (S6)MaxTests; docs updated
8latest_source_check_at and the greatest(source_verified_at, last_seen_at) rule in role_rows_for and role_filter_sql (S3)Maxdesk_rls.sql verified-filter assertions still pass; a new assertion for a synced role
9Production adoption (§8 steps 1–3): verify contract read-only, insert the baseline version row, apply the bridge migration through MCP with the file's exact nameStansupabase_migrations.schema_migrations on production lists 46 versions equal to the files
10Merge the PR (squash). The Supabase GitHub integration then finds no new migration for production; the preview branch is deleted automaticallyStanIntegration comment on the merge shows nothing pending; main CI green
11Vercel project job-com-pcr-sync (S11), env vars, first production deploy; confirm GET /api/tick without the secret answers 401StanDeployment Ready; cron listed in the project's Cron Jobs tab
12First production dry run from a laptop: pnpm --filter pcr-sync start -- --dry-run --force against production; review counts (created vs linked by id vs linked by requisition) and the quarantineStanCreated count is small (new since Oct 6); no systemic failures
13First live run (--force), then a second run that must report all unchangedStanSecond run: 0 created, 0 errors
14Cutover closure (B5.2) as a reviewed SQL run through MCP, after a manual look at the open-not-current roles that have pipeline rows or assignmentsStanOpen roles = linked live orders + Desk-created roles
15Desk changes (§6) on a follow-up branchStane2e mock green; copy lint green
16Enable the cron cadence (configure interval 60), watch three days of pcr_bridge_runs, then retire the daily report uploadStanThree consecutive clean runs; Julia confirms the Today strip

Gate before step 11: S13 (vendor approval) confirmed in writing.

8. Production adoption of the baseline, step by step

Why this is needed: the branch adds 20261001000000_desk_environment_baseline with a timestamp earlier than every applied migration. Production already has everything it creates (schemas, the five archive tables, the six helpers, the extensions) from the manual archive load. The file uses plain create table, so if it ever executes on production it fails on pcr.candidates already existing. The Supabase GitHub integration, with "Deploy to production" enabled, applies "new migrations" on merge to the production branch; Supabase tracks applied versions in supabase_migrations.schema_migrations. The safe sequence is therefore: record the baseline as applied before the merge, and apply the bridge migration ourselves so nothing is left for the integration to run.

  1. Verify the contract, read-only (already done once for this review, Table 3). Regenerate with node scripts/database/lifecycle.mjs sql from the branch and run the SELECT through the Supabase MCP execute_sql against production. It must return [].

  2. Record the baseline (the same statement metadataSql would run; the SHA256 is of the committed file):

    1begin; 2lock table supabase_migrations.schema_migrations in exclusive mode; 3insert into supabase_migrations.schema_migrations(version, name, statements) 4select '20261001000000', 'desk_environment_baseline', 5 array['-- Baseline adopted after complete read-only contract verification; historical SQL was not replayed. SQL SHA256: 47b40aef14598529e7515d4b114b703e6ac693789d9907b834a6db8c085d00a4'] 6where not exists (select 1 from supabase_migrations.schema_migrations where version = '20261001000000'); 7commit;

    That hash is the file at 943ec6d; recompute it with shasum -a 256 if the file changes before the merge. The CLI equivalent, for a linked checkout, is supabase migration repair 20261001000000 --status applied.

  3. Apply the bridge migration through MCP apply_migration with name desk_pcr_bridge and the file's content at the merge commit, so the recorded version is 20261006202911 and matches the file. (If step 8 of §7 changes the file, apply the final content.) Confirm: select version, name from supabase_migrations.schema_migrations order by version desc limit 3.

  4. Then merge. If "Deploy to production" is on, the integration compares history with the files and finds nothing new. If it is off, nothing happens either way. Either is fine; check the integration's comment on the merge commit.

  5. Never run pnpm db:adopt or supabase db push against production from a developer machine. The tooling refuses by design; the plan above keeps it that way.

Rollback: the bridge migration is additive (new tables, new functions, and create or replace of role_rows_for, match_role_source, apply_role_import). To back out, drop the pcr_bridge_* tables and functions and re-run the three previous definitions from 20261006052713_desk_review_hardening.sql and 20261001232507_…source_guard.sql; delete the two history rows. Roles created by the sync carry source='pcr' and an activity row, so they are identifiable.

9. Reply to Max (ready to paste)

Thanks Max, this is solid work and the preview numbers check out on my side. Three answers and a short list before we merge.

Oct 1: there is no PCR snapshot from Oct 1 and none can be made (no history endpoint; the archive froze Aug 21). We did verify on Oct 6 that the report is PCR "Available" at ~8 AM ET and that the 1,649 ids that left the report between Oct 1 and Oct 6 are really closed in PCR. Drop the Oct 1 reconciliation; it is not a merge condition.

~105 orders: the list came from Jason verbally; I will get it with MSP, requisition id and date. Not a merge condition either.

Before merge: (1) CI is red on pnpm-lock.yaml formatting; add it to a .prettierignore. (2) Make production an explicit opt-in instead of a hard refusal in store.ts and lifecycle.mjs. (3) Write posted_at and start_date from the raw dates with private.archive_ts; the timezone is confirmed (API values equal the SQL Server export). (4) Raise the function limit to 600 s and the deadline to 540 s, and retry a failed run in 15 min instead of a full interval. (5) Add a linking tier by requisition id plus normalized company name, and allow reopening roles closed with the cutover reason. (6) Remove the unused order-sync and pcrecruiter libs from Desk and move the report parser into a small package. (7) .gitignore should not swallow .env.example. Also note PCR's plans are per day (50k/day on Premium), so we can sync hourly; make the daily budget the real cap.

I will handle the production adoption (version row + bridge migration via MCP) right before merging, then run the first sync from my machine, do the cutover of stale open roles, and wire the Desk UI on a follow-up branch.

10. Citations

Official documentation used for the findings above (fetched 2026-10-06).

Supabase

Vercel

PCRecruiter

TypeScript and Node

Repository and project references

  • Original plan: docs/product/pcr-sync-implementation-plan.md (§0 blockers, §8.6 linking, §9.1 coexistence with the report import, §11 Desk UI).
  • Branch docs: docs/product/live-orders-bridge.md.
  • Desk conventions: apps/desk/AGENTS.md.
PCRecruiter role sync (PR #3): review and merge plan | MDX Limo