Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/app/blog/blog-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export const BLOG_CSS = `
.kb-article blockquote{border-left:3px solid var(--accent);margin:0 0 22px;padding:4px 0 4px 20px}
.kb-article blockquote p{margin:0;color:var(--ink-2);font-style:italic}
.kb-article hr{border:0;border-top:1px solid var(--border);margin:36px 0}
.kb-article img{display:block;width:100%;height:auto;margin:30px 0 8px;border:1px solid var(--border);border-radius:14px;background:var(--surface)}
.kb-article p>em:only-child{display:block;text-align:center;font-size:13px;color:var(--ink-3);font-style:normal;margin:-2px 0 26px}
.kb-related{max-width:720px;margin:0 auto;padding:0 40px 56px}
.kb-related-row{display:flex;gap:10px;flex-wrap:wrap}
.kb-related a{font-family:var(--font-mono),monospace;font-size:12px;color:var(--accent);text-decoration:none;border:1px solid var(--border);background:var(--surface);border-radius:999px;padding:6px 14px}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ Then the heavy lifting: every discrepant item had to be walked back through Odoo

By the end, the meeting had produced a dozen concrete decisions and tasks: what was wrong, what the corrected number was, who fixed it, and by when. The problem is what usually happens to those decisions after the call ends.

![Knowcap verification inbox showing four confirmed claims from the reconciliation meeting — each with its category, the named human who confirmed it, and the timestamp it was said.](/blog/odoo-inventory-reconciliation-problem-hiding-in-your-meetings/verification-inbox.svg)

*The actual claims behind this post — extracted automatically, each promoted to evidence by a named human. Nothing acts until it's confirmed.*

## What went wrong — and it wasn't the count

The inflection point was a single line: this was the third incorrect inventory sheet the lead had received. The reconciliation itself went fine. The recurring part is the failure — bad data arriving again and again, each round re-litigated from scratch because the last round's corrections lived only in someone's memory and a Telegram thread. The same call also surfaced a quieter cost: a team member had earlier stopped work entirely because he'd been told his output was wrong, with no clear record of what "right" was. Conflicting, unrecorded instructions don't just waste a meeting — they stall the people downstream of it.
Expand Down
1 change: 1 addition & 0 deletions app/lib/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function renderInline(s: string): string {
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/(^|[\s(])\*([^*\s][^*]*)\*/g, '$1<em>$2</em>')
.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_m, alt, src) => `<img src="${src}" alt="${alt}" loading="lazy" />`)
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, text, href) => {
const external = /^https?:\/\//.test(href) && !href.startsWith('https://knowcap.ai')
const rel = external ? ' target="_blank" rel="noopener noreferrer"' : ''
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions routines/blogger/lib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# blogger/lib — verification-panel generator

Generates a Knowcap **verification-inbox SVG** from a post's human-confirmed claims — the cheap, repeatable, **no-browser** alternative to screenshotting the live app.

## Why this exists

Screenshotting `app.knowcap.ai` (connect-chrome / headless) is slow, needs auth, and has to be redone every post. But the verification UI is deterministic and the blogger routine already pulls the real confirmed claims (Knowcap MCP). So we render those claims to SVG from data + the [DESIGN.md](../../../docs/DESIGN.md) tokens. No browser, ever. One asset per post, generated in seconds.

It's a faithful **styled replica fed real data**, not a literal pixel-grab — which for marketing is usually better (no PII to mask, no browser chrome to crop, crisp at any size, always on-brand, accurate to the post's own claims). If literal pixels are ever needed, connect-chrome is the fallback.

## Usage

```bash
node routines/blogger/lib/gen-verification-panel.mjs <claims.json> <out.svg>
```

Input JSON (`examples/odoo-inventory-reconciliation.claims.json` is a worked example):

```json
{
"project": "Odoo Partners",
"source": "Outbound Inventory Audit & Reconciliation",
"totalConfirmed": 9,
"claims": [
{ "category": "decision|task|risk|fact|note",
"summary": "one-line claim", "detail": "optional second line",
"confirmer": "Hassan Arslan", "timestamp": "34:13" }
]
}
```

Renders up to 6 claims. Category sets the accent (decision=blue, task=green, risk=amber, fact/note=grey).

## Routine step (case-study mode only)

After the draft passes its gates, the blogger routine should:

1. Take the `verified_claims` it already cited from `knowcap_sources` (the human-confirmed memories).
2. Map 3–5 of them to the input JSON shape (category, summary, detail, confirmer = the memory's `reviewer`, timestamp = `mm:ss` from `resolved_timestamp_seconds`). Skip if mode ≠ case-study or < 3 confirmed.
3. Generate the SVG to **the web-served path** (NOT the hub — hub assets don't deploy):
```
app/public/blog/<slug>/verification-inbox.svg
```
4. Embed it in the draft body, right after the story-spine section:
```markdown
![Knowcap verification inbox — confirmed claims from this meeting, each with its category, the named confirmer, and the timestamp.](/blog/<slug>/verification-inbox.svg)

