Seed the v5 thesis into the Architect Workshop (v0.1.0:53)
backend/thesis_seed.py builds the starting "living messaging source of truth" from docs/thesis-seed-v5.md: a core line (throughline; the open Option A/B banner as a competing variant group; the three pillars; the proof; voice rules), one line per LP segment carrying that segment's angle, and the five segment definitions. ensure_thesis_seed(conn) runs from init_db, seeding ONLY when the Workshop is empty (no thesis lines) — idempotent and non-destructive, so it bootstraps once and never overwrites partner edits. Everything lands draft/candidate; nothing is made canonical (that stays the partners' dual-approval action, guardrail #4). Content is Ten31's own messaging, not LP data. Test: backend/test_thesis_seed.py runs init_db and asserts the core line, 5 segment lines, the 2-member Option A/B variant group, 3 pillars, segment_cuts, and segment defs, plus re-seed-is-a-no-op (11/11). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -469,6 +469,13 @@ def init_db():
|
|||||||
except Exception as _e:
|
except Exception as _e:
|
||||||
print(f"[backfill] grid contact_id backfill warning: {_e}")
|
print(f"[backfill] grid contact_id backfill warning: {_e}")
|
||||||
|
|
||||||
|
# One-time: seed the v5 thesis into the Architect's Workshop if it is empty.
|
||||||
|
try:
|
||||||
|
from thesis_seed import ensure_thesis_seed as _ensure_thesis_seed
|
||||||
|
_ensure_thesis_seed(conn)
|
||||||
|
except Exception as _e:
|
||||||
|
print(f"[thesis] seed warning: {_e}")
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
print(f"Database initialized at {DB_PATH}")
|
print(f"Database initialized at {DB_PATH}")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Offline test for the v5 thesis seed (init_db auto-seed + idempotency).
|
||||||
|
|
||||||
|
Runs the real init_db against a throwaway DB (applies migration 0002 and the
|
||||||
|
auto-seed), then asserts the Workshop substrate: a core line, one line per segment,
|
||||||
|
the Option A/B banner as a 2-member variant group, the pillars/proof, and the
|
||||||
|
segments table — and that re-seeding is a no-op.
|
||||||
|
|
||||||
|
Run: cd backend && python3 test_thesis_seed.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
_tmp = tempfile.mkdtemp()
|
||||||
|
os.environ["CRM_DATA_DIR"] = _tmp
|
||||||
|
os.environ["CRM_DB_PATH"] = os.path.join(_tmp, "crm.db")
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
import server # noqa: E402
|
||||||
|
import thesis_seed # noqa: E402
|
||||||
|
|
||||||
|
FAILS = []
|
||||||
|
|
||||||
|
|
||||||
|
def check(cond, msg):
|
||||||
|
print((" PASS " if cond else " FAIL ") + msg)
|
||||||
|
if not cond:
|
||||||
|
FAILS.append(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
server.init_db()
|
||||||
|
conn = sqlite3.connect(server.DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
lines = {r["line_key"]: dict(r) for r in conn.execute("SELECT * FROM thesis_lines WHERE deleted_at IS NULL")}
|
||||||
|
check("core" in lines and lines["core"]["is_core"] == 1, "core thesis line seeded (is_core)")
|
||||||
|
seg_lines = [k for k in lines if k.startswith("seg_")]
|
||||||
|
check(len(seg_lines) == 5, f"five per-segment lines seeded (got {len(seg_lines)}: {sorted(seg_lines)})")
|
||||||
|
check(len(lines) == 6, f"exactly 6 lines total (got {len(lines)})")
|
||||||
|
|
||||||
|
core_id = lines["core"]["id"]
|
||||||
|
nodes = [dict(r) for r in conn.execute("SELECT * FROM thesis_nodes WHERE line_id=?", (core_id,))]
|
||||||
|
types = [n["node_type"] for n in nodes]
|
||||||
|
check("throughline" in types, "core has a throughline node")
|
||||||
|
check("proof_point" in types, "core has a proof_point node")
|
||||||
|
variants = [n for n in nodes if n["variant_group"] == "positioning"]
|
||||||
|
check(len(variants) == 2, f"Option A/B banner is a 2-member variant group (got {len(variants)})")
|
||||||
|
pillars = [n for n in nodes if n["node_type"] == "claim" and n["title"] and n["title"][0] in "123"]
|
||||||
|
check(len(pillars) == 3, f"three pillar claims (got {len(pillars)})")
|
||||||
|
|
||||||
|
segs = {r["segment_key"] for r in conn.execute("SELECT segment_key FROM segments WHERE status='active'")}
|
||||||
|
check(segs == {"btc_native_hnwi", "institution", "family_office", "smaller_accredited", "ai_energy_operator"},
|
||||||
|
f"five active segments (got {sorted(segs)})")
|
||||||
|
|
||||||
|
# segment lines each carry a segment_cut angle
|
||||||
|
cut = conn.execute("""SELECT COUNT(*) FROM thesis_nodes n JOIN thesis_lines l ON l.id=n.line_id
|
||||||
|
WHERE n.node_type='segment_cut' AND l.line_key LIKE 'seg_%'""").fetchone()[0]
|
||||||
|
check(cut == 5, f"each segment line has a segment_cut angle (got {cut})")
|
||||||
|
|
||||||
|
# idempotency: re-seeding does nothing (thesis not empty)
|
||||||
|
thesis_seed.ensure_thesis_seed(conn)
|
||||||
|
conn.commit()
|
||||||
|
n2 = conn.execute("SELECT COUNT(*) FROM thesis_lines WHERE deleted_at IS NULL").fetchone()[0]
|
||||||
|
check(n2 == 6, f"re-seed is a no-op when thesis already exists (got {n2})")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print()
|
||||||
|
if FAILS:
|
||||||
|
print(f"FAILED ({len(FAILS)})")
|
||||||
|
sys.exit(1)
|
||||||
|
print("ALL PASS (v5 thesis seed)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
"""Seed the Ten31 v5 thesis into the Architect's substrate (Phase 1).
|
||||||
|
|
||||||
|
Builds the starting "living messaging source of truth" the partners iterate on in
|
||||||
|
the Thesis Workshop: a CORE line (throughline, the open Option A/B banner debate as
|
||||||
|
competing variants, the three pillars, the proof, and the voice rules) plus one
|
||||||
|
LINE PER SEGMENT carrying that segment's angle, and the segment definitions.
|
||||||
|
|
||||||
|
Content is Ten31's OWN messaging (docs/thesis-seed-v5.md) — not LP data — so it is
|
||||||
|
safe to ship in code. Everything lands as DRAFT/CANDIDATE nodes: nothing is made
|
||||||
|
canonical here (that is the partners' dual-approval action — guardrail #4).
|
||||||
|
|
||||||
|
`ensure_thesis_seed(conn)` is idempotent: it seeds ONLY when no thesis lines exist,
|
||||||
|
so it bootstraps an empty Workshop once and never clobbers partner edits afterward.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
def _now():
|
||||||
|
return datetime.now(timezone.utc).replace(tzinfo=None).isoformat() + "Z"
|
||||||
|
|
||||||
|
|
||||||
|
def _eid(prefix):
|
||||||
|
return f"{prefix}_{uuid.uuid4().hex[:16]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _log(conn, action, target_id, payload):
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO interaction_log
|
||||||
|
(id, ts, actor_type, actor_id, action, target_type, target_id, payload, source, created_at)
|
||||||
|
VALUES (?,?, 'system', 'thesis_seed', ?, 'thesis', ?, ?, 'seed', ?)""",
|
||||||
|
(str(uuid.uuid4()), _now(), action, target_id, json.dumps(payload) if payload is not None else None, _now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _line(conn, line_key, name, segment_key=None, is_core=False, description=None):
|
||||||
|
lid = _eid("thl")
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO thesis_lines (id, line_key, name, segment_key, is_core, description, created_at, updated_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?)""",
|
||||||
|
(lid, line_key, name, segment_key, 1 if is_core else 0, description, _now(), _now()),
|
||||||
|
)
|
||||||
|
_log(conn, "thesis.line_seeded", lid, {"line_key": line_key, "is_core": bool(is_core)})
|
||||||
|
return lid
|
||||||
|
|
||||||
|
|
||||||
|
def _node(conn, line_id, parent_id, node_type, ordn, title, body, status="draft", variant_group=None):
|
||||||
|
nid = _eid("thn")
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO thesis_nodes (id, line_id, parent_id, node_type, ord, title, body, status, variant_group, created_at, updated_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
|
(nid, line_id, parent_id, node_type, float(ordn), title, body, status, variant_group, _now(), _now()),
|
||||||
|
)
|
||||||
|
return nid
|
||||||
|
|
||||||
|
|
||||||
|
# ── v5 content (docs/thesis-seed-v5.md) ──────────────────────────────────────
|
||||||
|
|
||||||
|
THROUGHLINE = (
|
||||||
|
"Bitcoin, AI, and energy are three of the largest growth markets of the next decade, "
|
||||||
|
"and they depend on the same scarce resources: cheap energy and computing power. We "
|
||||||
|
"believe that energy, compute, and AI infrastructure will settle on money that is hard "
|
||||||
|
"to produce. That is not the case today, and connecting these markets to bitcoin is the "
|
||||||
|
"part of the thesis that very few others are making, even as broader crypto tries to "
|
||||||
|
"attach itself to AI and energy. Ten31 invests in that infrastructure with strong conviction."
|
||||||
|
)
|
||||||
|
|
||||||
|
OPTION_A = (
|
||||||
|
"Ten31 invests in the infrastructure of scarcity. We back the bitcoin, energy, and AI "
|
||||||
|
"companies that produce and secure the scarce resources these markets are built on."
|
||||||
|
)
|
||||||
|
OPTION_B = (
|
||||||
|
"Ten31 invests in freedom technology. We back the bitcoin, energy, and AI companies "
|
||||||
|
"building the foundation for a more sovereign, less centralized economy."
|
||||||
|
)
|
||||||
|
|
||||||
|
PILLAR_1 = (
|
||||||
|
"Every one of these markets is bottlenecked on something scarce. AI and bitcoin both "
|
||||||
|
"compete for cheap energy and compute. And we believe energy, compute, and AI "
|
||||||
|
"infrastructure will increasingly settle on money that is hard to produce, which points "
|
||||||
|
"directly at bitcoin. The companies that own and supply the scarce side of that equation "
|
||||||
|
"capture the value as demand grows. That is where we invest. (The bitcoin connection is a "
|
||||||
|
"forward-looking conviction, not a description of today. That gap is the opportunity.)"
|
||||||
|
)
|
||||||
|
PILLAR_2 = (
|
||||||
|
"We invest in foundational infrastructure with real revenue: the companies that generate "
|
||||||
|
"energy, secure capital, and power computation. Real businesses earning real money from "
|
||||||
|
"real demand today."
|
||||||
|
)
|
||||||
|
PILLAR_3 = (
|
||||||
|
"Founders come to us because of our experience and our genuine alignment with bitcoin. We "
|
||||||
|
"pursue and lead opportunities exclusive to us. People in this ecosystem know our track "
|
||||||
|
"record and want us on their side."
|
||||||
|
)
|
||||||
|
|
||||||
|
PROOF = (
|
||||||
|
"$200M+ deployed across two funds into 30+ of the strongest companies in the space "
|
||||||
|
"(Strike, Start9, energy and mining infrastructure). A six-year track record that includes "
|
||||||
|
"large-scale M&A and public-markets activity unmatched by others in this space. Fund III "
|
||||||
|
"continues the same strategy."
|
||||||
|
)
|
||||||
|
|
||||||
|
VOICE = (
|
||||||
|
"Direct, concrete, conviction-driven. No \"betting\" language, no em dashes, no \"X, not Y\" "
|
||||||
|
"phrasing, no kitchen-sink lists. Plain sentences a serious LP can verify in their head."
|
||||||
|
)
|
||||||
|
|
||||||
|
POSITIONING_NOTE = (
|
||||||
|
"Open partner decision: which banner do we lead with? Option A frames Ten31 around "
|
||||||
|
"scarcity; Option B frames it around freedom technology (a banner in the spirit of a16z's "
|
||||||
|
"\"American Dynamism\"). Both wordings are still being refined — use the Architect to draft "
|
||||||
|
"more options."
|
||||||
|
)
|
||||||
|
|
||||||
|
# segment_key -> (display name, definition, needs_to_hear angle)
|
||||||
|
SEGMENTS = [
|
||||||
|
("btc_native_hnwi", "Bitcoin-native HNWIs (OGs)",
|
||||||
|
"Long-time bitcoin holders with significant net worth who care about the ecosystem succeeding.",
|
||||||
|
"Bitcoin only wins if people build on it. Holding is not enough. You care about making "
|
||||||
|
"bitcoin succeed, and so do we. We put capital behind the companies that turn bitcoin into "
|
||||||
|
"a working economy.",
|
||||||
|
"Don't lecture OGs or imply they don't understand bitcoin; don't suggest that simply holding is the goal."),
|
||||||
|
("institution", "Institutions",
|
||||||
|
"Institutional allocators evaluating exposure to the bitcoin, energy, and AI buildout.",
|
||||||
|
"Exposure to the bitcoin, energy, and AI buildout through a team with a six-year "
|
||||||
|
"institutional track record, including large-scale M&A and public-markets activity unmatched "
|
||||||
|
"by others in this space (and Grant's prior institutional experience on top of that).",
|
||||||
|
"No hype or moonshot framing. Lead with the verifiable track record, not vision."),
|
||||||
|
("family_office", "Family offices",
|
||||||
|
"Multi-generational capital seeking durable, long-horizon allocations.",
|
||||||
|
"A long-horizon allocation grounded in real businesses, run by a team with deep credibility "
|
||||||
|
"and a real track record.",
|
||||||
|
"Avoid short-term or trader framing; emphasize durability, real businesses, and horizon."),
|
||||||
|
("smaller_accredited", "Smaller accredited ($100k)",
|
||||||
|
"Accredited investors entering at a more accessible commitment size.",
|
||||||
|
"The same thesis our most convicted investors back, at an accessible entry point.",
|
||||||
|
"Don't talk down to them; it is the same thesis, just an accessible entry point."),
|
||||||
|
("ai_energy_operator", "AI & energy operators",
|
||||||
|
"Operators in AI and energy who are not yet focused on bitcoin.",
|
||||||
|
"You may not be focused on bitcoin today, and that is exactly the point. We believe bitcoin "
|
||||||
|
"becomes a larger component of energy and compute over time, and most operators in your "
|
||||||
|
"space are not yet positioned for it. We are, and we invest across the stack that connects them.",
|
||||||
|
"Don't assume they're bitcoin-focused and don't preach; connect bitcoin as a growing "
|
||||||
|
"component of their world over time."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def seed_v5(conn):
|
||||||
|
"""Insert the full v5 thesis (core line + per-segment lines + segments). Assumes
|
||||||
|
the caller has confirmed the thesis is empty; uses fresh ids."""
|
||||||
|
# ── segments table ──
|
||||||
|
for key, name, definition, needs, avoid in SEGMENTS:
|
||||||
|
sid = _eid("seg")
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO segments (id, segment_key, name, definition, needs_to_hear, avoid, version_no, status, created_at, updated_at)
|
||||||
|
VALUES (?,?,?,?,?,?,1,'active',?,?)""",
|
||||||
|
(sid, key, name, definition, needs, avoid, _now(), _now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── core line ──
|
||||||
|
core = _line(conn, "core", "Core Thesis", is_core=True,
|
||||||
|
description="The shared spine of the Ten31 thesis — throughline, banner, pillars, and proof.")
|
||||||
|
root = _node(conn, core, None, "thesis_root", 0, "Ten31 — Core Thesis", "")
|
||||||
|
_node(conn, core, root, "throughline", 1, "Throughline", THROUGHLINE)
|
||||||
|
|
||||||
|
pos = _node(conn, core, root, "section", 2, "Positioning / Banner (open debate)", POSITIONING_NOTE)
|
||||||
|
_node(conn, core, pos, "claim", 1, "Option A — Scarcity-forward", OPTION_A, status="candidate", variant_group="positioning")
|
||||||
|
_node(conn, core, pos, "claim", 2, "Option B — Freedom tech as the banner", OPTION_B, status="candidate", variant_group="positioning")
|
||||||
|
|
||||||
|
pillars = _node(conn, core, root, "section", 3, "Pillars", "")
|
||||||
|
_node(conn, core, pillars, "claim", 1, "1. Scarcity is the whole opportunity", PILLAR_1)
|
||||||
|
_node(conn, core, pillars, "claim", 2, "2. Foundational infrastructure with real revenue", PILLAR_2)
|
||||||
|
_node(conn, core, pillars, "claim", 3, "3. Founders seek us out, and we lead deals others never see", PILLAR_3)
|
||||||
|
|
||||||
|
_node(conn, core, root, "proof_point", 4, "The proof", PROOF)
|
||||||
|
_node(conn, core, root, "section", 5, "Voice", VOICE)
|
||||||
|
|
||||||
|
# ── per-segment lines ──
|
||||||
|
for key, name, _definition, needs, _avoid in SEGMENTS:
|
||||||
|
lid = _line(conn, f"seg_{key}", name, segment_key=key, is_core=False,
|
||||||
|
description=f"Segment-specific angle for {name}.")
|
||||||
|
sroot = _node(conn, lid, None, "thesis_root", 0, name, "")
|
||||||
|
_node(conn, lid, sroot, "segment_cut", 1, "Angle", needs)
|
||||||
|
|
||||||
|
return {"core_line": core, "segments": len(SEGMENTS)}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_thesis_seed(conn):
|
||||||
|
"""Seed the v5 thesis once, only when the Workshop is empty (no thesis lines).
|
||||||
|
Idempotent and non-destructive: never overwrites partner edits."""
|
||||||
|
try:
|
||||||
|
n = conn.execute("SELECT COUNT(*) FROM thesis_lines WHERE deleted_at IS NULL").fetchone()[0]
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
return # thesis tables not present yet (migration 0002 not applied)
|
||||||
|
if n:
|
||||||
|
return
|
||||||
|
out = seed_v5(conn)
|
||||||
|
_log(conn, "thesis.seeded_v5", out["core_line"], {"segments": out["segments"], "source": "thesis-seed-v5"})
|
||||||
|
conn.commit()
|
||||||
|
print(f"[thesis] seeded v5 thesis (core line + {out['segments']} segment lines)")
|
||||||
@@ -17,8 +17,9 @@ export const PACKAGE_TITLE = 'Ten31 Database'
|
|||||||
// * 0.1.0:49 (Architect: Claude thesis generation + Thesis Workshop screen)
|
// * 0.1.0:49 (Architect: Claude thesis generation + Thesis Workshop screen)
|
||||||
// * 0.1.0:50 (Set Anthropic API Key UI action — no terminal needed)
|
// * 0.1.0:50 (Set Anthropic API Key UI action — no terminal needed)
|
||||||
// * 0.1.0:51 (entity-resolution fix: people double-count + duplicate queue)
|
// * 0.1.0:51 (entity-resolution fix: people double-count + duplicate queue)
|
||||||
// * Current: 0.1.0:52 (grid/contacts unification: contact_id link + grid as front door)
|
// * 0.1.0:52 (grid/contacts unification: contact_id link + grid as front door)
|
||||||
export const PACKAGE_VERSION = '0.1.0:52'
|
// * Current: 0.1.0:53 (seed v5 thesis into the Architect Workshop)
|
||||||
|
export const PACKAGE_VERSION = '0.1.0:53'
|
||||||
|
|
||||||
export const DATA_MOUNT_PATH = '/data'
|
export const DATA_MOUNT_PATH = '/data'
|
||||||
export const WEB_PORT = 8080
|
export const WEB_PORT = 8080
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import { v_0_1_0_49 } from './v0.1.0.49'
|
|||||||
import { v_0_1_0_50 } from './v0.1.0.50'
|
import { v_0_1_0_50 } from './v0.1.0.50'
|
||||||
import { v_0_1_0_51 } from './v0.1.0.51'
|
import { v_0_1_0_51 } from './v0.1.0.51'
|
||||||
import { v_0_1_0_52 } from './v0.1.0.52'
|
import { v_0_1_0_52 } from './v0.1.0.52'
|
||||||
|
import { v_0_1_0_53 } from './v0.1.0.53'
|
||||||
|
|
||||||
export const versionGraph = VersionGraph.of({
|
export const versionGraph = VersionGraph.of({
|
||||||
current: v_0_1_0_52,
|
current: v_0_1_0_53,
|
||||||
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51],
|
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { VersionInfo } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
|
// Seed the v5 thesis into the Architect's Thesis Workshop. On upgrade, if the
|
||||||
|
// Workshop is empty, it is populated once with the core line (throughline, the
|
||||||
|
// open Option A/B banner debate as competing variants, the three pillars, the
|
||||||
|
// proof, voice rules), one line per LP segment carrying that segment's angle, and
|
||||||
|
// the segment definitions. Everything lands as draft/candidate — nothing is made
|
||||||
|
// canonical (that stays the partners' dual-approval action). Idempotent: never
|
||||||
|
// overwrites partner edits. No schema migration.
|
||||||
|
export const v_0_1_0_53 = VersionInfo.of({
|
||||||
|
version: '0.1.0:53',
|
||||||
|
releaseNotes: {
|
||||||
|
en_US: [
|
||||||
|
'Seeds the v5 thesis into the Thesis Workshop so you and your partner can start',
|
||||||
|
'iterating immediately: the core line (throughline, the Option A/B banner as',
|
||||||
|
'competing options, three pillars, the proof) plus a line per LP segment with its',
|
||||||
|
'angle. Seeds once when the Workshop is empty and never overwrites your edits.',
|
||||||
|
].join(' '),
|
||||||
|
},
|
||||||
|
migrations: { up: async () => {}, down: async () => {} },
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user