[object Object]

← back to Commercialrealestate

crcp: doctrine link-scrub — never link CREXi/aggregators; link firm-direct listing only, honest firm-site + Google finder fallback (TK-10081 Phase 2 display)

ff6c52b1c2c05eaf32d20eebbc24f2b26ca7726e · 2026-07-31 14:48:45 -0700 · Steve Abrams

Files touched

Diff

commit ff6c52b1c2c05eaf32d20eebbc24f2b26ca7726e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 31 14:48:45 2026 -0700

    crcp: doctrine link-scrub — never link CREXi/aggregators; link firm-direct listing only, honest firm-site + Google finder fallback (TK-10081 Phase 2 display)
---
 public/index.html | 30 +++++++++++++++++++++++++++---
 public/mls.html   | 29 ++++++++++++++++++++++++-----
 2 files changed, 51 insertions(+), 8 deletions(-)

diff --git a/public/index.html b/public/index.html
index 4de2864..7f0fb48 100644
--- a/public/index.html
+++ b/public/index.html
@@ -966,8 +966,9 @@ function loopnetLink(p){
   const ptype=LN_TYPE[p.type]||'';
   // exact: site-search resolves the specific LoopNet listing for this address if it exists.
   const exact=`https://www.google.com/search?q=${encodeURIComponent(`site:loopnet.com "${[p.address,p.city].filter(Boolean).join(' ')}"`)}`;
-  const browse=ptype?`https://www.loopnet.com/search/${ptype}/${city}/for-sale/`:`https://www.loopnet.com/search/commercial-real-estate/${city}/for-sale/`;
-  return `<a href="${exact}" target="_blank" rel="noopener noreferrer" title="Find this exact property on LoopNet (CoStar) — free search, opens in a new tab">LoopNet ↗</a> <a href="${browse}" target="_blank" rel="noopener noreferrer" class="small" title="Browse comparable ${p.type||'commercial'} for-sale on LoopNet in ${p.city||'LA'}" style="opacity:.7">(comps)</a>`;
+  const compsQ=`site:loopnet.com ${ptype||'commercial'} for sale ${p.city||'Los Angeles'} CA`;
+  const comps=`https://www.google.com/search?q=${encodeURIComponent(compsQ)}`;
+  return `<a href="${exact}" target="_blank" rel="noopener noreferrer" title="Find this exact property on LoopNet (CoStar) — free search, opens in a new tab">LoopNet ↗</a> <a href="${comps}" target="_blank" rel="noopener noreferrer" class="small" title="Browse comparable ${p.type||'commercial'} for-sale on LoopNet in ${p.city||'LA'}" style="opacity:.7">(comps)</a>`;
 }
 // Rich filter state
 const F = { types:new Set(), firms:new Set(), cityQuery:'', priceMin:null, priceMax:null,
@@ -1057,6 +1058,29 @@ function agentInfo(p){
 }
 function gsearch(q){ return 'https://www.google.com/search?q='+encodeURIComponent(q); } // retained; no longer linked out
 function safeUrl(u){ u=String(u==null?'':u); return /^https?:\/\//i.test(u)?u:'#'; }
+// Aggregator doctrine: never surface a clickable href to crexi/redfin/zillow/costar/loopnet/realtor/myelisting.
+// Mirror of mls.html brokerListing() — resolve canonical click-through from listing data.
+const _AGG=['crexi','redfin','zillow','costar','loopnet','realtor','myelisting'];
+const _host=u=>{try{return new URL(u).hostname.replace(/^www\./,'');}catch{return null;}};
+const _isAgg=u=>{const h=_host(u);return !!(h&&_AGG.some(a=>h.includes(a)));};
+// Refined listing-link doctrine (2026-07-31):
+// 1. Non-agg source → firm-direct listing link.
+// 2. Agg/absent source + non-agg broker_url → honest "firm site" label (NOT "the listing").
+// 3. Always append a Google ground-truth finder (never crexi/aggregator).
+// Returns array of HTML strings to push() into the find[] block.
+function listingLinkBits(p, firm){
+  const bits=[];
+  const listingQ='"'+(p.address||'')+'" '+(p.city||'')+' '+(firm&&firm!=='Unknown'?firm:'')+' commercial listing';
+  if(p.source&&!_isAgg(p.source)){
+    bits.push(`<a class="findlink" href="${safeUrl(p.source)}" target="_blank" rel="noopener noreferrer">↗ View firm listing</a>`);
+  } else {
+    if(p.broker_url&&!_isAgg(p.broker_url)){
+      bits.push(`<a class="findlink" href="${safeUrl(p.broker_url)}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm&&firm!=='Unknown'?firm:'Broker')} site</a>`);
+    }
+    bits.push(`<a class="findlink" href="https://www.google.com/search?q=${encodeURIComponent(listingQ)}" target="_blank" rel="noopener noreferrer">🔎 Find this listing</a>`);
+  }
+  return bits;
+}
 // Firm cell — clicking filters the grid to that firm IN-APP (no external link).
 function firmCell(p){ const n=firmLabel(p); if(!n||n==='Unknown') return esc(n||'—');
   return `<span class="flink" data-firmfilter="${esc(n).replace(/"/g,'&quot;')}" title="Show only ${esc(n)} listings">${esc(n)}</span>`; }
@@ -1718,7 +1742,7 @@ function contactModalBody(p){
     find.push(`<a class="findlink" href="https://www.linkedin.com/search/results/people/?keywords=${encodeURIComponent(a.name+(firm&&firm!=='Unknown'?' '+firm:''))}" target="_blank" rel="noopener noreferrer">🧑‍💼 ${esc(a.name)} — LinkedIn</a>`);
     find.push(`<a class="findlink" href="${gsearch('"'+a.name+'" DRE license California real estate')}" target="_blank" rel="noopener noreferrer">🪪 ${esc(a.name)} — DRE license</a>`);
   }
-  if(p.source) find.push(`<a class="findlink" href="${safeUrl(p.source)}" target="_blank" rel="noopener noreferrer">↗ View source listing</a>`);
+  listingLinkBits(p,firm).forEach(b=>find.push(b));
   find.push(`<span class="findlink findloop">${loopnetLink(p)}</span>`);
   const findHtml=find.length?`<div class="findgrid">${find.join('')}</div>`:'<div class="tabmut">Add an agent/firm in the Agent tab to enable lookups.</div>';
   return `<div class="composeCard tabwin">
diff --git a/public/mls.html b/public/mls.html
index 15ab423..47fa3bf 100644
--- a/public/mls.html
+++ b/public/mls.html
@@ -166,7 +166,7 @@ const COLS=[
   {k:'source_of_truth',l:'Source',t:'s'},
   {k:'financeScore',l:'Finance',t:'n'},
   {k:'composite',l:'Score',t:'n'},
-  {k:'source',l:'Source',t:'link'},
+  {k:'source',l:'Listing',t:'link'},
 ];
 let DATA=[], view='table', sortKey='dom', sortDir=1, q='', lastColSig='';  // default landing: freshest listings first (days-on-market asc); localStorage overrides for returning users
 const _rd=(function(){ const v=parseInt(localStorage.getItem('mlsRecentDays'),10); return (v>0&&v<=365)?v:7; })();
@@ -220,7 +220,7 @@ function rowSummary(r){ const L=[];
   const l2=[r.type, r.units?`${r.units} unit${r.units>1?'s':''}`:null, r.price?money(r.price):null].filter(Boolean).join(' · '); if(l2)L.push(l2);
   const m=[]; if(r.cap_rate!=null&&r.cap_rate!=='') m.push('Cap '+r.cap_rate+'%'); if(r.units&&r.price) m.push('$'+Math.round(r.price/r.units).toLocaleString()+'/unit'); if(r.sqft&&r.price) m.push('$'+Math.round(r.price/r.sqft).toLocaleString()+'/SF'); if(m.length)L.push(m.join(' · '));
   const ex=[]; if(r.year_built) ex.push('Built '+r.year_built); if(r.beds!=null) ex.push((r.beds)+'bd/'+(r.baths??'—')+'ba'); if(r.warrantable_status) ex.push('Warrantability: '+r.warrantable_status); if(ex.length)L.push(ex.join(' · '));
-  if(r.source) L.push(r.source); return L.join('\n');
+  const _lu=brokerListing(r); if(_lu.mode==='listing') L.push(_lu.href); else { const _bt=[_lu.firm,_lu.agent].filter(Boolean).join(' — '); if(_bt) L.push('Broker: '+_bt); } return L.join('\n');
 }
 function copySummary(id,btn){ const r=DATA.find(x=>x.id===id); if(!r) return; const done=()=>{ if(btn){ btn.textContent='✓'; setTimeout(()=>btn.textContent='📋',1200); } }; try{ navigator.clipboard.writeText(rowSummary(r)).then(done).catch(done); }catch(e){ done(); } }
 function median(a){ if(!a.length) return null; const s=a.slice().sort((x,y)=>x-y); const m=Math.floor(s.length/2); return s.length%2?s[m]:(s[m-1]+s[m])/2; }
@@ -323,15 +323,34 @@ function passFacets(r){
   return true;
 }
 const cval=(r,c)=>c.calc?c.calc(r):r[c.k];
+// Doctrine (docs/SOURCING.md): the aggregator is a breadcrumb to the broker, NEVER a link we show.
+// Resolve the canonical CLICK-THROUGH: a firm-direct source wins; else the broker's own site; else no link.
+const _AGG=['crexi','redfin','zillow','costar','loopnet','realtor','myelisting'];
+const _host=u=>{try{return new URL(u).hostname.replace(/^www\./,'');}catch{return null;}};
+const _isAgg=u=>{const h=_host(u);return !!(h&&_AGG.some(a=>h.includes(a)));};
+function brokerListing(r){
+  // 1. Firm-direct property listing URL → honest "view this listing"
+  if(r.source && !_isAgg(r.source)) return {mode:'listing',href:r.source,label:'View firm listing ↗',title:"the broker's own property listing"};
+  // 2. Aggregator / unknown source → text block with optional firm site + always a find link
+  const firm=r.broker_firm||r.firm||null;
+  const agent=r.broker_agent||null;
+  const firmSite=(r.broker_url && !_isAgg(r.broker_url))?r.broker_url:null;
+  const find='https://www.google.com/search?q='+encodeURIComponent('"'+(r.address||'')+'" '+(r.city||'')+' '+(firm||'')+' commercial listing');
+  return {mode:'text',firm,agent,firmSite,find};
+}
 function cellHTML(r,c){
   const v=cval(r,c);
   if(c.t==='money') return money(v);
   if(c.t==='pct') return v==null||v===''?'—':v+'%';
   if(c.t==='n') return num(v);
   if(c.t==='status'){ const a=String(v||'').startsWith('Active'); return `<span class="pill ${a?'active':'other'}">${esc(v||'—')}</span>`; }
-  if(c.t==='link') return v?`<a href="${esc(v)}" target="_blank" rel="noopener noreferrer">link↗</a>`:'—';
-  // Doctrine deep-link: the brokerage name clicks through to the broker's OWN site (canonical source).
-  if(c.k==='firm' && r.broker_url) return `<a href="${esc(r.broker_url)}" target="_blank" rel="noopener noreferrer" title="the listing broker's own site — source of truth">${esc(v==null||v===''?'—':v)} ↗</a>`;
+  if(c.t==='link'){ const u=brokerListing(r);
+    if(u.mode==='listing') return `<a href="${esc(u.href)}" target="_blank" rel="noopener noreferrer" title="${esc(u.title)}">${esc(u.label)}</a>`;
+    // mode==='text': firm site (if non-agg) + always a find link
+    const fs=u.firmSite?`<a href="${esc(u.firmSite)}" target="_blank" rel="noopener noreferrer" title="the listing broker's own site">🏢 site ↗</a> `:'';
+    return fs+`<a href="${esc(u.find)}" target="_blank" rel="noopener noreferrer" title="find this listing on the web">🔎 find</a>`; }
+  // Firm name links to the broker's own site (only when non-aggregator).
+  if(c.k==='firm' && r.broker_url && !_isAgg(r.broker_url)) return `<a href="${esc(r.broker_url)}" target="_blank" rel="noopener noreferrer" title="the listing broker's own site">${esc(v==null||v===''?'—':v)} ↗</a>`;
   return esc(v==null||v===''?'—':v);
 }
 function filtered(){

← 3275554 condos/stats + crcp/stats: re-aggregate warrantability with  ·  back to Commercialrealestate  ·  crcp: WIP $0 firm-direct harvesters (Lee/Buildout + Lyon Sta 0d5f471 →