[object Object]

← back to Rentv 2026

pr-intelligence: state-aware quality gate (stateGate(state)) — Arizona now a first-class gate/dashboard number; /api/pr/gate/check?state=AZ; californiaGate() back-compat wrapper; test (30/30). DTD-C punch-list item 2.

0bad92c373b44f1705791aa7dc979089cfe453cc · 2026-07-30 23:01:26 -0700 · Steve Abrams

Files touched

Diff

commit 0bad92c373b44f1705791aa7dc979089cfe453cc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 30 23:01:26 2026 -0700

    pr-intelligence: state-aware quality gate (stateGate(state)) — Arizona now a first-class gate/dashboard number; /api/pr/gate/check?state=AZ; californiaGate() back-compat wrapper; test (30/30). DTD-C punch-list item 2.
---
 src/pr/index.js             |  5 +++--
 src/pr/services/settings.js | 32 +++++++++++++++++++-------------
 test/pr/integration.test.js | 16 ++++++++++++++++
 3 files changed, 38 insertions(+), 15 deletions(-)

diff --git a/src/pr/index.js b/src/pr/index.js
index e59f1b34..d66547bf 100644
--- a/src/pr/index.js
+++ b/src/pr/index.js
@@ -139,7 +139,8 @@ module.exports = function mountPR(app, { adminOnly, sendPage }) {
         cells_covered: caCells.covered, cells_total: totalCells,
         gate,
       },
-      arizona: { unlocked: azUnlocked, organizations: orgTotals.az, status: azUnlocked ? 'active' : 'locked' },
+      arizona: { unlocked: azUnlocked, organizations: orgTotals.az, status: azUnlocked ? 'active' : 'locked',
+        gate: azUnlocked ? await settings.get('az_gate', null) : null },
       organizations: orgTotals, people: pplTotals,
       by_category: byCategory, by_role: byRole, by_metro: byMetro,
       orgs_without_pr_contact: noPr.n,
