Compare commits
2 commits
0f182236f7
...
99b4f7d3de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99b4f7d3de | ||
|
|
f9354a5c36 |
7 changed files with 991 additions and 4 deletions
BIN
decks/shkor-labs-fundraise/Shkor_Labs_Pre-seed_2026-10.pdf
Normal file
BIN
decks/shkor-labs-fundraise/Shkor_Labs_Pre-seed_2026-10.pdf
Normal file
Binary file not shown.
65
decks/shkor-labs-fundraise/build.py
Normal file
65
decks/shkor-labs-fundraise/build.py
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""deck.src.html → index.html (самодостаточная: шрифты и логотип инлайн) + PDF.
|
||||||
|
|
||||||
|
python3 decks/shkor-labs-fundraise/build.py [--pdf] [--png]
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||||
|
ASSETS = os.path.join(ROOT, "sites", "shkor-com", "v2", "assets")
|
||||||
|
|
||||||
|
|
||||||
|
def b64(name):
|
||||||
|
with open(os.path.join(ASSETS, name), "rb") as f:
|
||||||
|
return "data:font/woff2;base64," + base64.b64encode(f.read()).decode()
|
||||||
|
|
||||||
|
|
||||||
|
LOGO = ('<svg width="30" height="30" viewBox="0 0 200 200"><circle cx="100" cy="100" r="78" '
|
||||||
|
'fill="none" stroke="#c6ff4d" stroke-width="12" stroke-dasharray="20 27" '
|
||||||
|
'stroke-linecap="round"/><circle cx="100" cy="100" r="30" fill="#c6ff4d"/></svg>')
|
||||||
|
|
||||||
|
|
||||||
|
def build():
|
||||||
|
src = open(os.path.join(HERE, "deck.src.html"), encoding="utf-8").read()
|
||||||
|
for k, v in {"__FONT_INTER400__": "inter-400-latin.woff2",
|
||||||
|
"__FONT_INTER600__": "inter-600-latin.woff2",
|
||||||
|
"__FONT_SPACE__": "space-grotesk-latin.woff2",
|
||||||
|
"__FONT_MONO__": "jetbrains-mono-400-latin.woff2"}.items():
|
||||||
|
src = src.replace(k, b64(v))
|
||||||
|
src = src.replace("__LOGO_SMALL__", LOGO)
|
||||||
|
out = os.path.join(HERE, "index.html")
|
||||||
|
open(out, "w", encoding="utf-8").write(src)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def chrome():
|
||||||
|
c = glob.glob(os.path.expanduser("~/.cache/ms-playwright/chromium_headless_shell-*/*/chrome-headless-shell"))
|
||||||
|
return c[0] if c else None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
out = build()
|
||||||
|
print("OK", os.path.relpath(out, ROOT), os.path.getsize(out) // 1024, "KB", file=sys.stderr)
|
||||||
|
ch = chrome()
|
||||||
|
if "--pdf" in sys.argv and ch:
|
||||||
|
pdf = os.path.join(HERE, "Shkor_Labs_Pre-seed_2026-10.pdf")
|
||||||
|
subprocess.run([ch, "--no-sandbox", "--disable-gpu", "--run-all-compositor-stages-before-draw",
|
||||||
|
"--virtual-time-budget=3000", "--no-pdf-header-footer",
|
||||||
|
f"--print-to-pdf={pdf}", "file://" + out], check=True, capture_output=True)
|
||||||
|
print("OK", os.path.relpath(pdf, ROOT), file=sys.stderr)
|
||||||
|
if "--png" in sys.argv and ch:
|
||||||
|
d = os.path.join("/tmp", "deck-png")
|
||||||
|
os.makedirs(d, exist_ok=True)
|
||||||
|
n = len(re.findall(r'<section class="slide', open(out).read()))
|
||||||
|
for i in range(1, n + 1):
|
||||||
|
subprocess.run([ch, "--no-sandbox", "--disable-gpu", "--hide-scrollbars",
|
||||||
|
"--window-size=1920,1080", "--virtual-time-budget=2500",
|
||||||
|
f"--screenshot={d}/s{i:02d}.png", f"file://{out}#{i}"],
|
||||||
|
check=True, capture_output=True)
|
||||||
|
print("OK png →", d, file=sys.stderr)
|
||||||
389
decks/shkor-labs-fundraise/deck.src.html
Normal file
389
decks/shkor-labs-fundraise/deck.src.html
Normal file
|
|
@ -0,0 +1,389 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Shkor Labs — Autopilot for any business · Pre-seed</title>
|
||||||
|
<style>
|
||||||
|
@font-face{font-family:"Inter";font-weight:400;src:url(__FONT_INTER400__) format("woff2")}
|
||||||
|
@font-face{font-family:"Inter";font-weight:600;src:url(__FONT_INTER600__) format("woff2")}
|
||||||
|
@font-face{font-family:"Space Grotesk";font-weight:300 700;src:url(__FONT_SPACE__) format("woff2")}
|
||||||
|
@font-face{font-family:"JetBrains Mono";font-weight:400;src:url(__FONT_MONO__) format("woff2")}
|
||||||
|
:root{
|
||||||
|
--bg:#050506;--bg1:#0b0c0f;--bg2:#111318;--line:rgba(255,255,255,.08);--line2:rgba(255,255,255,.14);
|
||||||
|
--fg:#f2f3f5;--fg2:#b4b8c2;--fg3:#80869a;--acid:#c6ff4d;--acid-dim:rgba(198,255,77,.12);--cyan:#5ce1e6;
|
||||||
|
--display:"Space Grotesk",system-ui,sans-serif;--body:"Inter",system-ui,sans-serif;--mono:"JetBrains Mono",ui-monospace,monospace;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
html,body{background:#000;color:var(--fg);font-family:var(--body);overflow:hidden;height:100%}
|
||||||
|
#stage{position:fixed;inset:0;display:flex;align-items:center;justify-content:center}
|
||||||
|
.deck{width:1920px;height:1080px;position:relative;transform-origin:center center;flex:none}
|
||||||
|
.slide{position:absolute;inset:0;background:var(--bg);overflow:hidden;padding:110px 140px;display:flex;flex-direction:column;
|
||||||
|
opacity:0;pointer-events:none;transition:opacity .5s ease}
|
||||||
|
.slide.on{opacity:1;pointer-events:auto}
|
||||||
|
.slide::before{content:"";position:absolute;inset:0;background:
|
||||||
|
radial-gradient(1200px 700px at 85% -10%,rgba(198,255,77,.07),transparent 60%),
|
||||||
|
radial-gradient(900px 600px at -10% 110%,rgba(92,225,230,.06),transparent 60%);pointer-events:none}
|
||||||
|
.grid-bg::after{content:"";position:absolute;inset:0;pointer-events:none;opacity:.5;
|
||||||
|
background-image:linear-gradient(var(--line) 1px,transparent 1px),linear-gradient(90deg,var(--line) 1px,transparent 1px);
|
||||||
|
background-size:80px 80px;mask-image:radial-gradient(ellipse at 70% 40%,#000 0%,transparent 70%)}
|
||||||
|
.slide>*{position:relative;z-index:1}
|
||||||
|
.kicker{font-family:var(--mono);font-size:22px;letter-spacing:.18em;text-transform:uppercase;color:var(--acid);margin-bottom:28px;display:flex;align-items:center;gap:16px}
|
||||||
|
.kicker::before{content:"";width:12px;height:12px;border-radius:50%;background:var(--acid);box-shadow:0 0 18px var(--acid)}
|
||||||
|
h1{font-family:var(--display);font-weight:700;font-size:150px;line-height:.95;letter-spacing:-.035em}
|
||||||
|
h2{font-family:var(--display);font-weight:700;font-size:84px;line-height:1.02;letter-spacing:-.03em;max-width:1500px}
|
||||||
|
h3{font-family:var(--display);font-weight:600;font-size:38px;letter-spacing:-.01em;margin-bottom:14px}
|
||||||
|
.acid{color:var(--acid)} .cyan{color:var(--cyan)} .dim{color:var(--fg3)}
|
||||||
|
p,.p{font-size:30px;line-height:1.45;color:var(--fg2)}
|
||||||
|
.lead{font-size:38px;line-height:1.4;color:var(--fg2);max-width:1400px;margin-top:36px}
|
||||||
|
.mono{font-family:var(--mono)}
|
||||||
|
.foot{position:absolute;left:140px;right:140px;bottom:56px;display:flex;justify-content:space-between;align-items:center;
|
||||||
|
font-family:var(--mono);font-size:18px;color:var(--fg3);letter-spacing:.08em;z-index:2}
|
||||||
|
.foot .brand{display:flex;align-items:center;gap:14px;color:var(--fg2)}
|
||||||
|
.cards{display:grid;gap:32px;margin-top:64px}
|
||||||
|
.c3{grid-template-columns:repeat(3,1fr)} .c4{grid-template-columns:repeat(4,1fr)} .c2{grid-template-columns:repeat(2,1fr)}
|
||||||
|
.card{background:linear-gradient(180deg,var(--bg2),var(--bg1));border:1px solid var(--line2);border-radius:28px;padding:44px}
|
||||||
|
.card p{font-size:26px}
|
||||||
|
.card .num{font-family:var(--mono);color:var(--acid);font-size:22px;margin-bottom:22px;letter-spacing:.1em}
|
||||||
|
.card.hi{border-color:rgba(198,255,77,.45);box-shadow:0 0 0 1px rgba(198,255,77,.15),0 30px 80px -30px rgba(198,255,77,.25)}
|
||||||
|
.badge{display:inline-flex;align-items:center;gap:12px;border:1px solid var(--line2);border-radius:999px;padding:14px 26px;
|
||||||
|
font-family:var(--mono);font-size:22px;color:var(--fg2);background:rgba(255,255,255,.02)}
|
||||||
|
.badge b{color:var(--acid);font-weight:400}
|
||||||
|
.row{display:flex;gap:18px;flex-wrap:wrap}
|
||||||
|
/* ring mark */
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
@keyframes beat{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.18);opacity:.75}}
|
||||||
|
@keyframes ripple{0%{transform:scale(.6);opacity:.7}100%{transform:scale(2.4);opacity:0}}
|
||||||
|
.mark-spin{transform-origin:center;animation:spin 18s linear infinite}
|
||||||
|
.mark-beat{transform-origin:center;transform-box:fill-box;animation:beat 1.6s ease-in-out infinite}
|
||||||
|
/* hero */
|
||||||
|
.hero-ring{position:absolute;right:30px;top:50%;width:640px;height:640px;transform:translateY(-50%)}
|
||||||
|
.hero-ring .rip{position:absolute;inset:0;border:2px solid rgba(198,255,77,.35);border-radius:50%;animation:ripple 3.2s ease-out infinite}
|
||||||
|
.hero-ring .rip:nth-child(2){animation-delay:1.06s} .hero-ring .rip:nth-child(3){animation-delay:2.13s}
|
||||||
|
/* stats */
|
||||||
|
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:28px;margin-top:60px}
|
||||||
|
.stat{border-top:2px solid var(--line2);padding-top:28px}
|
||||||
|
.stat .v{font-family:var(--display);font-weight:700;font-size:96px;letter-spacing:-.04em;line-height:1}
|
||||||
|
.stat .l{font-size:24px;color:var(--fg2);margin-top:14px;line-height:1.35}
|
||||||
|
.stat.hi{border-top-color:var(--acid)} .stat.hi .v{color:var(--acid)}
|
||||||
|
/* labs */
|
||||||
|
.labs{display:grid;grid-template-columns:repeat(5,1fr);gap:20px;margin-top:56px}
|
||||||
|
.lab{border:1px solid var(--line2);border-radius:20px;padding:24px 26px;background:var(--bg1)}
|
||||||
|
.lab .t{font-family:var(--mono);font-size:17px;color:var(--fg3);letter-spacing:.1em;text-transform:uppercase}
|
||||||
|
.lab .n{font-family:var(--display);font-size:34px;font-weight:600;margin-top:8px}
|
||||||
|
.lab.core{border-color:rgba(198,255,77,.5);background:linear-gradient(180deg,rgba(198,255,77,.10),var(--bg1))}
|
||||||
|
.lab.core .n{color:var(--acid)}
|
||||||
|
/* table */
|
||||||
|
table{border-collapse:collapse;width:100%;margin-top:48px;font-size:28px}
|
||||||
|
td,th{padding:22px 24px;border-bottom:1px solid var(--line2);text-align:left;vertical-align:top}
|
||||||
|
th{font-family:var(--mono);font-size:20px;color:var(--fg3);letter-spacing:.1em;text-transform:uppercase;font-weight:400}
|
||||||
|
td b{font-family:var(--display);font-size:34px}
|
||||||
|
/* roadmap */
|
||||||
|
.road{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin-top:80px;position:relative}
|
||||||
|
.road::before{content:"";position:absolute;left:0;right:0;top:15px;height:2px;background:linear-gradient(90deg,var(--acid),var(--cyan) 60%,var(--line2))}
|
||||||
|
.step{padding-right:40px}
|
||||||
|
.step .dot{width:32px;height:32px;border-radius:50%;background:var(--bg);border:3px solid var(--acid);margin-bottom:34px;box-shadow:0 0 22px rgba(198,255,77,.5)}
|
||||||
|
.step:nth-child(n+3) .dot{border-color:var(--cyan);box-shadow:0 0 22px rgba(92,225,230,.4)}
|
||||||
|
.step .q{font-family:var(--mono);font-size:22px;color:var(--acid);letter-spacing:.08em}
|
||||||
|
.step h3{margin-top:12px}
|
||||||
|
.step p{font-size:25px}
|
||||||
|
/* team */
|
||||||
|
.person{display:flex;gap:28px;align-items:flex-start}
|
||||||
|
.ava{flex:none;width:96px;height:96px;border-radius:50%;display:flex;align-items:center;justify-content:center;
|
||||||
|
font-family:var(--display);font-size:38px;font-weight:700;color:var(--bg);background:var(--acid)}
|
||||||
|
.ava.c{background:var(--cyan)} .ava.g{background:var(--fg2)}
|
||||||
|
.term{font-family:var(--mono);font-size:24px;background:#020203;border:1px solid var(--line2);border-radius:18px;padding:26px 30px;color:var(--fg2);line-height:1.6}
|
||||||
|
.term .ok{color:var(--acid)}
|
||||||
|
.big-quote{font-family:var(--display);font-size:60px;line-height:1.12;letter-spacing:-.02em;max-width:1180px}
|
||||||
|
#bar{position:fixed;left:0;bottom:0;height:4px;background:var(--acid);transition:width .4s ease;z-index:10;box-shadow:0 0 12px var(--acid)}
|
||||||
|
#hint{position:fixed;right:18px;bottom:14px;font:12px var(--mono);color:#444;z-index:10}
|
||||||
|
@media print{
|
||||||
|
@page{size:1920px 1080px;margin:0}
|
||||||
|
html,body{overflow:visible;height:auto;background:var(--bg)}
|
||||||
|
#stage{position:static;display:block} .deck{transform:none!important;width:1920px;height:auto}
|
||||||
|
.slide{position:relative;opacity:1;page-break-after:always;break-after:page;height:1080px}
|
||||||
|
#bar,#hint{display:none}
|
||||||
|
.hero-ring .rip{animation:none;opacity:.25}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="stage"><div class="deck" id="deck">
|
||||||
|
|
||||||
|
<!-- 1 · TITLE -->
|
||||||
|
<section class="slide grid-bg on">
|
||||||
|
<div class="kicker">Shkor Labs · R&D for autonomous business</div>
|
||||||
|
<h1>Autopilot<br>for any <span class="acid">business.</span></h1>
|
||||||
|
<p class="lead" style="max-width:860px">We mirror a company into a living twin, rehearse decisions on it, and hand the routine to an AI agent that runs on a heartbeat — every second, minute, hour and day.</p>
|
||||||
|
<div class="row" style="margin-top:56px;max-width:1060px">
|
||||||
|
<span class="badge"><b>Pre-seed</b> $500K–1M</span>
|
||||||
|
<span class="badge">post-money SAFE · <b>$10M cap</b> + MFN</span>
|
||||||
|
<span class="badge">Shkor Labs LLC · Wyoming</span>
|
||||||
|
</div>
|
||||||
|
<div class="hero-ring">
|
||||||
|
<div class="rip"></div><div class="rip"></div><div class="rip"></div>
|
||||||
|
<svg viewBox="0 0 200 200" width="640" height="640">
|
||||||
|
<g class="mark-spin"><circle cx="100" cy="100" r="78" fill="none" stroke="#c6ff4d" stroke-width="7" stroke-dasharray="20 27" stroke-linecap="round"/></g>
|
||||||
|
<circle class="mark-beat" cx="100" cy="100" r="30" fill="#c6ff4d"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>shkor.com · Confidential · October 2026</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2 · PROBLEM -->
|
||||||
|
<section class="slide">
|
||||||
|
<div class="kicker">01 · The problem</div>
|
||||||
|
<h2>Every business runs on routine nobody has time for.</h2>
|
||||||
|
<div class="cards c3">
|
||||||
|
<div class="card"><div class="num">— HANDS</div><h3>Owners are the bottleneck</h3><p>Inbox, follow-ups, reports, scheduling, small fixes. A 5–50 person company can't hire for all of it, so most of it just doesn't happen.</p></div>
|
||||||
|
<div class="card"><div class="num">— COPILOTS</div><h3>AI tools still need a driver</h3><p>Chat windows and copilots wait for a human to prompt every step. They assist. They don't run anything.</p></div>
|
||||||
|
<div class="card"><div class="num">— TRUST</div><h3>Agents aren't trusted with keys</h3><p>Autonomous agents loop, hallucinate and act outside their remit. No audit trail, no limits, no memory of why — so nobody hands them real work.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>02</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3 · INSIGHT / METHOD -->
|
||||||
|
<section class="slide grid-bg">
|
||||||
|
<div class="kicker">02 · The method</div>
|
||||||
|
<h2>A company is a stream of events.<br><span class="acid">So it can be replayed — and run.</span></h2>
|
||||||
|
<div class="cards c3">
|
||||||
|
<div class="card"><div class="num">01 · MIRROR</div><h3>Capture the twin</h3><p>Messages, tasks, calls, payments and decisions become signed events in one append-only log. Replay it and you get the company as a living model.</p></div>
|
||||||
|
<div class="card"><div class="num">02 · REHEARSE</div><h3>Decide on the twin first</h3><p>Changes are tried against the twin and its real history. Where the agent isn't sure, it escalates to a person with the full story.</p></div>
|
||||||
|
<div class="card hi"><div class="num">03 · AUTOPILOT</div><h3>Hand over the routine</h3><p>An agent that lives by ticks takes the routine, with real tools. Every action feeds back into the twin, so it gets sharper over time.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>03</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4 · HEARTBEAT -->
|
||||||
|
<section class="slide">
|
||||||
|
<div class="kicker">03 · The product · Simbiot autopilot</div>
|
||||||
|
<h2>Not a chatbot.<br><span class="acid">A heartbeat.</span></h2>
|
||||||
|
<div style="display:grid;grid-template-columns:780px 1fr;gap:80px;margin-top:40px;align-items:center">
|
||||||
|
<svg viewBox="0 0 780 640" width="700" height="575">
|
||||||
|
<defs><radialGradient id="g1"><stop offset="0" stop-color="#c6ff4d" stop-opacity=".35"/><stop offset="1" stop-color="#c6ff4d" stop-opacity="0"/></radialGradient></defs>
|
||||||
|
<circle cx="390" cy="320" r="300" fill="none" stroke="rgba(92,225,230,.35)" stroke-width="2"/>
|
||||||
|
<circle cx="390" cy="320" r="225" fill="none" stroke="rgba(92,225,230,.5)" stroke-width="2" stroke-dasharray="6 10"/>
|
||||||
|
<circle cx="390" cy="320" r="150" fill="none" stroke="rgba(198,255,77,.6)" stroke-width="2" stroke-dasharray="14 12"/>
|
||||||
|
<circle cx="390" cy="320" r="80" fill="url(#g1)"/>
|
||||||
|
<g class="mark-spin" style="transform-origin:390px 320px"><circle cx="390" cy="320" r="78" fill="none" stroke="#c6ff4d" stroke-width="6" stroke-dasharray="18 22" stroke-linecap="round"/></g>
|
||||||
|
<circle class="mark-beat" cx="390" cy="320" r="26" fill="#c6ff4d"/>
|
||||||
|
<g font-family="JetBrains Mono" font-size="22" fill="#b4b8c2">
|
||||||
|
<text x="390" y="200" text-anchor="middle" fill="#c6ff4d">1 s · reflex</text>
|
||||||
|
<text x="390" y="125" text-anchor="middle" fill="#c6ff4d">1 min · triage</text>
|
||||||
|
<text x="390" y="50" text-anchor="middle" fill="#5ce1e6">1 h · execute</text>
|
||||||
|
<text x="390" y="615" text-anchor="middle" fill="#5ce1e6">1 day · plan</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<table style="margin-top:0">
|
||||||
|
<tr><td class="mono acid" style="width:120px">1 s</td><td><b>Reflexes</b><br><span class="p" style="font-size:24px">telemetry, alerts, bot commands — no LLM, zero cost</span></td></tr>
|
||||||
|
<tr><td class="mono acid">1 min</td><td><b>Triage</b><br><span class="p" style="font-size:24px">inbox, routing, quick fixes</span></td></tr>
|
||||||
|
<tr><td class="mono cyan">1 h</td><td><b>Execution</b><br><span class="p" style="font-size:24px">tasks end-to-end with real tools + hourly digest</span></td></tr>
|
||||||
|
<tr><td class="mono cyan">1 day</td><td><b>Planning</b><br><span class="p" style="font-size:24px">larger work, daily manifest, nightly self-training</span></td></tr>
|
||||||
|
</table>
|
||||||
|
<p style="margin-top:28px;font-size:24px">Busy levels skip a beat instead of piling up. Only digests travel upward.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>04</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5 · TRUST / MOAT -->
|
||||||
|
<section class="slide grid-bg">
|
||||||
|
<div class="kicker">04 · Why it can be trusted with real work</div>
|
||||||
|
<h2>The moat is governance,<br>not the model.</h2>
|
||||||
|
<div class="cards c4" style="margin-top:56px">
|
||||||
|
<div class="card"><div class="num">SIGNED</div><h3>Every step on the record</h3><p>ed25519-signed, hash-chained event log. 3.1M events verified end-to-end.</p>
|
||||||
|
<div class="term" style="margin-top:22px;font-size:20px;padding:18px 20px">$ simbiot verify<br><span class="ok">✓ chain intact</span></div></div>
|
||||||
|
<div class="card"><div class="num">BOUNDED</div><h3>Trust circles</h3><p>Rights come from verified identity, never from message text. A guard checks every tool call before it runs.</p></div>
|
||||||
|
<div class="card"><div class="num">FRUGAL</div><h3>LLM only where it matters</h3><p>Rules and fast decision models filter before an LLM wakes. Reflexes and quiet beats cost nothing.</p></div>
|
||||||
|
<div class="card hi"><div class="num">LEARNING</div><h3>Improves on outcomes</h3><p>Small student models train nightly on real outcomes — and only replace the rules after beating a baseline on fresh data.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>05</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 6 · CUBE -->
|
||||||
|
<section class="slide">
|
||||||
|
<div class="kicker">05 · One policy, not 64 programs</div>
|
||||||
|
<h2>Who · how often · how deep.<br><span class="acid">4 × 4 × 4.</span></h2>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:32px;margin-top:64px">
|
||||||
|
<div class="card"><div class="num">4 TRUST CIRCLES</div>
|
||||||
|
<p style="font-size:27px;line-height:1.9"><span class="acid">1</span> Owner — full tools<br><span class="acid">2</span> Board — work, no secrets<br><span class="acid">3</span> Members & agents — scoped<br><span class="acid">4</span> Everyone else — reply only</p></div>
|
||||||
|
<div class="card"><div class="num">4 RHYTHMS</div>
|
||||||
|
<p style="font-size:27px;line-height:1.9"><span class="cyan">1 s</span> reflex<br><span class="cyan">1 min</span> triage<br><span class="cyan">1 h</span> execution<br><span class="cyan">1 day</span> planning</p></div>
|
||||||
|
<div class="card"><div class="num">4 MEMORY DEPTHS</div>
|
||||||
|
<p style="font-size:27px;line-height:1.9"><span class="acid">D0</span> just this message<br><span class="acid">D1</span> the conversation<br><span class="acid">D2</span> relevant past episodes<br><span class="acid">D3</span> the archive</p></div>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top:44px;max-width:1500px">Memory is private by circle: a board-level conversation can never surface the owner's private chats. The same code serves a two-person studio and a 500-person company — only the table changes.</p>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>06</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 7 · LABS -->
|
||||||
|
<section class="slide grid-bg">
|
||||||
|
<div class="kicker">06 · The labs</div>
|
||||||
|
<h2>We don't sell what we don't run.</h2>
|
||||||
|
<p class="lead" style="margin-top:24px;font-size:32px">Every lab is a real business — a founder plus AI agents on shared infrastructure inside Cooperative 42. Each one is a live test bench for the autopilot.</p>
|
||||||
|
<div class="labs">
|
||||||
|
<div class="lab core"><div class="t">Core · autopilot</div><div class="n">Simbiot</div></div>
|
||||||
|
<div class="lab"><div class="t">Media</div><div class="n">ReelsUp</div></div>
|
||||||
|
<div class="lab"><div class="t">Marketing</div><div class="n">Avatar</div></div>
|
||||||
|
<div class="lab"><div class="t">AI consulting</div><div class="n">Mainbeam</div></div>
|
||||||
|
<div class="lab"><div class="t">Construction</div><div class="n">MalyarKa</div></div>
|
||||||
|
<div class="lab"><div class="t">Fintech</div><div class="n">Vybirai</div></div>
|
||||||
|
<div class="lab"><div class="t">Fintech</div><div class="n">Monetix</div></div>
|
||||||
|
<div class="lab"><div class="t">Trust infra</div><div class="n">GroupSign</div></div>
|
||||||
|
<div class="lab"><div class="t">Trust infra</div><div class="n">WalletChain</div></div>
|
||||||
|
<div class="lab"><div class="t">Sales</div><div class="n">xSaleBot</div></div>
|
||||||
|
<div class="lab"><div class="t">Marketplace</div><div class="n">DEIP</div></div>
|
||||||
|
<div class="lab"><div class="t">Sports media</div><div class="n">LAIve Sports</div></div>
|
||||||
|
<div class="lab"><div class="t">Real estate</div><div class="n">Domostroy</div></div>
|
||||||
|
<div class="lab"><div class="t">Compliance</div><div class="n">Probity</div></div>
|
||||||
|
<div class="lab"><div class="t">+ more</div><div class="n">Totem · Yorio · PipL</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>07</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 8 · TRACTION -->
|
||||||
|
<section class="slide">
|
||||||
|
<div class="kicker">07 · Traction · live, queryable, not projected</div>
|
||||||
|
<h2>A real system, <span class="acid">running every second.</span></h2>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat hi"><div class="v">19</div><div class="l">labs in the registry since July 2026</div></div>
|
||||||
|
<div class="stat hi"><div class="v">16</div><div class="l">AI agents running 24/7 on dedicated servers</div></div>
|
||||||
|
<div class="stat"><div class="v">1,742</div><div class="l">messages answered by the autopilot in its first 19 days</div></div>
|
||||||
|
<div class="stat"><div class="v">374</div><div class="l">tasks shipped end-to-end by the autopilot</div></div>
|
||||||
|
<div class="stat"><div class="v">3.1M</div><div class="l">signed, hash-chained events in the audit log</div></div>
|
||||||
|
<div class="stat"><div class="v">2,075</div><div class="l">items in the agent's searchable, circle-private memory</div></div>
|
||||||
|
<div class="stat"><div class="v">10</div><div class="l">founders & members in Cooperative 42</div></div>
|
||||||
|
<div class="stat hi"><div class="v">LIVE</div><div class="l">billing: Stripe (US LLC) + card acquiring (Belarus)</div></div>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top:44px;font-size:22px" class="dim mono">Source: production event store and cooperative portal, 18 Oct 2026. Every number above is a query, not an estimate.</p>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>08</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 9 · SELF-IMPROVING -->
|
||||||
|
<section class="slide grid-bg">
|
||||||
|
<div class="kicker">08 · It improves itself — under supervision</div>
|
||||||
|
<h2>Mistakes become rules.<br><span class="acid">Rules become instinct.</span></h2>
|
||||||
|
<div class="cards c4" style="margin-top:44px">
|
||||||
|
<div class="card"><div class="num">1 · OBSERVE</div><h3>Outcomes, not opinions</h3><p>Every decision is logged with what happened next — a reaction, a retry, a fix.</p></div>
|
||||||
|
<div class="card"><div class="num">2 · LEARN</div><h3>Nightly students</h3><p>Small models train on that experience and stay in shadow until they beat a baseline.</p></div>
|
||||||
|
<div class="card"><div class="num">3 · REFLECT</div><h3>Rules for humans</h3><p>Repeated failures turn into a proposed rule, sent to the owner for a yes.</p></div>
|
||||||
|
<div class="card hi"><div class="num">4 · GATE</div><h3>Ship with proof</h3><p>Code changes close only with a commit and a passing test. No silent self-edits.</p></div>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top:44px;max-width:1550px">Example from this week: the agent improved its own Telegram behaviour, then tried to close the task without a test. The new gate now refuses that — the change shipped with a commit and a passing test.</p>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>09</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 10 · BUSINESS MODEL -->
|
||||||
|
<section class="slide">
|
||||||
|
<div class="kicker">09 · Business model</div>
|
||||||
|
<h2>Two doors. <span class="acid">One flywheel.</span></h2>
|
||||||
|
<div class="cards c2" style="margin-top:56px">
|
||||||
|
<div class="card hi"><div class="num">FOR COMPANIES</div><h3 style="font-size:46px">Autopilot subscription</h3>
|
||||||
|
<div style="font-family:var(--display);font-size:120px;font-weight:700;letter-spacing:-.04em;line-height:1;margin:18px 0">$100<span style="font-size:40px;color:var(--fg3)"> /month</span></div>
|
||||||
|
<p>The same system our labs run in production. Deployment in the month you join; LLM access through the cooperative's router included. One price for everyone — 10 presale seats.</p></div>
|
||||||
|
<div class="card"><div class="num">FOR FOUNDERS</div><h3 style="font-size:46px">Start a lab</h3>
|
||||||
|
<div style="font-family:var(--display);font-size:120px;font-weight:700;letter-spacing:-.04em;line-height:1;margin:18px 0" class="cyan">rev share</div>
|
||||||
|
<p>The autopilot, shared infrastructure, a site, US payments and a founder community. The lab shares a part of its revenue with the cooperative — withheld automatically at settlement.</p></div>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top:40px" class="mono">labs → real experience → sharper autopilot → more customers → more labs</p>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>10</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 11 · WHY NOW -->
|
||||||
|
<section class="slide grid-bg">
|
||||||
|
<div class="kicker">10 · Why now</div>
|
||||||
|
<h2>The cost of doing collapsed.<br><span class="acid">The cost of trusting didn't.</span></h2>
|
||||||
|
<div class="cards c3">
|
||||||
|
<div class="card"><div class="num">AGENTS WORK</div><h3>Capability crossed the line</h3><p>Agents with real tools now finish multi-step work end-to-end. Orchestration, not intelligence, is the bottleneck.</p></div>
|
||||||
|
<div class="card"><div class="num">COST FELL</div><h3>Judgment got cheap</h3><p>Sub-second decision models and local embeddings let the autopilot think constantly and call a large model only when it matters.</p></div>
|
||||||
|
<div class="card hi"><div class="num">TRUST IS THE GAP</div><h3>Nobody owns the harness</h3><p>Every company wants autonomy; almost none can audit it. A signed, bounded, self-correcting autopilot is the missing layer.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>11</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 12 · ROADMAP -->
|
||||||
|
<section class="slide">
|
||||||
|
<div class="kicker">11 · Plan · next 12 months</div>
|
||||||
|
<h2>From our labs<br>to <span class="acid">everyone's business.</span></h2>
|
||||||
|
<div class="road">
|
||||||
|
<div class="step"><div class="dot"></div><div class="q">Q4 2026</div><h3>Presale cohort</h3><p>10 companies on the autopilot. Hardened onboarding, weekly autopilot reports.</p></div>
|
||||||
|
<div class="step"><div class="dot"></div><div class="q">Q1 2027</div><h3>Self-serve</h3><p>Deploy in a day from the portal. Students graduate from shadow to on.</p></div>
|
||||||
|
<div class="step"><div class="dot"></div><div class="q">Q2 2027</div><h3>Longer horizons</h3><p>Week and month rhythms: the autopilot plans quarters, not just days.</p></div>
|
||||||
|
<div class="step"><div class="dot"></div><div class="q">Q3 2027</div><h3>Scale the labs</h3><p>New labs every month on shared infra; the autopilot as the default operator.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>12</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 13 · TEAM -->
|
||||||
|
<section class="slide grid-bg">
|
||||||
|
<div class="kicker">12 · Team</div>
|
||||||
|
<h2>Half human, <span class="acid">half agents.</span></h2>
|
||||||
|
<div class="cards c2" style="margin-top:56px;gap:36px">
|
||||||
|
<div class="card person"><div class="ava">AS</div><div><h3>Alex Shkor</h3><p>Founder & managing member. Architect of the Simbiot autopilot; chairman of Cooperative 42. US entity, payments and production infrastructure.</p></div></div>
|
||||||
|
<div class="card person"><div class="ava c">Y</div><div><h3>Yura</h3><p>Board. 14 years building products and businesses; finance, tax, legal and M&A. Runs Vybirai and Monetix.</p></div></div>
|
||||||
|
<div class="card person"><div class="ava c">V</div><div><h3>Vlad</h3><p>Board. End-to-end video production with his own studio; runs ReelsUp, Avatar and xSaleBot.</p></div></div>
|
||||||
|
<div class="card person"><div class="ava g">16</div><div><h3>The fleet</h3><p>Sixteen agents in production — sales, content, research, finance, operations — each with its own server, memory and signed track record.</p></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>13</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 14 · THE ASK -->
|
||||||
|
<section class="slide">
|
||||||
|
<div class="kicker">13 · The ask</div>
|
||||||
|
<div style="display:grid;grid-template-columns:1.05fr 1fr;gap:90px;align-items:start">
|
||||||
|
<div>
|
||||||
|
<div style="font-family:var(--display);font-weight:700;font-size:200px;letter-spacing:-.05em;line-height:.9" class="acid">$1M</div>
|
||||||
|
<p style="font-size:34px;margin-top:20px">Angel pre-seed · target $500K–1M · 5–8 checks · rolling close</p>
|
||||||
|
<div class="term" style="margin-top:40px">
|
||||||
|
instrument post-money SAFE<br>
|
||||||
|
cap <span class="ok">$10M</span><br>
|
||||||
|
terms MFN side letter<br>
|
||||||
|
issuer Shkor Labs LLC (Wyoming)<br>
|
||||||
|
offering Reg D 506(b), warm network only
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 style="font-size:44px;margin-bottom:30px">Use of funds</h3>
|
||||||
|
<div class="card" style="padding:34px;margin-bottom:22px"><div class="num">AUTOPILOT</div><p>Productize Simbiot for outside companies: self-serve deploy, onboarding, reliability.</p></div>
|
||||||
|
<div class="card" style="padding:34px;margin-bottom:22px"><div class="num">LABS</div><p>More founders through the lab pipeline — each one a new test bench and revenue line.</p></div>
|
||||||
|
<div class="card" style="padding:34px"><div class="num">GO-TO-MARKET</div><p>Fill the presale cohort and turn lab results into a repeatable sales motion.</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>14</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 15 · CLOSE -->
|
||||||
|
<section class="slide grid-bg">
|
||||||
|
<div style="flex:1;display:flex;flex-direction:column;justify-content:center">
|
||||||
|
<div class="kicker">Shkor Labs</div>
|
||||||
|
<div class="big-quote">Every company will run on an autopilot.<br><span class="acid">We're building the one you can trust — by running our own businesses on it first.</span></div>
|
||||||
|
<div class="row" style="margin-top:64px">
|
||||||
|
<span class="badge"><b>hello@shkor.com</b></span><span class="badge">shkor.com</span><span class="badge">Cooperative 42 · cil.by</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-ring" style="width:440px;height:440px;right:140px">
|
||||||
|
<div class="rip"></div><div class="rip"></div><div class="rip"></div>
|
||||||
|
<svg viewBox="0 0 200 200" width="440" height="440"><g class="mark-spin"><circle cx="100" cy="100" r="78" fill="none" stroke="#c6ff4d" stroke-width="7" stroke-dasharray="20 27" stroke-linecap="round"/></g><circle class="mark-beat" cx="100" cy="100" r="30" fill="#c6ff4d"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="foot"><span class="brand">__LOGO_SMALL__ Shkor Labs</span><span>← → navigate · F fullscreen</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div></div>
|
||||||
|
<div id="bar"></div><div id="hint">← → · F</div>
|
||||||
|
<script>
|
||||||
|
(()=>{const d=document.getElementById('deck'),s=[...d.querySelectorAll('.slide')],bar=document.getElementById('bar');
|
||||||
|
let i=Math.max(0,Math.min(s.length-1,(parseInt(location.hash.slice(1))||1)-1));
|
||||||
|
const fit=()=>{const k=Math.min(innerWidth/1920,innerHeight/1080);d.style.transform=`scale(${k})`};
|
||||||
|
const go=n=>{i=Math.max(0,Math.min(s.length-1,n));s.forEach((x,j)=>x.classList.toggle('on',j===i));bar.style.width=((i+1)/s.length*100)+'%';history.replaceState(null,'','#'+(i+1))};
|
||||||
|
addEventListener('resize',fit);fit();go(i);
|
||||||
|
addEventListener('keydown',e=>{if(['ArrowRight','PageDown',' ','Enter'].includes(e.key)){e.preventDefault();go(i+1)}
|
||||||
|
else if(['ArrowLeft','PageUp','Backspace'].includes(e.key)){e.preventDefault();go(i-1)}
|
||||||
|
else if(e.key==='Home')go(0);else if(e.key==='End')go(s.length-1);
|
||||||
|
else if(e.key==='f'||e.key==='F'){document.fullscreenElement?document.exitFullscreen():document.documentElement.requestFullscreen()}});
|
||||||
|
let x0=null;addEventListener('touchstart',e=>x0=e.touches[0].clientX);addEventListener('touchend',e=>{if(x0===null)return;const dx=e.changedTouches[0].clientX-x0;if(Math.abs(dx)>40)go(i+(dx<0?1:-1));x0=null});
|
||||||
|
addEventListener('click',e=>{if(e.target.closest('a'))return;go(i+(e.clientX>innerWidth/2?1:-1))});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
389
decks/shkor-labs-fundraise/index.html
Normal file
389
decks/shkor-labs-fundraise/index.html
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -97,6 +97,8 @@ LEVELS = {
|
||||||
"автоматом), list_calls/get_call — прошедшие звонки и саммари, "
|
"автоматом), list_calls/get_call — прошедшие звонки и саммари, "
|
||||||
"list_members/member_create(email[,name]) — реестр членов и "
|
"list_members/member_create(email[,name]) — реестр членов и "
|
||||||
"создание аккаунта с magic-link (только агентам председателя), "
|
"создание аккаунта с magic-link (только агентам председателя), "
|
||||||
|
"member_remove(member_id,reason) — удаление с бэкапом и аудитом "
|
||||||
|
"(только агентам председателя; Председателя удалить нельзя), "
|
||||||
"projects_list/list_agents/treasury_status/list_events — "
|
"projects_list/list_agents/treasury_status/list_events — "
|
||||||
"реестры, казна, лента. Когда Alex просит назначить/перенести звонок "
|
"реестры, казна, лента. Когда Alex просит назначить/перенести звонок "
|
||||||
"или «посмотреть расписание» — это portal_mcp (до 4 вызовов за тик): "
|
"или «посмотреть расписание» — это portal_mcp (до 4 вызовов за тик): "
|
||||||
|
|
@ -151,7 +153,9 @@ LEVELS = {
|
||||||
"booking_slots/booking_create (звонки с Алексом: слот → создание "
|
"booking_slots/booking_create (звонки с Алексом: слот → создание "
|
||||||
"jitsi-звонка со ссылкой, гостю письмо), list_calls/get_call, "
|
"jitsi-звонка со ссылкой, гостю письмо), list_calls/get_call, "
|
||||||
"list_members/member_create (создание аккаунта члена с magic-link, "
|
"list_members/member_create (создание аккаунта члена с magic-link, "
|
||||||
"только агентам председателя), projects_list/list_agents, "
|
"только агентам председателя)/member_remove(member_id,reason) — "
|
||||||
|
"удаление члена с бэкапом и аудитом (только агентам председателя; "
|
||||||
|
"Председателя удалить нельзя), projects_list/list_agents, "
|
||||||
"treasury_status, list_events. "
|
"treasury_status, list_events. "
|
||||||
"Просьбы про назначение/перенос звонков и «что по расписанию» — "
|
"Просьбы про назначение/перенос звонков и «что по расписанию» — "
|
||||||
"через него (до 4 вызовов за тик). НЕ создавай звонки без явной "
|
"через него (до 4 вызовов за тик). НЕ создавай звонки без явной "
|
||||||
|
|
|
||||||
|
|
@ -244,8 +244,9 @@ def describe_tools(max_names: int = 12) -> str:
|
||||||
"kind='call'|'session') — создать jitsi-звонок со ссылкой (гостю "
|
"kind='call'|'session') — создать jitsi-звонок со ссылкой (гостю "
|
||||||
"письмо, Алексу — в TG, запись+транскрипция автоматом); booking_cancel"
|
"письмо, Алексу — в TG, запись+транскрипция автоматом); booking_cancel"
|
||||||
"(meeting_id); list_calls/get_call — прошедшие звонки и их саммари; "
|
"(meeting_id); list_calls/get_call — прошедшие звонки и их саммари; "
|
||||||
"list_members/member_create(email[,name]) — реестр членов и создание "
|
"list_members/member_create(email[,name])/member_remove(member_id,"
|
||||||
"аккаунта с magic-link письмом (member_create — только агентам "
|
"reason) — реестр членов, создание аккаунта с magic-link и "
|
||||||
"председателя); projects_list/list_agents/treasury_status/list_events — "
|
"безопасное удаление с бэкапом и аудитом (create/remove — только "
|
||||||
|
"агентам председателя); projects_list/list_agents/treasury_status/list_events — "
|
||||||
"реестры и казна; whoami/report_task/report_event/read_context — "
|
"реестры и казна; whoami/report_task/report_event/read_context — "
|
||||||
"идентичность и лента портала.")
|
"идентичность и лента портала.")
|
||||||
|
|
|
||||||
139
tests/test_member_remove.py
Normal file
139
tests/test_member_remove.py
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""t-261018141319: member_remove — безопасное удаление члена (ядро domains/member_ops.py).
|
||||||
|
|
||||||
|
Запуск: ./venv/bin/python tests/test_member_remove.py
|
||||||
|
|
||||||
|
Сеть/портал НЕ поднимаются: ядро flask-free (sqlite3 + stdlib), тестируется
|
||||||
|
на временной базе. Проверяет:
|
||||||
|
- happy-path: users/login_tokens/project_members зачищены, снимок в
|
||||||
|
member_archive + json-файл бэкапа (снимок содержит user/shares/votes);
|
||||||
|
- паи (shares) и голоса (member_votes) НЕ удаляются — финансовая история;
|
||||||
|
- guardrail: Председателя удалить нельзя (chairman_protected);
|
||||||
|
- неизвестный id → not_found, без исключения;
|
||||||
|
- колбэк on_event (MemberRemoved) вызывается с reason/actor;
|
||||||
|
- файловый бэкап недоступен (dead dir) — удаление всё равно ок (best effort).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
sys.path.insert(0, "/root/simbiot-harness")
|
||||||
|
sys.path.insert(0, "/root/coop")
|
||||||
|
|
||||||
|
from domains import member_ops # noqa: E402
|
||||||
|
|
||||||
|
fails = []
|
||||||
|
|
||||||
|
|
||||||
|
def check(name, cond, extra=""):
|
||||||
|
print(("PASS" if cond else "FAIL"), name, ("" if cond else " | " + str(extra)[:300]))
|
||||||
|
if not cond:
|
||||||
|
fails.append(name)
|
||||||
|
|
||||||
|
|
||||||
|
_tmp = tempfile.mkdtemp(prefix="simbiot_memberremove_")
|
||||||
|
DB = os.path.join(_tmp, "coop.db")
|
||||||
|
ARCH = os.path.join(_tmp, "archive")
|
||||||
|
|
||||||
|
db = sqlite3.connect(DB)
|
||||||
|
db.executescript("""
|
||||||
|
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL,
|
||||||
|
name TEXT NOT NULL, password TEXT NOT NULL, email TEXT DEFAULT '', role TEXT DEFAULT 'Член',
|
||||||
|
avatar TEXT DEFAULT '', created_at TEXT DEFAULT '', is_board INTEGER DEFAULT 0, bio TEXT DEFAULT '');
|
||||||
|
CREATE TABLE login_tokens (token TEXT PRIMARY KEY, email TEXT NOT NULL, username TEXT DEFAULT '',
|
||||||
|
created_at TEXT DEFAULT '', used INTEGER DEFAULT 0);
|
||||||
|
CREATE TABLE project_members (id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL,
|
||||||
|
member_type TEXT NOT NULL DEFAULT 'user', username TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'member');
|
||||||
|
CREATE TABLE shares (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL,
|
||||||
|
usd_amount REAL NOT NULL, note TEXT DEFAULT '');
|
||||||
|
CREATE TABLE member_votes (application_id INTEGER NOT NULL, username TEXT NOT NULL, vote TEXT NOT NULL);
|
||||||
|
INSERT INTO users (username, name, password, email, role) VALUES
|
||||||
|
('alex', 'Alex', 'x', 'alex@x.io', 'Председатель'),
|
||||||
|
('serg_test', 'Serg Test', 'x', 'serg.test@x.io', 'Член');
|
||||||
|
INSERT INTO login_tokens (token, email, username) VALUES ('t1', 'serg.test@x.io', 'serg_test');
|
||||||
|
INSERT INTO project_members (project_id, member_type, username) VALUES ('vybirai', 'user', 'serg_test');
|
||||||
|
INSERT INTO shares (username, usd_amount, note) VALUES ('serg_test', 500.0, 'паевой');
|
||||||
|
INSERT INTO member_votes (application_id, username, vote) VALUES (1, 'serg_test', 'yes');
|
||||||
|
""")
|
||||||
|
db.commit()
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def q(sql, *p):
|
||||||
|
db = sqlite3.connect(DB)
|
||||||
|
r = db.execute(sql, p).fetchall()
|
||||||
|
db.close()
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
events = []
|
||||||
|
|
||||||
|
# ── guardrail: Председателя не трогаем ───────────────────────────────────────
|
||||||
|
res = member_ops.remove_member(DB, 1, reason="тест", actor="t", archive_dir=ARCH)
|
||||||
|
check("chairman protected", res.get("error") == "chairman_protected", res)
|
||||||
|
check("chairman still in users", q("SELECT COUNT(*) FROM users WHERE username='alex'")[0][0] == 1)
|
||||||
|
|
||||||
|
# ── not_found ────────────────────────────────────────────────────────────────
|
||||||
|
res = member_ops.remove_member(DB, 999, reason="нет такого", actor="t", archive_dir=ARCH)
|
||||||
|
check("not_found without exception", res.get("error") == "not_found", res)
|
||||||
|
|
||||||
|
# ── happy-path: удаление члена с бэкапом и аудитом ───────────────────────────
|
||||||
|
def on_event(username, payload, actor):
|
||||||
|
events.append((username, payload, actor))
|
||||||
|
|
||||||
|
|
||||||
|
res = member_ops.remove_member(DB, 2, reason="вышел из Labs, m-516175137",
|
||||||
|
actor="mcp:simbiot", on_event=on_event,
|
||||||
|
archive_dir=ARCH)
|
||||||
|
check("ok removed", bool(res.get("ok")), res)
|
||||||
|
check("username in result", res.get("username") == "serg_test", res)
|
||||||
|
check("users row gone", q("SELECT COUNT(*) FROM users WHERE username='serg_test'")[0][0] == 0)
|
||||||
|
check("login_tokens cleaned", q("SELECT COUNT(*) FROM login_tokens WHERE username='serg_test' OR email='serg.test@x.io'")[0][0] == 0)
|
||||||
|
check("project_members cleaned", q("SELECT COUNT(*) FROM project_members WHERE username='serg_test'")[0][0] == 0)
|
||||||
|
check("shares kept (финансовая история)", q("SELECT COUNT(*) FROM shares WHERE username='serg_test'")[0][0] == 1)
|
||||||
|
check("votes kept", q("SELECT COUNT(*) FROM member_votes WHERE username='serg_test'")[0][0] == 1)
|
||||||
|
|
||||||
|
arch_rows = q("SELECT user_id, username, snapshot, reason, actor FROM member_archive")
|
||||||
|
check("member_archive row", len(arch_rows) == 1, arch_rows)
|
||||||
|
if arch_rows:
|
||||||
|
snap = json.loads(arch_rows[0][2])
|
||||||
|
check("snapshot user email", snap["user"]["email"] == "serg.test@x.io", snap.get("user"))
|
||||||
|
check("snapshot shares inside", len(snap.get("shares", [])) == 1, snap.get("shares"))
|
||||||
|
check("snapshot votes inside", len(snap.get("member_votes", [])) == 1, snap.get("member_votes"))
|
||||||
|
check("archive reason/actor", arch_rows[0][3] == "вышел из Labs, m-516175137" and arch_rows[0][4] == "mcp:simbiot")
|
||||||
|
|
||||||
|
check("json backup file written", isinstance(res.get("archive_file"), str)
|
||||||
|
and os.path.exists(res["archive_file"]), res.get("archive_file"))
|
||||||
|
if res.get("archive_file"):
|
||||||
|
disk = json.load(open(res["archive_file"], encoding="utf-8"))
|
||||||
|
check("json backup has user+shares", disk["user"]["username"] == "serg_test"
|
||||||
|
and len(disk.get("shares", [])) == 1)
|
||||||
|
|
||||||
|
check("on_event fired once with payload", len(events) == 1
|
||||||
|
and events[0][0] == "serg_test"
|
||||||
|
and events[0][1].get("reason") == "вышел из Labs, m-516175137"
|
||||||
|
and events[0][1].get("user_id") == 2, events)
|
||||||
|
|
||||||
|
# ── файловый бэкап недоступен — удаление всё равно проходит ─────────────────
|
||||||
|
db = sqlite3.connect(DB)
|
||||||
|
db.execute("INSERT INTO users (username, name, password, email, role) VALUES ('tmp2', 'T2', 'x', 't2@x.io', 'Член')")
|
||||||
|
db.commit()
|
||||||
|
# путь-ловушка: archive_dir указывает на обычный файл — makedirs упадёт с OSError
|
||||||
|
# (chmod для root не барьер, поэтому симулируем отказ так)
|
||||||
|
not_a_dir = os.path.join(_tmp, "blocked_file.json")
|
||||||
|
open(not_a_dir, "w").write("i am a file")
|
||||||
|
res2 = member_ops.remove_member(DB, q("SELECT id FROM users WHERE username='tmp2'")[0][0],
|
||||||
|
reason="x", actor="t", archive_dir=not_a_dir)
|
||||||
|
check("removed even if file backup fails", bool(res2.get("ok")) and res2.get("archive_file") is None, res2)
|
||||||
|
check("second archive row", q("SELECT COUNT(*) FROM member_archive")[0][0] == 2)
|
||||||
|
|
||||||
|
# ── идемпотентность повторного вызова: уже удалён → not_found ───────────────
|
||||||
|
res3 = member_ops.remove_member(DB, 2, reason="повтор", actor="t", archive_dir=ARCH)
|
||||||
|
check("re-remove → not_found", res3.get("error") == "not_found", res3)
|
||||||
|
|
||||||
|
shutil.rmtree(_tmp, ignore_errors=True)
|
||||||
|
print(("ALL PASS" if not fails else "FAILURES: " + ", ".join(fails)))
|
||||||
|
sys.exit(1 if fails else 0)
|
||||||
Loading…
Reference in a new issue