← back to Feature Harvest
feature-harvest: add skills + agents as review sources (244+74) + officer-recommendation badges from 7-VP cabinet review
921f77f8e7c0a37f14b30b01fa0bb155ce967d81 · 2026-06-26 10:11:47 -0700 · Steve
Files touched
M build-corpus.jsA data/officer-recs.jsonM public/index.htmlM server.js
Diff
commit 921f77f8e7c0a37f14b30b01fa0bb155ce967d81
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jun 26 10:11:47 2026 -0700
feature-harvest: add skills + agents as review sources (244+74) + officer-recommendation badges from 7-VP cabinet review
---
build-corpus.js | 57 ++++++
data/officer-recs.json | 532 +++++++++++++++++++++++++++++++++++++++++++++++++
public/index.html | 17 +-
server.js | 7 +
4 files changed, 610 insertions(+), 3 deletions(-)
diff --git a/build-corpus.js b/build-corpus.js
index 0051802..fa27ec2 100644
--- a/build-corpus.js
+++ b/build-corpus.js
@@ -150,10 +150,65 @@ function addHtmlTools() {
return n;
}
+// ---- D) skills + E) agents (the "forgotten tool" review) ----------
+function frontmatter(file) {
+ try {
+ const t = fs.readFileSync(file, 'utf8');
+ const m = t.match(/^---\n([\s\S]*?)\n---/);
+ if (!m) return {};
+ const fm = {};
+ const nm = m[1].match(/^name:\s*(.+)$/m); if (nm) fm.name = nm[1].trim();
+ const ds = m[1].match(/^description:\s*([\s\S]+?)(?:\n[a-z_]+:|\n*$)/m);
+ if (ds) fm.description = ds[1].replace(/\s+/g, ' ').trim();
+ return fm;
+ } catch { return {}; }
+}
+function addSkills() {
+ const root = path.join(HOME, '.claude/skills');
+ if (!fs.existsSync(root)) return 0;
+ let n = 0;
+ for (const name of fs.readdirSync(root)) {
+ const sk = path.join(root, name, 'SKILL.md');
+ if (!fs.existsSync(sk)) continue;
+ let st; try { st = fs.statSync(sk); } catch { continue; }
+ const fm = frontmatter(sk);
+ items.push({
+ id: `skill:${name}`, source: 'skill', title: fm.name || name, project: 'skills',
+ date: st.mtime.toISOString().slice(0, 10), ts: st.mtime.getTime(),
+ summary: (fm.description || '').slice(0, 500), value: '', commit: '',
+ path: sk, port: '', hasUI: false,
+ });
+ n++;
+ }
+ return n;
+}
+function addAgents() {
+ const root = path.join(HOME, '.claude/agents');
+ if (!fs.existsSync(root)) return 0;
+ let n = 0;
+ for (const f of fs.readdirSync(root)) {
+ if (!f.endsWith('.md')) continue;
+ const fp = path.join(root, f);
+ let st; try { st = fs.statSync(fp); if (!st.isFile()) continue; } catch { continue; }
+ const fm = frontmatter(fp);
+ items.push({
+ id: `agent:${f.replace(/\.md$/, '')}`, source: 'agent',
+ title: fm.name || f.replace(/\.md$/, ''), project: 'agents',
+ date: st.mtime.toISOString().slice(0, 10), ts: st.mtime.getTime(),
+ summary: (fm.description || '').slice(0, 500), value: '', commit: '',
+ path: fp, port: '', hasUI: false,
+ });
+ n++;
+ }
+ return n;
+}
+
// ---- run ----------------------------------------------------------
const nWins = addWins();
const nDW = addDirApps(path.join(HOME, 'Projects/Designer-Wallcoverings/DW-Programming'), 'dw-app', 'DW-Programming');
const nTools = addHtmlTools();
+const nSkills = addSkills();
+const nAgents = addAgents();
// stable sort: newest first
items.sort((a, b) => (b.ts || 0) - (a.ts || 0));
@@ -162,4 +217,6 @@ console.log(`corpus built → ${OUT}`);
console.log(` wins: ${nWins}`);
console.log(` dw-apps: ${nDW}`);
console.log(` wpb-tools:${nTools}`);
+console.log(` skills: ${nSkills}`);
+console.log(` agents: ${nAgents}`);
console.log(` TOTAL: ${items.length}`);
diff --git a/data/officer-recs.json b/data/officer-recs.json
new file mode 100644
index 0000000..5c12e35
--- /dev/null
+++ b/data/officer-recs.json
@@ -0,0 +1,532 @@
+{
+ "price-finder": {
+ "name": "price-finder",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "dw-commerce"
+ ],
+ "count": 1,
+ "practice": "Confirm net cost + trade discount before any import",
+ "why": "Closes the ~30k no-cost-held SKU gap; gates publish on real cost not a guess",
+ "conflict": false
+ },
+ "pairs-well-with": {
+ "name": "pairs-well-with",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "dw-commerce"
+ ],
+ "count": 1,
+ "practice": "Every PDP gets 6 cross-sell tiles (60-30-10)",
+ "why": "Pure AOV lift on 64k active PDPs — built and unused",
+ "conflict": false
+ },
+ "competitor-of-product": {
+ "name": "competitor-of-product",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "dw-commerce"
+ ],
+ "count": 1,
+ "practice": "Per-SKU \"who else sells this MFR SKU\" web check",
+ "why": "Pricing intel: surfaces where we are undercut vs exclusive",
+ "conflict": false
+ },
+ "kravet-price-watch": {
+ "name": "kravet-price-watch",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "dw-commerce"
+ ],
+ "count": 1,
+ "practice": "Auto-ingest Kravet price-list emails into authoritative MAP",
+ "why": "Stale cost = MAP breach across ~4,500 ROLL SKUs; self-healing",
+ "conflict": false
+ },
+ "collection-creator": {
+ "name": "collection-creator",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT",
+ "ADOPT"
+ ],
+ "officers": [
+ "dw-commerce",
+ "dw-marketing"
+ ],
+ "count": 2,
+ "practice": "Catalog-driven curated collections w/ rationale",
+ "why": "SEO landing pages + campaign hooks across 160k products, dormant",
+ "conflict": false
+ },
+ "weekend-csv-products": {
+ "name": "weekend-csv-products",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "dw-commerce"
+ ],
+ "count": 1,
+ "practice": "Scheduled weekend-ordered SKU export to purchasing",
+ "why": "Faster vendor POs, fewer fulfillment-delay refunds",
+ "conflict": false
+ },
+ "architectural-update-skill": {
+ "name": "architectural-update-skill",
+ "verdict": "RETIRE",
+ "verdicts": [
+ "RETIRE"
+ ],
+ "officers": [
+ "dw-commerce"
+ ],
+ "count": 1,
+ "practice": "(de-dup paid-rebrand churn)",
+ "why": "Overlaps enrich-ai-tags + conflicts with the SEO vendor-name carve-out",
+ "conflict": false
+ },
+ "advertising-signals": {
+ "name": "advertising-signals",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE",
+ "AUTOMATE",
+ "AUTOMATE"
+ ],
+ "officers": [
+ "dw-marketing",
+ "research-content",
+ "directories"
+ ],
+ "count": 3,
+ "practice": "Weekly competitor ad-platform reconnaissance",
+ "why": "Tells DW where rivals buy ads so it stops guessing spend; zero API cost",
+ "conflict": false
+ },
+ "dw-pinterest": {
+ "name": "dw-pinterest",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "dw-marketing"
+ ],
+ "count": 1,
+ "practice": "Pinterest as primary visual-discovery funnel",
+ "why": "Highest-intent interior-design search funnel; DW has zero presence",
+ "conflict": false
+ },
+ "analytics": {
+ "name": "analytics",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE",
+ "AUTOMATE"
+ ],
+ "officers": [
+ "dw-marketing",
+ "directories"
+ ],
+ "count": 2,
+ "practice": "GA4 coverage + weekly traffic reporting across the fleet",
+ "why": "Spend/posts ship blind today — the conversion-measurement foundation",
+ "conflict": false
+ },
+ "dw-youtube": {
+ "name": "dw-youtube",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "dw-marketing"
+ ],
+ "count": 1,
+ "practice": "YouTube Shorts + room-walkthrough channel",
+ "why": "Evergreen how-to/inspiration search capture; near-zero output cost",
+ "conflict": false
+ },
+ "stampede": {
+ "name": "stampede",
+ "verdict": "RETIRE",
+ "verdicts": [
+ "RETIRE"
+ ],
+ "officers": [
+ "dw-marketing"
+ ],
+ "count": 1,
+ "practice": "(de-dup creative tooling)",
+ "why": "Overlaps four-horsemen + canvas-design; produced nothing for DW",
+ "conflict": false
+ },
+ "claude-codex-kimi": {
+ "name": "claude-codex-kimi",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "engineering"
+ ],
+ "count": 1,
+ "practice": "Multi-LLM adversarial review before merge",
+ "why": "Catches the silent-bug class single-pass review misses, ~5c/run",
+ "conflict": false
+ },
+ "secret-strip": {
+ "name": "secret-strip",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "engineering"
+ ],
+ "count": 1,
+ "practice": "Fleet-wide hardcoded-credential scrub, weekly",
+ "why": "Closes the recurring 478-file leak class (May-30 compromise)",
+ "conflict": false
+ },
+ "validation-loop": {
+ "name": "validation-loop",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "engineering"
+ ],
+ "count": 1,
+ "practice": "Re-run the review panel on POST-edit code",
+ "why": "Stops \"shipped without re-testing\" false-confidence regressions",
+ "conflict": false
+ },
+ "test-engineer": {
+ "name": "test-engineer",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "engineering"
+ ],
+ "count": 1,
+ "practice": "Tests + CI on the cadence importer + budget/refund paths",
+ "why": "The live revenue path has had repeated silent-drain/publish bugs",
+ "conflict": false
+ },
+ "compound-engineering": {
+ "name": "compound-engineering",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "engineering"
+ ],
+ "count": 1,
+ "practice": "Plan-Work-Review-Compound: codify each fix as a guardrail",
+ "why": "Turns one-off debugging into permanent velocity leverage",
+ "conflict": false
+ },
+ "debugger": {
+ "name": "debugger",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "engineering"
+ ],
+ "count": 1,
+ "practice": "Structured root-cause vs ad-hoc poking",
+ "why": "Recurring \"cost hours\" incidents are classic misdiagnoses",
+ "conflict": false
+ },
+ "codex-debate": {
+ "name": "codex-debate",
+ "verdict": "RETIRE",
+ "verdicts": [
+ "RETIRE"
+ ],
+ "officers": [
+ "engineering"
+ ],
+ "count": 1,
+ "practice": "(consolidate debate skills)",
+ "why": "Redundant with cck + dtd; four overlapping debate skills = clutter",
+ "conflict": false
+ },
+ "competitors": {
+ "name": "competitors",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "research-content"
+ ],
+ "count": 1,
+ "practice": "Name the structural differentiation vectors rivals cannot copy",
+ "why": "The artifact that makes a pitch land; unused outside SDCC",
+ "conflict": false
+ },
+ "session-debrief": {
+ "name": "session-debrief",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "research-content"
+ ],
+ "count": 1,
+ "practice": "End every session with a VC-lens recap MP4",
+ "why": "Storytelling as byproduct — highest ROI/min content skill, idle",
+ "conflict": false
+ },
+ "peer-survey": {
+ "name": "peer-survey",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "research-content"
+ ],
+ "count": 1,
+ "practice": "Screenshot N peers, steal one UX move each, before redesign",
+ "why": "Grounds design in market reality not taste",
+ "conflict": false
+ },
+ "info-hub": {
+ "name": "info-hub",
+ "verdict": "CONFLICT",
+ "verdicts": [
+ "ADOPT",
+ "RETIRE"
+ ],
+ "officers": [
+ "research-content",
+ "directories"
+ ],
+ "count": 2,
+ "practice": "Curated authoritative-source hub per vertical = trust/SEO moat",
+ "why": "Hard-to-copy content asset; built once for SDCC, never reused",
+ "conflict": true
+ },
+ "ad-social-tracker": {
+ "name": "ad-social-tracker",
+ "verdict": "CONFLICT",
+ "verdicts": [
+ "RETIRE",
+ "ADOPT"
+ ],
+ "officers": [
+ "research-content",
+ "directories"
+ ],
+ "count": 2,
+ "practice": "(consolidate to advertising-signals)",
+ "why": "Always-on directory batch version is redundant + unused",
+ "conflict": true
+ },
+ "appointment-agent": {
+ "name": "appointment-agent",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "directories"
+ ],
+ "count": 1,
+ "practice": "venturacorridor booking + calendar-connect health check",
+ "why": "Live revenue surface untouched since May; weekly liveness probe",
+ "conflict": false
+ },
+ "site-audit": {
+ "name": "site-audit",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "directories"
+ ],
+ "count": 1,
+ "practice": "SEO/a11y/dead-link + 90-day roadmap per directory front door",
+ "why": "Surfaces SEO gaps blocking inbound (only allowed growth channel)",
+ "conflict": false
+ },
+ "dw-restore-rehearsal": {
+ "name": "dw-restore-rehearsal",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "operations"
+ ],
+ "count": 1,
+ "practice": "Backups must be proven restorable, not just present",
+ "why": "A fresh dump that wont restore is worse than none; monthly drill",
+ "conflict": false
+ },
+ "secrets-env-backup": {
+ "name": "secrets-env-backup",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "operations"
+ ],
+ "count": 1,
+ "practice": "Secrets registry must have a recoverable off-box backup",
+ "why": "launchd job is exit-1 FAILING NOW — invisible until a disk loss",
+ "conflict": false
+ },
+ "machine-sync": {
+ "name": "machine-sync",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "operations"
+ ],
+ "count": 1,
+ "practice": "Mac2/Mac1/Kamatera must not silently drift apart",
+ "why": "NEVER produced a run — antidote to repeated fleet-collapse recoveries",
+ "conflict": false
+ },
+ "dw-canary-meta-watchdog": {
+ "name": "dw-canary-meta-watchdog",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "operations"
+ ],
+ "count": 1,
+ "practice": "Every canary must prove it is itself still firing",
+ "why": "The 12-day silent pg_dump death is the canonical failure",
+ "conflict": false
+ },
+ "disk-space-canary": {
+ "name": "disk-space-canary",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "operations"
+ ],
+ "count": 1,
+ "practice": "Verify-then-delete + monitor, never autonomous deletion",
+ "why": "Full-disk/forced-sleep are recurring continuity killers",
+ "conflict": false
+ },
+ "keep-alive": {
+ "name": "keep-alive",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "operations"
+ ],
+ "count": 1,
+ "practice": "pm2 online lies — restart on HTTP-dead not just process-exit",
+ "why": "Only thing that auto-revives a hung site serving 500s while pm2=green",
+ "conflict": false
+ },
+ "comms-compliance": {
+ "name": "comms-compliance",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "compliance"
+ ],
+ "count": 1,
+ "practice": "CAN-SPAM/TCPA/CCPA pre-send audit of every mailer/SMS/voice sender",
+ "why": "Only thing between an info@ mailer and a per-email fine; zero scheduled run while mailers ship live",
+ "conflict": false
+ },
+ "dw-leak-scanner": {
+ "name": "dw-leak-scanner",
+ "verdict": "AUTOMATE",
+ "verdicts": [
+ "AUTOMATE"
+ ],
+ "officers": [
+ "compliance"
+ ],
+ "count": 1,
+ "practice": "Private-label leak scan (Brewster/York/WallQuest/Command54 never front-facing)",
+ "why": "Hard-banned names + ~430 SKUs/day ship rate; tighten weekly→daily",
+ "conflict": false
+ },
+ "best-practices": {
+ "name": "best-practices",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "compliance"
+ ],
+ "count": 1,
+ "practice": "Standing-rules pre-flight before any non-trivial action",
+ "why": "Codifies every MEMORY.md rule whose skip caused real incidents (285 spec-less publishes)",
+ "conflict": false
+ },
+ "dw-legal-compliance": {
+ "name": "dw-legal-compliance",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "compliance"
+ ],
+ "count": 1,
+ "practice": "Trademark/contract/description legal review for vendor copy",
+ "why": "Should gate every vendor-landing + microsite description build; on-demand only today",
+ "conflict": false
+ },
+ "settlement": {
+ "name": "settlement",
+ "verdict": "ADOPT",
+ "verdicts": [
+ "ADOPT"
+ ],
+ "officers": [
+ "compliance"
+ ],
+ "count": 1,
+ "practice": "Pre-generation legal gate on ALL pattern creation",
+ "why": "Court-binding; only wired into 3 pipelines — any path skipping it is a breach vector",
+ "conflict": false
+ }
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 3b11962..4263b34 100644
--- a/public/index.html
+++ b/public/index.html
@@ -59,6 +59,14 @@
.src.win{background:rgba(52,165,244,.16);color:var(--win)}
.src.dw-app{background:rgba(224,169,59,.16);color:var(--app)}
.src.wpb-tool{background:rgba(176,124,240,.16);color:var(--tool)}
+ .src.skill{background:rgba(61,220,151,.16);color:#3ddc97}
+ .src.agent{background:rgba(240,140,90,.16);color:#f08c5a}
+ /* officer recommendation badge */
+ .rec{display:inline-flex;gap:6px;align-items:center;margin:10px 0 0;padding:8px 11px;border-radius:9px;font-size:13px;
+ border-left:3px solid var(--gold);background:rgba(202,164,90,.08)}
+ .rec b{color:var(--gold)} .rec .v{font-weight:800;font-size:11px;padding:1px 7px;border-radius:5px;background:var(--gold);color:#1a1407}
+ .rec .v.RETIRE{background:var(--skip);color:#fff}
+ .mini .recdot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--gold);margin-left:5px}
.when{color:var(--mut);font-size:12px;margin-left:auto;white-space:nowrap}
.verdict-tag{font-size:11px;font-weight:800;padding:2px 8px;border-radius:6px}
.vt-dw{background:rgba(202,164,90,.18);color:var(--dw)}
@@ -131,6 +139,8 @@
<option value="win">Shipped wins</option>
<option value="dw-app">DW apps</option>
<option value="wpb-tool">WPB tools</option>
+ <option value="skill">Skills</option>
+ <option value="agent">Agents</option>
</select>
<select id="fStatus">
<option value="unreviewed">Unreviewed</option>
@@ -164,7 +174,7 @@
<script>
const $ = (s,r=document)=>r.querySelector(s);
const $$ = (s,r=document)=>[...r.querySelectorAll(s)];
-const SRC_LABEL = {win:'Shipped win', 'dw-app':'DW app', 'wpb-tool':'WPB tool'};
+const SRC_LABEL = {win:'Shipped win', 'dw-app':'DW app', 'wpb-tool':'WPB tool', skill:'Skill', agent:'Agent'};
const DEC = {dw:'→ DW', wpb:'→ WPB', both:'Both', skip:'Skip'};
let ITEMS=[], V={}, qi=0;
const state = {
@@ -290,6 +300,7 @@ function cardHTML(it, big){
const path = it.path ? `<div class="pathline">📁 <code>${esc(it.path)}</code>${it.port?` · port ${esc(it.port)}`:''}${it.hasUI?' · has UI':''}</div>` : '';
const commit = it.commit ? ` · <code>${esc(it.commit)}</code>` : '';
const val = it.value ? `<div class="value"><b>Value:</b> ${esc(it.value)}</div>` : '';
+ const rec = it.rec ? `<div class="rec"><span class="v ${it.rec.conflict?'RETIRE':''}">${it.rec.conflict?('CONFLICT: '+it.rec.verdicts.join(' / ')):it.rec.verdict}</span> <b>${it.rec.count} officer${it.rec.count>1?'s':''}</b> (${esc(it.rec.officers.join(', '))}) · ${esc(it.rec.practice)} — ${esc(it.rec.why)}</div>` : '';
return `
<div class="card" data-id="${esc(it.id)}">
${previewHTML(it)}
@@ -302,7 +313,7 @@ function cardHTML(it, big){
<h2>${esc(it.title)}</h2>
<div class="meta">${esc(it.project||'')}${commit}</div>
<div class="summary">${esc(it.summary||'')||'<span class="hint">no description captured</span>'}</div>
- ${val}${path}
+ ${val}${rec}${path}
</div>
<div class="verdict">
<div class="vbtn dw ${v&&v.decision==='dw'?'on':''}" data-d="dw">→ DW <span class="k">D</span></div>
@@ -366,7 +377,7 @@ function renderBoard(){
function miniHTML(it){
return `<div class="mini" data-id="${esc(it.id)}" title="${esc(it.title)}">
${previewHTML(it)}
- <div class="mt">${esc(it.title)}</div>
+ <div class="mt">${esc(it.title)}${it.rec?'<span class="recdot" title="officer pick"></span>':''}</div>
<div class="ms"><span class="src ${it.source}" style="font-size:9px;padding:1px 5px">${(SRC_LABEL[it.source]||it.source)}</span> ${esc(it.project||'')}</div>
</div>`;
}
diff --git a/server.js b/server.js
index d7fb748..954e9ea 100644
--- a/server.js
+++ b/server.js
@@ -17,6 +17,7 @@ const DATA = path.join(ROOT, 'data');
const SNAP = path.join(DATA, 'wins-snapshot.json');
const ITEMS = path.join(DATA, 'items.json');
const VERDICTS = path.join(DATA, 'verdicts.json');
+const RECS = path.join(DATA, 'officer-recs.json');
const THUMBS = path.join(DATA, 'thumbs');
const THUMB_MANIFEST = path.join(THUMBS, 'manifest.json');
const CNCP_WINS = 'http://127.0.0.1:3333/api/wins';
@@ -59,9 +60,15 @@ function loadWins() { return readJSON(SNAP, []); }
function loadItems() {
const items = readJSON(ITEMS, loadWins());
const man = readJSON(THUMB_MANIFEST, {});
+ const recs = readJSON(RECS, {});
for (const it of items) {
const m = man[it.id];
it.thumb = m && m.ok ? m.file : null; // null → UI shows gradient poster
+ if (it.source === 'skill' || it.source === 'agent') {
+ const key = (it.title || '').toLowerCase();
+ const idkey = (it.id.split(':')[1] || '').toLowerCase();
+ it.rec = recs[key] || recs[idkey] || null; // officer recommendation, if any
+ }
}
return items;
}
← bd3f8ae feature-harvest: handle /favicon.ico (204) — silences consol
·
back to Feature Harvest
·
(newest)