Gap fixes: pattern fallback, narrative citations, call_log plumbing

Closing trust-breaks surfaced in the strategic audit.

A — MEMORY chip renders even when sparse:
Previously rendered nothing when no trait crossed threshold, which
recruiters would read as "system has no signal." Now explicitly
says "memory is sparse for this role+geo — no trait crossed
threshold" or "no similar past playbooks yet — first fill of this
kind will seed it." Honest when it doesn't know.

B — Removed /intelligence/learn dead endpoint:
Legacy CSV-writer path that destructively re-wrote
successful_playbooks. /log and /log_failure replace it cleanly.
Leaving dead code confuses future maintainers.

C — Narrative tooltips on Endorsed chips:
Hovering the green "Endorsed · N playbooks" chip now fetches
the worker's past operations from successful_playbooks_live and
shows a story: "Maria — past endorsements: • Welder x2 in
Toledo (2026-04-15), • Welder x1 in Toledo (2026-04-18)..."
Falls back to honest "narrative unavailable" if the seed
didn't land in SQL.

D — call_log infrastructure in worker modal:
New "Recent Contact" section queries call_log JOIN candidates by
name. Surfaces last 3 call entries with timestamp, recruiter,
disposition, duration. When empty (which is today's reality —
candidates table only has 1000 rows vs call_log's higher IDs),
shows an honest message about the data gap and what real ATS
integration would unlock.

Honest call: D ships infrastructure. Actual utility depends on
aligning candidate IDs between the candidates table and
call_log — current synthetic data doesn't cross-ref cleanly.
When real ATS data lands, this section becomes the
"system knows who we called yesterday" feature the recruiter
needs.

Deferred (would require a dedicated session):
- Rate awareness (needs worker pay_rate + contract bill_rate)
- Push / background daemon (Slack/SMS/email integration)
- Confidence calibration (needs a probabilistic ranking layer)
This commit is contained in:
root 2026-04-20 17:20:22 -05:00
parent 1f56630d5d
commit 2595d48535
2 changed files with 82 additions and 17 deletions

View File

@ -1391,15 +1391,11 @@ tr:hover{background:#111827}
}
}
// Intelligence: Log a search → selection as a learned pattern
if (url.pathname === "/intelligence/learn" && req.method === "POST") {
const b = await json();
const csv = `timestamp,operation,approach,result,context\n"${new Date().toISOString()}","search: ${(b.query||"").replace(/"/g,'""')}","${(b.filters||"").replace(/"/g,'""')}","selected: ${(b.worker_name||"").replace(/"/g,'""')} (${b.worker_id||""})","role=${b.worker_role||""} state=${b.worker_state||""} city=${b.worker_city||""}"`;
const form = new FormData();
form.append("file", new Blob([csv], { type: "text/csv" }), "playbook.csv");
await fetch(`${BASE}/ingest/file?name=successful_playbooks`, { method: "POST", body: form });
return ok({ learned: true, pattern: `"${b.query}" → ${b.worker_name}` });
}
// Removed 2026-04-20: /intelligence/learn was a legacy CSV writer
// that destructively re-wrote successful_playbooks. /log and
// /log_failure replace it cleanly via /vectors/playbook_memory/seed
// and /mark_failed. Keeping the endpoint would only mislead
// future callers — dead code rots.
// Intelligence: Activity feed — what the system has learned
if (url.pathname === "/intelligence/activity" && req.method === "POST") {

View File

@ -619,6 +619,39 @@ function showProfile(workerData){
body.appendChild(srcBox);
}
// Call history — recruiter-facing institutional memory from call_log.
// Queries for prior contact with this specific worker (by name
// cross-ref). Fails soft: if no rows, shows "no recent contact" which
// is itself a useful signal (or an honest tell about data sparsity).
addSection(body,'Recent Contact','Last phone outreach logged in call_log');
var callBox=document.createElement('div');
callBox.style.cssText='background:#0d1117;border-radius:8px;padding:14px;margin-bottom:20px;font-size:12px;color:#8b949e;line-height:1.6';
callBox.textContent='Checking call log...';body.appendChild(callBox);
var nameLitC=(workerData.nm||'').replace(/'/g,"''");
var callSQL="SELECT cl.timestamp, cl.recruiter, cl.duration_seconds, cl.disposition "
+"FROM call_log cl JOIN candidates c ON c.candidate_id = cl.candidate_id "
+"WHERE CONCAT(c.first_name, ' ', c.last_name) = '"+nameLitC+"' "
+"ORDER BY cl.timestamp DESC LIMIT 3";
api('/sql',{sql:callSQL}).then(function(r){
callBox.textContent='';
var rows=(r&&r.rows)||[];
if(rows.length===0){
callBox.textContent='No recent call logged for '+(workerData.nm||'this worker')+'. Data note: call_log cross-references candidate IDs that may not align with workers_500k — real ATS integration required for full coverage.';
callBox.style.color='#484f58';return;
}
rows.forEach(function(c){
var row=document.createElement('div');row.style.cssText='padding:6px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;border-left:2px solid #58a6ff;display:flex;justify-content:space-between;gap:10px';
var left=document.createElement('div');
var ts=(c.timestamp||'').substring(0,10);
var dur=Math.round((c.duration_seconds||0)/60);
var l1=document.createElement('div');l1.style.cssText='color:#e6edf3;font-weight:500;font-size:12px';
l1.textContent=ts+(c.recruiter?' · by '+c.recruiter:'');left.appendChild(l1);
var l2=document.createElement('div');l2.style.cssText='color:#8b949e;font-size:10px';
l2.textContent=(c.disposition||'?').replace(/_/g,' ')+(dur?' · '+dur+' min':'');left.appendChild(l2);
row.appendChild(left);callBox.appendChild(row);
});
}).catch(function(){callBox.textContent='(call log unavailable)';callBox.style.color='#484f58'});
// Past playbook history — Phase 19 institutional memory surfaced on
// the worker's own profile. Shows every past fill this worker was
// endorsed in (from successful_playbooks_live), so the recruiter can
@ -700,14 +733,35 @@ function addWorkerInsight(parent,name,detail,why,idx,highlight){
var info=document.createElement('div');info.className='info';
var nm=document.createElement('div');nm.className='nm';nm.textContent=name;
// Phase 19: when a past playbook endorsed this worker, show a green chip
// next to the name. Hover reveals the citation IDs.
// next to the name. Hover reveals a NARRATIVE of past endorsements
// derived from successful_playbooks_live — "filled X in Y on date" —
// rather than opaque pb-seed-xxx ids. Recruiters need stories, not
// citation keys. Lazy-loaded per card on first render.
if(boostInfo && boostInfo.boost > 0){
var chip=document.createElement('span');
chip.style.cssText='display:inline-block;margin-left:8px;padding:2px 7px;border-radius:9px;font-size:10px;font-weight:600;background:#0d2818;border:1px solid #2ea043;color:#3fb950;vertical-align:middle';
chip.style.cssText='display:inline-block;margin-left:8px;padding:2px 7px;border-radius:9px;font-size:10px;font-weight:600;background:#0d2818;border:1px solid #2ea043;color:#3fb950;vertical-align:middle;cursor:help';
var n=(boostInfo.citations && boostInfo.citations.length) || 0;
chip.textContent='Endorsed · '+n+' playbook'+(n!==1?'s':'');
chip.title='Boosted by past playbooks: '+(boostInfo.citations||[]).join(', ');
chip.title='Loading past playbooks for '+name+'...';
nm.appendChild(chip);
// Fetch narrative for this worker lazily
var safeName = (name||'').replace(/'/g,"''");
var narrativeSQL = "SELECT operation, result, timestamp FROM successful_playbooks_live "
+ "WHERE result LIKE '%"+safeName+"%' ORDER BY timestamp DESC LIMIT 5";
api('/sql',{sql:narrativeSQL}).then(function(r){
var rows=(r&&r.rows)||[];
if(rows.length===0){
chip.title=name+' — endorsed in '+n+' playbook'+(n!==1?'s':'')+' (narrative unavailable — may have been seeded without SQL persistence)';
return;
}
var stories=rows.map(function(pb){
var d=(pb.timestamp||'').substring(0,10);
return '• '+(pb.operation||'?').replace(/^fill:\s*/,'')+' ('+d+')';
});
chip.title=name+' — past endorsements:\n'+stories.join('\n');
}).catch(function(){
chip.title=name+' — endorsed in '+n+' playbook'+(n!==1?'s':'');
});
}
var dt=document.createElement('div');dt.className='detail';dt.textContent=detail;
info.appendChild(nm);info.appendChild(dt);
@ -770,16 +824,31 @@ function doSearch(){
var h=document.createElement('div');h.style.cssText='color:#8b949e;font-size:12px;margin-bottom:10px';
h.textContent=(d.sql_matches?d.sql_matches.toLocaleString()+' workers matched — ':'')+'showing best results ('+(d.duration_ms||0)+'ms)';
out.appendChild(h);
// Meta-index signal — what past similar fills had in common. Only
// renders when memory had at least one relevant playbook.
if(d.discovered_pattern && d.pattern_playbooks_matched > 0){
// Meta-index signal — ALWAYS render when the system has any memory,
// even if no trait crossed threshold. Silence here would have
// recruiters assume "no signal" when the reality is "threshold
// filtered it out" or "memory is sparse for this geo." Trust
// depends on the system being honest about what it doesn't know.
if(d.pattern_playbooks_matched > 0 || d.discovered_pattern){
var mem=document.createElement('div');
mem.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5';
var label=document.createElement('span');label.style.cssText='color:#3fb950;font-weight:600;margin-right:6px';
label.textContent='MEMORY ('+d.pattern_playbooks_matched+' playbooks):';
label.textContent='MEMORY ('+(d.pattern_playbooks_matched||0)+' playbook'+(d.pattern_playbooks_matched===1?'':'s')+'):';
mem.appendChild(label);
mem.appendChild(document.createTextNode(' '+d.discovered_pattern));
var pattern = d.discovered_pattern || '';
if(!pattern || pattern.indexOf('No similar')>=0 || pattern.indexOf('0 workers')>=0){
mem.appendChild(document.createTextNode(' memory is sparse for this role+geo — no trait crossed threshold. Will accumulate as fills land.'));
mem.style.color='#6ca885';
} else {
mem.appendChild(document.createTextNode(' '+pattern));
}
out.appendChild(mem);
} else {
// Zero playbooks matched — be explicit
var mem0=document.createElement('div');
mem0.style.cssText='background:#161b22;border:1px solid #21262d;border-radius:6px;padding:6px 12px;margin-bottom:10px;font-size:11px;color:#6e7681';
mem0.textContent='MEMORY: no similar past playbooks yet — first fill of this kind will seed it.';
out.appendChild(mem0);
}
// Render results based on type
var workers=d.sql_results||[];