@@ -398,7 +399,7 @@ module.exports = function mountPR(app, { adminOnly, sendPage }) {
   app.get('/api/pr/settings', adminOnly, h(async (_q, res) => res.json(await settings.getAll())));
   app.put('/api/pr/settings/:key', adminOnly, h(async (req, res) =>
     res.json(await settings.set(req.params.key, (req.body || {}).value, actorOf(req)))));
-  app.post('/api/pr/gate/check', adminOnly, h(async (_q, res) => res.json(await settings.californiaGate())));
+  app.post('/api/pr/gate/check', adminOnly, h(async (req, res) => res.json(await settings.stateGate((req.query.state || (req.body || {}).state || 'CA')))));
   app.post('/api/pr/gate/unlock-arizona', adminOnly, h(async (req, res) =>
     res.json(await settings.unlockArizona({ actor: actorOf(req), override: (req.body || {}).override === true }))));
 
diff --git a/src/pr/services/settings.js b/src/pr/services/settings.js
index d4b82882..a4c7a2d6 100644
--- a/src/pr/services/settings.js
+++ b/src/pr/services/settings.js
@@ -32,13 +32,14 @@ async function set(key, value, actor) {
  *  6. no inferred/generated contact mislabeled verified (structural query)
  *  7. dashboard shows remaining coverage gaps (always true once the dashboard is live — reported)
  */
-async function californiaGate() {
+async function stateGate(state = 'CA') {
+  const ST = String(state).toUpperCase();
   const checks = {};
 
   const runs = await db.one(
     `SELECT count(*) FILTER (WHERE status='completed') AS completed,
             count(*) FILTER (WHERE checkpoint <> '{}'::jsonb) AS checkpointed
-       FROM pr_research_runs WHERE state='CA'`, []);
+       FROM pr_research_runs WHERE state=$1`, [ST]);
   checks.pipeline_resumable = Number(runs.completed) > 0 && Number(runs.checkpointed) > 0;
 
   const dups = await db.one(
@@ -54,14 +55,14 @@ async function californiaGate() {
     `WITH reviewed AS (
        SELECT o.*, EXISTS (SELECT 1 FROM pr_field_evidence e WHERE e.entity_type='organization' AND e.entity_id=o.id) AS has_evidence
          FROM pr_organizations o
-        WHERE 'CA' = ANY(o.state_presence)
+        WHERE $1 = ANY(o.state_presence)
           AND o.lifecycle_status IN ('verified','ready_for_outreach','contacted','active_relationship'))
      SELECT count(*) AS n,
             count(*) FILTER (WHERE display_name IS NOT NULL AND website_url IS NOT NULL
                              AND organization_type IS NOT NULL
                              AND (cardinality(metros)>0 OR cardinality(counties)>0)
                              AND has_evidence) AS complete
-       FROM reviewed`, []);
+       FROM reviewed`, [ST]);
   const nReviewed = Number(orgQ.n), nComplete = Number(orgQ.complete);
   checks.org_completeness = { reviewed: nReviewed, complete: nComplete, pct: nReviewed ? Math.round(nComplete / nReviewed * 100) : 0, pass: nReviewed > 0 && nComplete / nReviewed >= 0.8 };
 
@@ -70,26 +71,26 @@ async function californiaGate() {
   // in the catalog, just not counted as gate-relevant outreach targets.
   const exclude = await get('coverage_exclude_types', ['bank', 'credit_union']);
   const hi = await db.one(
-    `WITH hi AS (SELECT id FROM pr_organizations WHERE 'CA'=ANY(state_presence) AND priority_score >= 60
-                 AND lifecycle_status NOT IN ('duplicate','archived') AND organization_type <> ALL($1::text[]))
+    `WITH hi AS (SELECT id FROM pr_organizations WHERE $1=ANY(state_presence) AND priority_score >= 60
+                 AND lifecycle_status NOT IN ('duplicate','archived') AND organization_type <> ALL($2::text[]))
      SELECT count(*) AS n,
             count(*) FILTER (WHERE EXISTS (
               SELECT 1 FROM pr_people p WHERE p.organization_id = hi.id
                 AND p.lifecycle_status NOT IN ('duplicate','suppressed','wrong_person','left_company')
                 AND p.department IN ('communications','marketing','leadership','bd','agency'))) AS with_contact
-       FROM hi`, [exclude]);
+       FROM hi`, [ST, exclude]);
   const nHi = Number(hi.n), nWith = Number(hi.with_contact);
   // Strict lens (informational, Cody cycle 2): comms/marketing/agency/bd ONLY — the
   // spec's gate criterion explicitly includes leadership, so pass/fail stays on the
   // broader set, but this number keeps "we can email a real comms person" honest.
   const commsOnly = await db.one(
-    `WITH hi AS (SELECT id FROM pr_organizations WHERE 'CA'=ANY(state_presence) AND priority_score >= 60
-                 AND lifecycle_status NOT IN ('duplicate','archived') AND organization_type <> ALL($1::text[]))
+    `WITH hi AS (SELECT id FROM pr_organizations WHERE $1=ANY(state_presence) AND priority_score >= 60
+                 AND lifecycle_status NOT IN ('duplicate','archived') AND organization_type <> ALL($2::text[]))
      SELECT count(*) FILTER (WHERE EXISTS (
               SELECT 1 FROM pr_people p WHERE p.organization_id = hi.id
                 AND p.lifecycle_status NOT IN ('duplicate','suppressed','wrong_person','left_company')
                 AND p.department IN ('communications','marketing','agency','bd'))) AS n
-       FROM hi`, [exclude]);
+       FROM hi`, [ST, exclude]);
   checks.contact_coverage = {
     high_priority: nHi, with_contact: nWith, pct: nHi ? Math.round(nWith / nHi * 100) : 0,
     pass: nHi > 0 && nWith / nHi >= 0.7,
@@ -115,11 +116,16 @@ async function californiaGate() {
   const passed = checks.pipeline_resumable && checks.dedupe_working &&
     checks.org_completeness.pass && checks.contact_coverage.pass &&
     checks.workflows_exercised && checks.no_mislabeled_inferred;
-  const result = { passed, checked_at: new Date().toISOString(), checks };
-  await set('california_gate', result, 'system:gate');
+  const result = { state: ST, passed, checked_at: new Date().toISOString(), checks };
+  // Store per-state; keep california_gate for back-compat (dashboard reads it).
+  await set(ST.toLowerCase() + '_gate', result, 'system:gate');
+  if (ST === 'CA') await set('california_gate', result, 'system:gate');
   return result;
 }
 
+/** Back-compat wrapper — the CA gate is the phase-transition gate that unlocks Arizona. */
+async function californiaGate() { return stateGate('CA'); }
+
 /** Arizona is unlocked ONLY by a passing CA gate or an explicit, audit-logged admin override. */
 async function unlockArizona({ actor, override = false }) {
   const gate = await californiaGate();
@@ -133,4 +139,4 @@ async function unlockArizona({ actor, override = false }) {
 
 async function arizonaUnlocked() { return (await get('arizona_unlocked', false)) === true; }
 
-module.exports = { get, getAll, set, californiaGate, unlockArizona, arizonaUnlocked };
+module.exports = { get, getAll, set, stateGate, californiaGate, unlockArizona, arizonaUnlocked };
diff --git a/test/pr/integration.test.js b/test/pr/integration.test.js
index 2dd03cd8..ecc4c210 100644
--- a/test/pr/integration.test.js
+++ b/test/pr/integration.test.js
@@ -231,3 +231,19 @@ test('audit log recorded every step of the workflow', async () => {
 test.after(async () => {
   // leave the test DB in place for inspection; it is throwaway by name.
 });
+
+test('state-aware quality gate: computes CA and AZ independently, stores per-state', async () => {
+  const caGate = await settings.stateGate('CA');
+  assert.equal(caGate.state, 'CA');
+  assert.ok(typeof caGate.passed === 'boolean');
+  assert.ok(caGate.checks.contact_coverage && typeof caGate.checks.contact_coverage.pct === 'number');
+  const azGate = await settings.stateGate('AZ');
+  assert.equal(azGate.state, 'AZ');
+  // stored per-state and back-compat CA key still written
+  assert.ok(await settings.get('az_gate'));
+  assert.ok(await settings.get('california_gate'));
+  // the two are independent computations (different state filter) — coverage objects distinct
+  assert.notEqual(caGate.checks.contact_coverage, azGate.checks.contact_coverage);
+  // californiaGate() wrapper still works
+  assert.equal((await settings.californiaGate()).state, 'CA');
+});

← cf8d9dc2 yoloforever: cycle 7 ledger — AZ coverage 81%, name gate har  ·  back to Rentv 2026  ·  yoloforever ledger: autonomous tick — state-aware gate shipp 2413bbdd →