*The actual claims behind this post — extracted automatically, each confirmed by a named human.*
```

The blog renderer supports `![alt](src)` images (added in `app/lib/blog.ts`), styled by `app/app/blog/blog-styles.ts`.

## Honesty rule

Only render claims that are genuinely **human-confirmed** (`review_status: confirmed`, `confirmation_source: human`). The whole point of the panel is to *show* the verification workflow — rendering unconfirmed claims as "Confirmed" would be a lie that defeats the thesis.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"project": "Odoo Partners",
"source": "Outbound Inventory Audit & Reconciliation",
"totalConfirmed": 9,
"claims": [
{
"category": "decision",
"summary": "Carafe Small quantity corrected 3 → 2 — the entry of 3 was a mistake.",
"detail": "Rationale: actual dispatched quantity was 2; the sheet value was a typo.",
"confirmer": "Hassan Arslan",
"timestamp": "34:13"
},
{
"category": "risk",
"summary": "Third incorrect inventory sheet this cycle — a recurring data-accuracy problem.",
"detail": "Severity: medium · mitigation: verify before submission, find the root cause.",
"confirmer": "Hassan Arslan",
"timestamp": "42:54"
},
{
"category": "task",
"summary": "Joud → resolve two \"brandy\" items dispatched with no transfer or pick record.",
"detail": "Assignee: Joud Aljunaidy · priority: high · due: today",
"confirmer": "Hassan Arslan",
"timestamp": "33:33"
},
{
"category": "fact",
"summary": "128 + ~250 item discrepancy between the warehouse sheet and Odoo.",
"detail": "Source: final comparison sheet, warehouse data vs Odoo · sent via Telegram.",
"confirmer": "Hassan Arslan",
"timestamp": "01:12"
}
]
}
107 changes: 107 additions & 0 deletions routines/blogger/lib/gen-verification-panel.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env node
// gen-verification-panel.mjs — generate a Knowcap "verification inbox" SVG panel
// from a post's HUMAN-CONFIRMED claims. No browser, no deps, deterministic.
//
// This is the cheap, repeatable alternative to screenshotting the live app:
// the verification UI is deterministic, and the blogger routine already has the
// real confirmed claims (Knowcap MCP). So we render them to SVG from data +
// the DESIGN.md tokens instead of capturing pixels through a browser.
//
// Usage:
// node gen-verification-panel.mjs <claims.json> <out.svg>
//
// Input JSON shape:
// {
// "project": "Odoo Partners",
// "source": "Outbound Inventory Audit & Reconciliation",
// "totalConfirmed": 9,
// "claims": [
// { "category": "decision|task|risk|fact|note",
// "summary": "...", "detail": "...",
// "confirmer": "Hassan Arslan", "timestamp": "34:13" }
// ]
// }
// Renders up to 6 claims. Output is a self-contained SVG (served as a normal <img>).

import fs from 'fs'

const [, , inPath, outPath] = process.argv
if (!inPath || !outPath) {
console.error('usage: gen-verification-panel.mjs <claims.json> <out.svg>')
process.exit(1)
}
const data = JSON.parse(fs.readFileSync(inPath, 'utf8'))

const esc = (s) =>
String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
const clip = (s, n) => {
s = String(s ?? '')
return s.length > n ? s.slice(0, n - 1).trimEnd() + '…' : s
}

const CAT = {
decision: { label: 'DECISION', bar: '#4C8DF6', pill: '#EAF1FE', text: '#2C5FB0' },
task: { label: 'TASK', bar: '#1F6B3A', pill: '#EAF3EC', text: '#1F6B3A' },
risk: { label: 'RISK', bar: '#E0A33A', pill: '#FBF1DC', text: '#A9781E' },
fact: { label: 'FACT', bar: '#8A8A8A', pill: '#EFEDE9', text: '#5A5A5A' },
note: { label: 'NOTE', bar: '#8A8A8A', pill: '#EFEDE9', text: '#5A5A5A' },
}

const W = 1160, PAD = 40, CARD_W = 1080, CARD_H = 116, GAP = 12, HEAD = 124, FOOT = 52
const claims = (data.claims || []).slice(0, 6)
const H = HEAD + claims.length * (CARD_H + GAP) - GAP + FOOT

const badge = (confirmer, ts) => `
<g transform="translate(24,80)">
<circle cx="8" cy="8" r="8" fill="#1F6B3A"/>
<path d="M4.5 8 L7 10.5 L11.5 5.5" stroke="#fff" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<text x="24" y="12" class="mono" font-size="12" fill="#1F6B3A" font-weight="700">Confirmed</text>
<text x="104" y="12" class="mono faint" font-size="12">${esc(confirmer)}${ts ? ' · ' + esc(ts) : ''}</text>
</g>`

const card = (c, i) => {
const cat = CAT[c.category] || CAT.note
const y = HEAD + i * (CARD_H + GAP)
const pillW = 30 + cat.label.length * 8
return `
<g transform="translate(${PAD},${y})" filter="url(#sh)">
<rect width="${CARD_W}" height="${CARD_H}" rx="12" fill="#FFFFFF" stroke="#E7E3DC"/>
<rect x="0" y="0" width="4" height="${CARD_H}" rx="2" fill="${cat.bar}"/>
<rect x="24" y="22" width="${pillW}" height="22" rx="11" fill="${cat.pill}"/>
<text x="${24 + pillW / 2}" y="37" text-anchor="middle" class="mono" font-size="11" fill="${cat.text}" font-weight="700">${cat.label}</text>
<text x="${24 + pillW + 16}" y="39" class="ink" font-size="16" font-weight="600">${esc(clip(c.summary, 86))}</text>
${c.detail ? `<text x="${24 + pillW + 16}" y="64" class="mut" font-size="13">${esc(clip(c.detail, 104))}</text>` : ''}
${badge(c.confirmer || 'a named human', c.timestamp)}
</g>`
}

const badgeLabel = `${data.totalConfirmed ?? claims.length} claims · all human-confirmed`
const bw = Math.round(36 + badgeLabel.length * 7.2)

const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family="'Space Grotesk', system-ui, -apple-system, sans-serif">
<defs>
<style>
.mono{font-family:'JetBrains Mono',ui-monospace,Menlo,Consolas,monospace}
.ink{fill:#18181B}.mut{fill:#6B6B6B}.faint{fill:#9A958C}
</style>
<filter id="sh" x="-4%" y="-4%" width="108%" height="116%">
<feDropShadow dx="0" dy="2" stdDeviation="6" flood-color="#18181B" flood-opacity="0.06"/>
</filter>
</defs>
<rect width="${W}" height="${H}" fill="#FBFAF8"/>
<rect x="1" y="1" width="${W - 2}" height="${H - 2}" rx="16" fill="none" stroke="#ECE8E1"/>
<text x="${PAD}" y="58" class="ink" font-size="26" font-weight="700">Verification Inbox</text>
<text x="${PAD}" y="86" class="mono mut" font-size="13">${esc(clip([data.project, data.source].filter(Boolean).join(' · '), 92))}</text>
<g transform="translate(${W - PAD - bw},40)">
<rect width="${bw}" height="34" rx="17" fill="#EAF3EC" stroke="#CDE5D4"/>
<circle cx="20" cy="17" r="4" fill="#1F6B3A"/>
<text x="34" y="22" class="mono" font-size="12" fill="#1F6B3A" font-weight="700">${esc(badgeLabel)}</text>
</g>
<line x1="${PAD}" y1="106" x2="${W - PAD}" y2="106" stroke="#ECE8E1"/>
${claims.map(card).join('\n')}
<text x="${PAD}" y="${H - 24}" class="mono faint" font-size="12">Agents read only these — search_memories(verification_strictness: "human_only"). No "Confirm All" button exists.</text>
</svg>
`

fs.writeFileSync(outPath, svg)
console.log(`wrote ${outPath} (${claims.length} claims, ${W}x${H})`)