779ba6db59
Brings the §1 bot wrapper, the §4 cache (webhook + reconciler), the §5 schema (six numbered migrations), Gitea OAuth + §6 user provisioning, the §7 catalog left pane, and the propose-to-merge vertical: propose modal opens an idea PR against the meta repo, an owner merges from the pending-idea view, the cache picks it up via webhook or reconciler sweep, and the catalog renders the new super-draft. Per §1 the bot is the only Git writer; every commit, branch creation, and PR merge carries the §6.5 On-behalf-of: trailer and an `actions` audit row. Per §4 the cache is never written from a user action — it's webhook+reconciler only. Covered by `backend/tests/test_propose_vertical.py` against an in-process Gitea simulator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1435 lines
60 KiB
HTML
1435 lines
60 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>RFC App — main pane mockup</title>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||
<style>
|
||
/* ───────────────────────────────────────────────────────────────────────────
|
||
This file is a hand-built mockup, NOT real app code. It exists to make the
|
||
main-pane design decisions in SPEC.md §8 concrete enough to argue about.
|
||
Vanilla HTML/CSS/JS, no build step. Switch states with the pills at the
|
||
top of the page.
|
||
─────────────────────────────────────────────────────────────────────────── */
|
||
|
||
:root {
|
||
--bg: #fafaf9;
|
||
--surface: #ffffff;
|
||
--border: #e5e5e5;
|
||
--border-2: #ececec;
|
||
--ink: #1a1a1a;
|
||
--ink-2: #444;
|
||
--ink-3: #777;
|
||
--ink-4: #999;
|
||
|
||
--accent: #5b21b6; /* purple — claude */
|
||
--accent-soft: #ede9fe;
|
||
--add: #166534; /* green */
|
||
--add-bg: #dcfce7;
|
||
--add-bg-soft: #ecfdf5;
|
||
--rem: #991b1b; /* red */
|
||
--rem-bg: #fee2e2;
|
||
--rem-bg-soft: #fef2f2;
|
||
--warn-bg: #fef3c7;
|
||
--warn-ink: #92400e;
|
||
|
||
--pill: #f1f5f9;
|
||
--pill-ink: #475569;
|
||
|
||
--shadow-1: 0 1px 2px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.06);
|
||
--shadow-2: 0 4px 16px rgba(0,0,0,0.10);
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
|
||
body {
|
||
margin: 0;
|
||
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||
color: var(--ink);
|
||
background: var(--bg);
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* ── Mockup chrome (state-switcher) ──────────────────────────────────── */
|
||
.mock-switcher {
|
||
background: #0f172a;
|
||
color: #e2e8f0;
|
||
padding: 6px 12px;
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
.mock-switcher b { font-weight: 600; margin-right: 6px; color: #fff; }
|
||
.mock-switcher button {
|
||
background: rgba(255,255,255,0.08);
|
||
border: 1px solid rgba(255,255,255,0.18);
|
||
color: #cbd5e1;
|
||
border-radius: 999px;
|
||
padding: 3px 10px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
}
|
||
.mock-switcher button.active {
|
||
background: #fff;
|
||
color: #0f172a;
|
||
border-color: #fff;
|
||
font-weight: 600;
|
||
}
|
||
.mock-switcher .spacer { flex: 1; }
|
||
.mock-switcher .note { color: #94a3b8; font-style: italic; }
|
||
|
||
/* ── App chrome ─────────────────────────────────────────────────────── */
|
||
.app { flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
||
|
||
.app-header {
|
||
height: 44px;
|
||
background: #1a1a1a;
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 16px;
|
||
gap: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
.app-header .brand { font-weight: 600; font-size: 13px; }
|
||
.app-header .crumbs { color: #aaa; font-size: 13px; display: flex; gap: 8px; align-items: center; }
|
||
.app-header .crumbs .sep { color: #555; }
|
||
.app-header .crumbs .active { color: #fff; }
|
||
.app-header .spacer { flex: 1; }
|
||
.app-header .me {
|
||
width: 28px; height: 28px; border-radius: 50%;
|
||
background: linear-gradient(135deg,#7c3aed,#3b82f6);
|
||
color: #fff; font-weight: 700; font-size: 12px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
|
||
.app-body { flex: 1; display: flex; min-height: 0; overflow: hidden; }
|
||
|
||
/* ── Left pane ──────────────────────────────────────────────────────── */
|
||
.left {
|
||
width: 280px;
|
||
border-right: 1px solid var(--border);
|
||
background: var(--surface);
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
}
|
||
.left .search {
|
||
padding: 10px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.left .search input {
|
||
width: 100%;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
padding: 6px 8px;
|
||
font-size: 12px;
|
||
}
|
||
.rfc-list { flex: 1; overflow: auto; padding: 4px 0; }
|
||
.rfc-row {
|
||
padding: 6px 12px;
|
||
font-size: 13px;
|
||
color: var(--ink-2);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.rfc-row .id { color: var(--ink-4); font-variant-numeric: tabular-nums; font-size: 11px; }
|
||
.rfc-row.super-draft .id { font-style: italic; }
|
||
.rfc-row.selected {
|
||
background: #eef2ff;
|
||
color: var(--ink);
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Tree view inside selected RFC */
|
||
.rfc-tree {
|
||
background: #f8fafc;
|
||
padding: 4px 0;
|
||
border-top: 1px solid var(--border-2);
|
||
border-bottom: 1px solid var(--border-2);
|
||
}
|
||
.tree-row {
|
||
padding: 4px 12px 4px 28px;
|
||
font-size: 12px;
|
||
color: var(--ink-3);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.tree-row .icon { width: 14px; text-align: center; color: var(--ink-4); }
|
||
.tree-row .meta { color: var(--ink-4); font-size: 10px; }
|
||
.tree-row.selected {
|
||
background: #e0e7ff;
|
||
color: var(--ink);
|
||
font-weight: 500;
|
||
}
|
||
.tree-section {
|
||
padding: 4px 12px 2px 12px;
|
||
font-size: 10px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
color: var(--ink-4);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* ── Main pane ──────────────────────────────────────────────────────── */
|
||
.main {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
background: var(--bg);
|
||
}
|
||
|
||
/* Subheader strip — the "what am I looking at?" bar */
|
||
.main-sub {
|
||
background: var(--surface);
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 8px 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-height: 44px;
|
||
flex-shrink: 0;
|
||
}
|
||
.ref-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: var(--pill);
|
||
color: var(--pill-ink);
|
||
border-radius: 6px;
|
||
padding: 3px 8px;
|
||
font-size: 12px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
}
|
||
.ref-pill.branch { background: #f0f9ff; color: #075985; }
|
||
.ref-pill.pr { background: #fff7ed; color: #9a3412; }
|
||
.ref-pill.main { background: #f1f5f9; color: #334155; }
|
||
|
||
.cmp {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
color: var(--ink-3); font-size: 12px;
|
||
}
|
||
.cmp .arrow { color: var(--ink-4); }
|
||
|
||
.sub-spacer { flex: 1; }
|
||
|
||
/* View toggle (Diff / Edit) */
|
||
.view-toggle {
|
||
display: inline-flex;
|
||
background: var(--pill);
|
||
border-radius: 6px;
|
||
padding: 2px;
|
||
}
|
||
.view-toggle button {
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 4px 10px;
|
||
font-size: 12px;
|
||
color: var(--ink-3);
|
||
cursor: pointer;
|
||
}
|
||
.view-toggle button.active {
|
||
background: #fff;
|
||
color: var(--ink);
|
||
box-shadow: var(--shadow-1);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.btn {
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
padding: 5px 12px;
|
||
font-size: 12px;
|
||
color: var(--ink-2);
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
}
|
||
.btn.primary {
|
||
background: var(--ink);
|
||
color: #fff;
|
||
border-color: var(--ink);
|
||
}
|
||
.btn.primary:disabled {
|
||
opacity: 0.4;
|
||
cursor: default;
|
||
}
|
||
|
||
/* In-flight rail (RFC-root state) */
|
||
.inflight-rail {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.inflight-rail .label {
|
||
color: var(--ink-3); font-size: 12px; margin-right: 4px;
|
||
}
|
||
.inflight-chip {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
background: #fff; border: 1px solid var(--border);
|
||
border-radius: 999px; padding: 3px 10px;
|
||
font-size: 12px; color: var(--ink-2);
|
||
cursor: pointer;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
}
|
||
.inflight-chip .dot {
|
||
width: 6px; height: 6px; border-radius: 50%; background: #0ea5e9;
|
||
}
|
||
.inflight-chip.pr .dot { background: #f97316; }
|
||
.inflight-chip .age { color: var(--ink-4); }
|
||
.inflight-chip:hover { border-color: var(--ink-3); }
|
||
|
||
/* ── Body: shared ───────────────────────────────────────────────────── */
|
||
.main-body {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 28px 56px;
|
||
}
|
||
.doc { max-width: 760px; margin: 0 auto; }
|
||
.doc h1 { font-size: 24px; margin: 0 0 8px; }
|
||
.doc h2 {
|
||
font-size: 18px; margin: 28px 0 8px;
|
||
padding-bottom: 4px; border-bottom: 1px solid var(--border-2);
|
||
}
|
||
.doc h3 { font-size: 15px; margin: 18px 0 6px; }
|
||
.doc p { margin: 8px 0; color: var(--ink-2); }
|
||
.doc ul { margin: 6px 0; padding-left: 22px; color: var(--ink-2); }
|
||
.doc code {
|
||
background: #f1f5f9; padding: 1px 4px; border-radius: 3px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* RFC-root header block */
|
||
.root-header {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 14px 18px;
|
||
margin-bottom: 18px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
.root-header .meta {
|
||
color: var(--ink-3); font-size: 12px;
|
||
}
|
||
.root-header .meta b { color: var(--ink); font-weight: 600; }
|
||
.root-header .spacer { flex: 1; }
|
||
|
||
/* ── Diff view ──────────────────────────────────────────────────────── */
|
||
.diff-summary {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 10px 14px;
|
||
margin-bottom: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
font-size: 12px;
|
||
color: var(--ink-3);
|
||
}
|
||
.diff-summary .count { font-weight: 600; color: var(--ink); }
|
||
.diff-summary .add { color: var(--add); }
|
||
.diff-summary .rem { color: var(--rem); }
|
||
.diff-summary .pend { color: var(--warn-ink); }
|
||
|
||
/* Mini-toggle inside the diff-summary for full-doc vs changes-only */
|
||
.scope-toggle {
|
||
display: inline-flex;
|
||
background: var(--pill);
|
||
border-radius: 5px;
|
||
padding: 2px;
|
||
margin-left: auto;
|
||
}
|
||
.scope-toggle button {
|
||
background: transparent; border: none; border-radius: 3px;
|
||
padding: 3px 8px; font-size: 11px; color: var(--ink-3);
|
||
cursor: pointer;
|
||
}
|
||
.scope-toggle button.active {
|
||
background: #fff; color: var(--ink); font-weight: 500;
|
||
box-shadow: var(--shadow-1);
|
||
}
|
||
|
||
/* "Hunks" — each region of contiguous change in the diff view */
|
||
.hunk {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
margin-bottom: 14px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
.hunk-header {
|
||
background: #f8fafc;
|
||
border-bottom: 1px solid var(--border-2);
|
||
padding: 6px 12px;
|
||
font-size: 11px;
|
||
color: var(--ink-3);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
display: flex; align-items: center; gap: 10px;
|
||
}
|
||
.hunk-header .loc { color: var(--ink-4); }
|
||
.hunk-header .prov {
|
||
background: var(--accent-soft); color: var(--accent);
|
||
padding: 1px 6px; border-radius: 3px; font-size: 10px;
|
||
font-family: inherit; font-weight: 600;
|
||
}
|
||
.hunk-header .prov.manual { background: var(--pill); color: var(--pill-ink); }
|
||
.hunk-header .convo-link {
|
||
color: var(--ink-3); text-decoration: underline;
|
||
text-underline-offset: 2px; cursor: pointer;
|
||
}
|
||
.hunk-header .state {
|
||
margin-left: auto;
|
||
background: var(--warn-bg); color: var(--warn-ink);
|
||
padding: 1px 8px; border-radius: 999px;
|
||
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
font-family: inherit;
|
||
}
|
||
.hunk-header .state.accepted { background: var(--add-bg); color: var(--add); }
|
||
.hunk-header .state.declined { background: #f3f4f6; color: var(--ink-3); }
|
||
|
||
.hunk-body {
|
||
padding: 8px 0;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 12.5px;
|
||
line-height: 1.55;
|
||
}
|
||
.line {
|
||
display: flex;
|
||
gap: 8px;
|
||
padding: 0 12px;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
.line .gutter {
|
||
width: 14px;
|
||
flex-shrink: 0;
|
||
color: var(--ink-4);
|
||
text-align: center;
|
||
user-select: none;
|
||
}
|
||
.line.context { background: transparent; color: var(--ink-2); }
|
||
.line.add { background: var(--add-bg-soft); }
|
||
.line.add .gutter { color: var(--add); }
|
||
.line.add .content { color: var(--add); }
|
||
.line.rem { background: var(--rem-bg-soft); }
|
||
.line.rem .gutter { color: var(--rem); }
|
||
.line.rem .content { color: var(--rem); }
|
||
|
||
.hunk-actions {
|
||
border-top: 1px solid var(--border-2);
|
||
background: #fafbfc;
|
||
padding: 6px 12px;
|
||
display: flex; gap: 6px; align-items: center;
|
||
font-size: 11px; color: var(--ink-3);
|
||
}
|
||
.hunk-actions .spacer { flex: 1; }
|
||
.hunk-actions .btn { padding: 3px 10px; font-size: 11px; }
|
||
.hunk-actions .btn-accept { background: var(--add); color: #fff; border-color: var(--add); }
|
||
.hunk-actions .btn-decline { background: #fff; color: var(--rem); border-color: var(--rem-bg); }
|
||
|
||
/* Anchored conversation marker in margin */
|
||
.anchor-bubble {
|
||
position: absolute;
|
||
right: -260px;
|
||
top: 8px;
|
||
width: 230px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 8px 10px;
|
||
box-shadow: var(--shadow-1);
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||
font-size: 11px;
|
||
color: var(--ink-3);
|
||
cursor: pointer;
|
||
}
|
||
.anchor-bubble:hover { box-shadow: var(--shadow-2); border-color: var(--ink-4); }
|
||
.anchor-bubble .who {
|
||
font-weight: 600; color: var(--ink);
|
||
display: flex; align-items: center; gap: 5px;
|
||
margin-bottom: 2px;
|
||
}
|
||
.anchor-bubble .who .avatar {
|
||
width: 14px; height: 14px; border-radius: 50%;
|
||
background: var(--accent); color: #fff; font-size: 9px;
|
||
display: flex; align-items: center; justify-content: center; font-weight: 700;
|
||
}
|
||
.anchor-bubble .preview {
|
||
color: var(--ink-3);
|
||
overflow: hidden;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
}
|
||
.anchor-bubble .footer {
|
||
margin-top: 6px;
|
||
display: flex; gap: 8px;
|
||
font-size: 10px; color: var(--ink-4);
|
||
}
|
||
.anchor-bubble .footer .pending {
|
||
color: var(--warn-ink); font-weight: 600;
|
||
}
|
||
|
||
/* Show anchor bubbles only when there's room (wide viewports) */
|
||
@media (max-width: 1380px) {
|
||
.anchor-bubble { display: none; }
|
||
}
|
||
|
||
/* Unchanged-context prose between hunks: the full document reads naturally,
|
||
with diff hunks embedded at their locations. In "changes only" mode the
|
||
unchanged regions collapse to one-line skips. */
|
||
.ctx { color: var(--ink-2); }
|
||
.ctx h2 {
|
||
font-size: 18px;
|
||
margin: 28px 0 8px;
|
||
padding-bottom: 4px;
|
||
border-bottom: 1px solid var(--border-2);
|
||
}
|
||
.ctx p { margin: 8px 0; }
|
||
.ctx p .loc {
|
||
color: var(--ink-4);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 10px;
|
||
margin-right: 6px;
|
||
user-select: none;
|
||
opacity: 0;
|
||
transition: opacity 0.1s;
|
||
}
|
||
.ctx p:hover .loc { opacity: 1; }
|
||
|
||
.skip-region {
|
||
text-align: center;
|
||
color: var(--ink-4);
|
||
font-size: 11px;
|
||
padding: 6px 0;
|
||
border-top: 1px dashed var(--border-2);
|
||
border-bottom: 1px dashed var(--border-2);
|
||
margin: 10px 0;
|
||
cursor: pointer;
|
||
}
|
||
.skip-region:hover { color: var(--ink-2); }
|
||
|
||
/* Changes-only mode: hide all .ctx blocks and h2 headers between hunks,
|
||
replacing them with the skip-region markers we leave in place. */
|
||
.main-body.changes-only .ctx { display: none; }
|
||
.main-body.changes-only .skip-region { display: block; }
|
||
.main-body:not(.changes-only) .skip-region { display: none; }
|
||
|
||
/* ── Edit view (track-changes inline) ──────────────────────────────── */
|
||
.edit-doc {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 28px 36px;
|
||
line-height: 1.7;
|
||
color: var(--ink-2);
|
||
}
|
||
.edit-doc h2 { font-size: 17px; margin-top: 22px; }
|
||
.tracked-delete {
|
||
background: var(--rem-bg);
|
||
color: var(--rem);
|
||
text-decoration: line-through;
|
||
text-decoration-color: rgba(153, 27, 27, 0.5);
|
||
padding: 0 2px; border-radius: 2px;
|
||
}
|
||
.tracked-insert {
|
||
background: var(--add-bg);
|
||
color: var(--add);
|
||
padding: 0 2px; border-radius: 2px;
|
||
}
|
||
.anchor-pin {
|
||
display: inline-flex;
|
||
vertical-align: middle;
|
||
width: 16px; height: 16px;
|
||
border-radius: 50%;
|
||
background: var(--accent);
|
||
color: #fff;
|
||
font-size: 9px;
|
||
font-weight: 700;
|
||
align-items: center; justify-content: center;
|
||
margin: 0 2px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* ── PR view extras ─────────────────────────────────────────────────── */
|
||
.pr-banner {
|
||
background: #fffbeb;
|
||
border: 1px solid #fcd34d;
|
||
color: #92400e;
|
||
border-radius: 8px;
|
||
padding: 10px 14px;
|
||
font-size: 12px;
|
||
margin-bottom: 14px;
|
||
display: flex; align-items: center; gap: 10px;
|
||
}
|
||
.pr-banner b { font-weight: 600; }
|
||
|
||
/* ── Right pane ─────────────────────────────────────────────────────── */
|
||
.right {
|
||
width: 360px;
|
||
background: var(--surface);
|
||
border-left: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
min-height: 0;
|
||
}
|
||
.right-tabs {
|
||
display: flex;
|
||
border-bottom: 1px solid var(--border);
|
||
flex-shrink: 0;
|
||
}
|
||
.right-tabs button {
|
||
flex: 1;
|
||
background: transparent;
|
||
border: none;
|
||
padding: 10px 8px;
|
||
font-size: 12px;
|
||
color: var(--ink-3);
|
||
cursor: pointer;
|
||
border-bottom: 2px solid transparent;
|
||
}
|
||
.right-tabs button.active {
|
||
color: var(--ink);
|
||
font-weight: 600;
|
||
border-bottom-color: var(--ink);
|
||
}
|
||
.right-tabs button .count {
|
||
background: var(--pill);
|
||
color: var(--pill-ink);
|
||
border-radius: 999px;
|
||
padding: 1px 6px;
|
||
font-size: 10px;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.right-body { flex: 1; overflow: auto; }
|
||
|
||
/* Tendril list (overview) */
|
||
.tendril-list { padding: 6px 0; }
|
||
.tendril-item {
|
||
padding: 10px 14px;
|
||
border-bottom: 1px solid var(--border-2);
|
||
cursor: pointer;
|
||
}
|
||
.tendril-item:hover { background: #fafbfc; }
|
||
.tendril-item.active {
|
||
background: #eef2ff;
|
||
border-left: 3px solid var(--accent);
|
||
padding-left: 11px;
|
||
}
|
||
.tendril-item .anchor {
|
||
font-size: 11px; color: var(--ink-4);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
margin-bottom: 2px;
|
||
}
|
||
.tendril-item .quote {
|
||
background: #fef9c3; padding: 2px 6px;
|
||
border-radius: 3px; font-size: 12px;
|
||
color: var(--ink-2);
|
||
overflow: hidden;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 1;
|
||
-webkit-box-orient: vertical;
|
||
font-style: italic;
|
||
margin-bottom: 4px;
|
||
}
|
||
.tendril-item .summary {
|
||
font-size: 12px; color: var(--ink-2);
|
||
overflow: hidden;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
}
|
||
.tendril-item .meta {
|
||
font-size: 10px; color: var(--ink-4);
|
||
margin-top: 5px; display: flex; gap: 8px;
|
||
}
|
||
.tendril-item .meta .pending {
|
||
color: var(--warn-ink); font-weight: 600;
|
||
}
|
||
.tendril-item .meta .accepted {
|
||
color: var(--add); font-weight: 600;
|
||
}
|
||
|
||
/* Branch-level chat at top of tendril list */
|
||
.branch-thread {
|
||
background: #f0f9ff;
|
||
padding: 10px 14px;
|
||
border-bottom: 1px solid var(--border);
|
||
cursor: pointer;
|
||
display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.branch-thread .icon { font-size: 14px; }
|
||
.branch-thread .label { font-size: 12px; color: var(--ink); font-weight: 600; flex: 1; }
|
||
.branch-thread .meta { font-size: 10px; color: var(--ink-3); }
|
||
|
||
/* Active conversation detail (when one tendril is opened) */
|
||
.convo-detail {
|
||
display: flex; flex-direction: column; height: 100%;
|
||
}
|
||
.convo-detail .convo-header {
|
||
padding: 10px 14px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: #fafbfc;
|
||
}
|
||
.convo-detail .convo-header .back {
|
||
background: none; border: none; color: var(--ink-3);
|
||
font-size: 11px; cursor: pointer; padding: 0;
|
||
margin-bottom: 4px;
|
||
}
|
||
.convo-detail .convo-header .anchor {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 11px; color: var(--ink-4);
|
||
}
|
||
.convo-detail .convo-header .quote {
|
||
background: #fef9c3; padding: 4px 8px;
|
||
border-radius: 4px; font-size: 12px;
|
||
font-style: italic; color: var(--ink-2);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.convo-messages { flex: 1; overflow: auto; padding: 10px 12px; }
|
||
.msg { margin-bottom: 10px; }
|
||
.msg .who {
|
||
font-size: 11px; font-weight: 600; color: var(--ink-3);
|
||
margin-bottom: 3px;
|
||
display: flex; align-items: center; gap: 5px;
|
||
}
|
||
.msg .who .dot {
|
||
width: 6px; height: 6px; border-radius: 50%;
|
||
}
|
||
.msg.user .who .dot { background: #3b82f6; }
|
||
.msg.ai .who .dot { background: var(--accent); }
|
||
.msg.ai .who .model { color: var(--accent); font-weight: 500; font-size: 10px; }
|
||
.msg .bubble {
|
||
background: #f8fafc;
|
||
border-radius: 8px;
|
||
padding: 8px 10px;
|
||
font-size: 12.5px;
|
||
color: var(--ink);
|
||
line-height: 1.5;
|
||
}
|
||
.msg.ai .bubble { background: var(--accent-soft); }
|
||
|
||
.msg .change-chip {
|
||
margin-top: 4px;
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
background: var(--warn-bg); color: var(--warn-ink);
|
||
border-radius: 4px; padding: 2px 6px;
|
||
font-size: 11px; font-weight: 600;
|
||
cursor: pointer;
|
||
}
|
||
.msg .change-chip.accepted { background: var(--add-bg); color: var(--add); }
|
||
|
||
/* Compose box */
|
||
.compose {
|
||
border-top: 1px solid var(--border);
|
||
padding: 10px 12px;
|
||
background: #fff;
|
||
flex-shrink: 0;
|
||
}
|
||
.compose textarea {
|
||
width: 100%;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
font-family: inherit;
|
||
font-size: 12.5px;
|
||
resize: vertical;
|
||
min-height: 56px;
|
||
}
|
||
.compose-row {
|
||
display: flex; align-items: center;
|
||
margin-top: 6px; gap: 6px;
|
||
}
|
||
.compose-row .model-pill {
|
||
background: var(--pill); color: var(--pill-ink);
|
||
border-radius: 4px; padding: 2px 7px; font-size: 11px;
|
||
}
|
||
.compose-row .spacer { flex: 1; }
|
||
|
||
/* Hide alternate states */
|
||
[data-state] { display: none; }
|
||
[data-state].active { display: block; }
|
||
[data-state="root"].active { display: contents; }
|
||
[data-state="branch-diff"].active,
|
||
[data-state="branch-edit"].active,
|
||
[data-state="pr"].active { display: contents; }
|
||
|
||
/* Right-pane mode-specific overrides */
|
||
.right[data-mode="convo"] .tendril-list { display: none; }
|
||
.right[data-mode="convo"] .branch-thread { display: none; }
|
||
.right[data-mode="list"] .convo-detail { display: none; }
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ── State switcher (mockup chrome) ──────────────────────────────────── -->
|
||
<div class="mock-switcher">
|
||
<b>Main pane states:</b>
|
||
<button class="state-btn" data-target="root">RFC root</button>
|
||
<button class="state-btn active" data-target="branch-diff">Branch · Diff view</button>
|
||
<button class="state-btn" data-target="branch-edit">Branch · Edit view</button>
|
||
<button class="state-btn" data-target="pr">PR</button>
|
||
<span class="spacer"></span>
|
||
<span class="note">Mockup for SPEC.md §8 — not real app code</span>
|
||
</div>
|
||
|
||
<div class="app">
|
||
<!-- ── App header ───────────────────────────────────────────────────── -->
|
||
<div class="app-header">
|
||
<span class="brand">OHM RFC Contributor</span>
|
||
<span class="crumbs">
|
||
<span>wiggleverse</span><span class="sep">/</span>
|
||
<span>RFC-0042 · Open Human Model</span>
|
||
<span class="sep" id="crumb-sep" style="display:none">/</span>
|
||
<span class="active" id="crumb-ref"></span>
|
||
</span>
|
||
<span class="spacer"></span>
|
||
<div class="me">B</div>
|
||
</div>
|
||
|
||
<div class="app-body">
|
||
|
||
<!-- ── Left pane (shared across states) ──────────────────────────── -->
|
||
<aside class="left">
|
||
<div class="search">
|
||
<input type="text" placeholder="Search RFCs by title, slug, or ID…" />
|
||
</div>
|
||
<div class="rfc-list">
|
||
<div class="rfc-row"><span class="id">RFC-0038</span><span>Capability negotiation</span></div>
|
||
<div class="rfc-row"><span class="id">RFC-0040</span><span>Provider keys & rotation</span></div>
|
||
<div class="rfc-row selected"><span class="id">RFC-0042</span><span>Open Human Model</span></div>
|
||
|
||
<!-- Tree view appears under the selected RFC -->
|
||
<div class="rfc-tree">
|
||
<div class="tree-section">Main</div>
|
||
<div class="tree-row" data-jump="root">
|
||
<span class="icon">●</span>
|
||
<span>main</span>
|
||
<span class="meta">v1 · 4d ago</span>
|
||
</div>
|
||
|
||
<div class="tree-section">Open PRs (1)</div>
|
||
<div class="tree-row" data-jump="pr">
|
||
<span class="icon">⇪</span>
|
||
<span>#4 split §5 into §5/§5a</span>
|
||
<span class="meta">alice</span>
|
||
</div>
|
||
|
||
<div class="tree-section">Open branches (3)</div>
|
||
<div class="tree-row selected" data-jump="branch-diff">
|
||
<span class="icon">⎇</span>
|
||
<span>ben/clarify-graduation</span>
|
||
<span class="meta">2d</span>
|
||
</div>
|
||
<div class="tree-row">
|
||
<span class="icon">⎇</span>
|
||
<span>raj/inline-arbiters</span>
|
||
<span class="meta">5d</span>
|
||
</div>
|
||
<div class="tree-row">
|
||
<span class="icon">⎇</span>
|
||
<span>jo/spike-id-scheme</span>
|
||
<span class="meta">9d</span>
|
||
</div>
|
||
|
||
<div class="tree-section">Closed (3) <span style="float:right;color:#999;cursor:pointer">show ▾</span></div>
|
||
</div>
|
||
|
||
<div class="rfc-row super-draft"><span class="id">super-draft</span><span>Conflict resolution model</span></div>
|
||
<div class="rfc-row"><span class="id">RFC-0035</span><span>Bot service account</span></div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- ── Main pane ─────────────────────────────────────────────────── -->
|
||
<main class="main">
|
||
|
||
<!-- ── State: RFC root ──────────────────────────────────────────── -->
|
||
<div data-state="root">
|
||
<div class="main-sub">
|
||
<span class="ref-pill main">main</span>
|
||
<span class="cmp">v1 · merged 4d ago by ben</span>
|
||
<span class="inflight-rail">
|
||
<span class="label">In flight:</span>
|
||
<span class="inflight-chip" data-jump="pr"><span class="dot"></span>PR #4 · split §5 <span class="age">· 1d</span></span>
|
||
<span class="inflight-chip" data-jump="branch-diff"><span class="dot"></span>ben/clarify-graduation <span class="age">· 2d</span></span>
|
||
<span class="inflight-chip"><span class="dot"></span>raj/inline-arbiters <span class="age">· 5d</span></span>
|
||
<span class="inflight-chip"><span class="dot"></span>jo/spike-id-scheme <span class="age">· 9d</span></span>
|
||
</span>
|
||
<span class="sub-spacer"></span>
|
||
<button class="btn">+ New branch</button>
|
||
</div>
|
||
|
||
<div class="main-body">
|
||
<div class="doc">
|
||
<div class="root-header">
|
||
<div>
|
||
<div style="font-weight:600;font-size:15px">RFC-0042 · Open Human Model</div>
|
||
<div class="meta">Active · owners <b>ben</b>, <b>raj</b> · arbiters <b>ben</b> · tags identity, schema</div>
|
||
</div>
|
||
<span class="spacer"></span>
|
||
<button class="btn">☆ Star</button>
|
||
<button class="btn primary">+ Propose change</button>
|
||
</div>
|
||
|
||
<h1>Open Human Model</h1>
|
||
<p style="color:var(--ink-3);font-style:italic">This is the canonical, merged-to-main version. Drill into a branch or PR (left pane or chips above) to see proposed changes.</p>
|
||
|
||
<h2>1. Why this RFC exists</h2>
|
||
<p>The Open Human Model (OHM) is the schema that lets a contributor describe themselves to the rest of the system in a way that other tools — RFCs, applications, the meta-repo's directory — can read uniformly.</p>
|
||
|
||
<h2>2. Scope</h2>
|
||
<p>This RFC defines the on-disk shape of an OHM record, its required and optional fields, the versioning policy, and the migration path from the prior ad-hoc <code>profile.yaml</code>.</p>
|
||
|
||
<h2>3. Schema</h2>
|
||
<p>An OHM record is a YAML document with a frontmatter block of canonical fields and an open free-form body. The canonical fields are…</p>
|
||
|
||
<h2>4. Versioning</h2>
|
||
<p>OHM follows semantic versioning at the document level. Major bumps may break consumers; minor bumps add fields…</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── State: Branch · Diff view ─────────────────────────────────── -->
|
||
<div data-state="branch-diff" class="active">
|
||
<div class="main-sub">
|
||
<span class="ref-pill branch">⎇ ben/clarify-graduation</span>
|
||
<span class="cmp"><span class="arrow">◂</span> compared to <b style="color:var(--ink);font-weight:500">main</b></span>
|
||
<span class="sub-spacer"></span>
|
||
<div class="view-toggle">
|
||
<button class="active" data-toggle="branch-diff">Diff</button>
|
||
<button data-toggle="branch-edit">Edit</button>
|
||
</div>
|
||
<button class="btn">Open PR ▸</button>
|
||
</div>
|
||
|
||
<div class="main-body" id="branch-diff-body">
|
||
<div class="doc">
|
||
<div class="diff-summary">
|
||
<span><span class="count">5 changes</span> across 3 sections</span>
|
||
<span class="add">+34 lines</span>
|
||
<span class="rem">−18 lines</span>
|
||
<span>·</span>
|
||
<span><b style="color:var(--add)">2 accepted</b>, <b class="pend">2 pending</b>, <b style="color:var(--ink-4)">1 declined</b></span>
|
||
<div class="scope-toggle" data-scope-target="branch-diff-body">
|
||
<button class="active" data-scope="full">Full doc</button>
|
||
<button data-scope="changes">Changes only</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── §1 Why this RFC exists ── -->
|
||
<div class="ctx">
|
||
<h2>1. Why this RFC exists</h2>
|
||
<p><span class="loc">§1 ¶1</span>The Open Human Model (OHM) is the schema that lets a contributor describe themselves to the rest of the system in a way that other tools — RFCs, applications, the meta-repo's directory — can read uniformly.</p>
|
||
<p><span class="loc">§1 ¶2</span>Today, every consumer reaches into the ad-hoc <code>profile.yaml</code> and projects its own subset of fields. The result is silent drift: two consumers can read the same file and disagree about what a contributor's handle even is.</p>
|
||
</div>
|
||
<div class="skip-region">▴ §1 unchanged · 2 paragraphs · expand</div>
|
||
|
||
<!-- ── §2 Scope ── -->
|
||
<div class="ctx">
|
||
<h2>2. Scope</h2>
|
||
<p><span class="loc">§2 ¶1</span>This RFC defines the on-disk shape of an OHM record, its required and optional fields, the versioning policy, and the migration path from the prior ad-hoc <code>profile.yaml</code>.</p>
|
||
<p><span class="loc">§2 ¶2</span>It does <em>not</em> define an API for reading OHM records, nor a publishing protocol — those are independent concerns and will be specified in their own RFCs.</p>
|
||
</div>
|
||
<div class="skip-region">▴ §2 unchanged · 2 paragraphs · expand</div>
|
||
|
||
<!-- ── §3 Schema ── -->
|
||
<div class="ctx">
|
||
<h2>3. Schema</h2>
|
||
<p><span class="loc">§3 ¶1</span>An OHM record is a YAML document with a frontmatter block of canonical fields and an open free-form body. The frontmatter is what consumers parse; the body is for the contributor to write in.</p>
|
||
</div>
|
||
|
||
<div class="hunk">
|
||
<div class="hunk-header">
|
||
<span class="loc">§3 ¶2</span>
|
||
<span class="prov">Claude · sonnet-4.5</span>
|
||
<span class="convo-link" data-open-tendril="t1">from anchored thread "schema field naming"</span>
|
||
<span class="state accepted">Accepted</span>
|
||
</div>
|
||
<div class="hunk-body">
|
||
<div class="line rem"><span class="gutter">−</span><span class="content">The canonical fields are <code>name</code>, <code>handle</code>, <code>pronouns</code>, and <code>bio</code>.</span></div>
|
||
<div class="line add"><span class="gutter">+</span><span class="content">The canonical fields are <code>display_name</code>, <code>handle</code>, <code>pronouns</code>, <code>bio</code>, and <code>established_at</code>.</span></div>
|
||
<div class="line add"><span class="gutter">+</span><span class="content">The <code>display_name</code> rename clarifies that this field is the human-facing label, distinct from the immutable <code>handle</code>.</span></div>
|
||
</div>
|
||
<div class="hunk-actions">
|
||
<span>Accepted by ben · 4h ago · edited before accepting</span>
|
||
<span class="spacer"></span>
|
||
<button class="btn">↶ Unaccept</button>
|
||
<button class="btn">View convo</button>
|
||
</div>
|
||
<!-- Margin anchor bubble pointing into this hunk -->
|
||
<div class="anchor-bubble" data-open-tendril="t1">
|
||
<div class="who"><span class="avatar">C</span> schema field naming</div>
|
||
<div class="preview">"the name field is ambiguous — display label or stable id?" → Claude proposed renaming…</div>
|
||
<div class="footer">
|
||
<span>4 msgs</span>
|
||
<span class="accepted">1 accepted</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hunk">
|
||
<div class="hunk-header">
|
||
<span class="loc">§3 ¶3 (new paragraph)</span>
|
||
<span class="prov manual">Manual · ben</span>
|
||
<span class="state accepted">Accepted</span>
|
||
</div>
|
||
<div class="hunk-body">
|
||
<div class="line add"><span class="gutter">+</span><span class="content">A record's <code>established_at</code> is the date the contributor first claimed this OHM, expressed as ISO-8601. Subsequent edits do not bump it.</span></div>
|
||
</div>
|
||
<div class="hunk-actions">
|
||
<span>Typed directly by ben · 4h ago</span>
|
||
<span class="spacer"></span>
|
||
<button class="btn">↶ Revert</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ctx">
|
||
<p><span class="loc">§3 ¶4</span>Each field has a normative type in §3.1 (string, ISO-8601 date, list of strings, etc.) and a non-normative example in §3.2. Consumers MUST validate the type and SHOULD treat any extra field they don't recognize as opaque.</p>
|
||
<p><span class="loc">§3 ¶5</span>The free-form body is plain Markdown. It is intended for the contributor's own narrative — bio, working hours, time-zone notes, whatever they want collaborators to know. Consumers that don't render Markdown should pass the body through unchanged.</p>
|
||
</div>
|
||
<div class="skip-region">▴ §3 ¶4–5 unchanged · 2 paragraphs · expand</div>
|
||
|
||
<!-- ── §4 Versioning ── -->
|
||
<div class="ctx">
|
||
<h2>4. Versioning</h2>
|
||
</div>
|
||
|
||
<div class="hunk">
|
||
<div class="hunk-header">
|
||
<span class="loc">§4 ¶1</span>
|
||
<span class="prov">Claude · sonnet-4.5</span>
|
||
<span class="convo-link" data-open-tendril="t2">from anchored thread "is semver overkill?"</span>
|
||
<span class="state">Pending</span>
|
||
</div>
|
||
<div class="hunk-body">
|
||
<div class="line context"><span class="gutter"> </span><span class="content">OHM follows semantic versioning at the document level.</span></div>
|
||
<div class="line rem"><span class="gutter">−</span><span class="content">Major bumps may break consumers; minor bumps add fields without breaking; patch bumps are clarifications.</span></div>
|
||
<div class="line add"><span class="gutter">+</span><span class="content">Major bumps may break consumers (renamed or removed fields); minor bumps add optional fields; patch bumps are clarifications that don't change the schema's shape.</span></div>
|
||
<div class="line add"><span class="gutter">+</span><span class="content">Consumers MUST tolerate unknown optional fields; producers MUST NOT emit major versions without a written migration note in the RFC's repo.</span></div>
|
||
</div>
|
||
<div class="hunk-actions">
|
||
<span>Proposed by Claude · 3h ago</span>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-accept">Accept</button>
|
||
<button class="btn">Edit</button>
|
||
<button class="btn btn-decline">Decline</button>
|
||
</div>
|
||
<div class="anchor-bubble" data-open-tendril="t2">
|
||
<div class="who"><span class="avatar">C</span> is semver overkill?</div>
|
||
<div class="preview">"do we really need MAJOR/MINOR/PATCH for a schema this small?" — discussion led to a more explicit producer/consumer contract…</div>
|
||
<div class="footer">
|
||
<span>9 msgs</span>
|
||
<span class="pending">1 pending</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ctx">
|
||
<p><span class="loc">§4 ¶2</span>The current version is <code>v1</code>. The version is encoded as a single integer prefix (<code>v1</code>, <code>v2</code>, …) at the top of the file rather than embedded in the schema URI, so that consumers can read it without parsing.</p>
|
||
</div>
|
||
<div class="skip-region">▴ §4 ¶2 unchanged · expand</div>
|
||
|
||
<div class="hunk">
|
||
<div class="hunk-header">
|
||
<span class="loc">§4 ¶3</span>
|
||
<span class="prov">Claude · gemini-2.5</span>
|
||
<span class="convo-link" data-open-tendril="t3">from anchored thread "migration ergonomics"</span>
|
||
<span class="state">Pending</span>
|
||
</div>
|
||
<div class="hunk-body">
|
||
<div class="line add"><span class="gutter">+</span><span class="content">Migration scripts live alongside the RFC in <code>.ohm/migrations/<from>-><to>.py</code>. The bot runs them on graduation.</span></div>
|
||
</div>
|
||
<div class="hunk-actions">
|
||
<span>Proposed by Claude · 1h ago</span>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-accept">Accept</button>
|
||
<button class="btn">Edit</button>
|
||
<button class="btn btn-decline">Decline</button>
|
||
</div>
|
||
<div class="anchor-bubble" data-open-tendril="t3">
|
||
<div class="who"><span class="avatar">C</span> migration ergonomics</div>
|
||
<div class="preview">Selection asked: "how does a downstream tool know there's a migration to run?" — Claude proposed convention…</div>
|
||
<div class="footer">
|
||
<span>3 msgs</span>
|
||
<span class="pending">1 pending</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ctx">
|
||
<p><span class="loc">§4 ¶4</span>A version bump is a commit to this RFC's repo. The commit message MUST start with <code>v<n>:</code> and the body MUST summarize the changes in human-readable terms. Tooling depends on this format.</p>
|
||
</div>
|
||
<div class="skip-region">▴ §4 ¶4 unchanged · expand</div>
|
||
|
||
<!-- ── §5 Migration ── -->
|
||
<div class="ctx">
|
||
<h2>5. Migration from <code>profile.yaml</code></h2>
|
||
</div>
|
||
|
||
<div class="hunk">
|
||
<div class="hunk-header">
|
||
<span class="loc">§5 ¶1</span>
|
||
<span class="prov">Claude · sonnet-4.5</span>
|
||
<span class="state declined">Declined</span>
|
||
</div>
|
||
<div class="hunk-body">
|
||
<div class="line rem"><span class="gutter">−</span><span class="content">The migration tool reads the legacy <code>profile.yaml</code> and emits a v1 OHM record, preserving unknown fields under a <code>legacy:</code> key.</span></div>
|
||
<div class="line add" style="opacity:0.5"><span class="gutter">+</span><span class="content">The migration tool DELETES the legacy profile.yaml after emitting the v1 OHM record. Legacy fields are dropped.</span></div>
|
||
</div>
|
||
<div class="hunk-actions">
|
||
<span>Declined by ben · 2h ago · "we want non-destructive migration"</span>
|
||
<span class="spacer"></span>
|
||
<button class="btn">↶ Reconsider</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ctx">
|
||
<p><span class="loc">§5 ¶2</span>The tool runs once per contributor, idempotently. Re-running it on an account that's already migrated is a no-op: the tool reads the existing v1 record, validates it, and exits.</p>
|
||
<p><span class="loc">§5 ¶3</span>Failure modes are noisy by design. A malformed legacy file aborts the migration with a clear error pointing at the offending line; nothing is half-migrated.</p>
|
||
<p><span class="loc">§5 ¶4</span>For accounts that never had a <code>profile.yaml</code>, the tool seeds a minimal v1 record with only the <code>handle</code> field populated from the Gitea login.</p>
|
||
</div>
|
||
<div class="skip-region">▴ §5 ¶2–4 unchanged · 3 paragraphs · expand</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── State: Branch · Edit view ─────────────────────────────────── -->
|
||
<div data-state="branch-edit">
|
||
<div class="main-sub">
|
||
<span class="ref-pill branch">⎇ ben/clarify-graduation</span>
|
||
<span class="cmp"><span class="arrow">◂</span> compared to <b style="color:var(--ink);font-weight:500">main</b></span>
|
||
<span class="sub-spacer"></span>
|
||
<div class="view-toggle">
|
||
<button data-toggle="branch-diff">Diff</button>
|
||
<button class="active" data-toggle="branch-edit">Edit</button>
|
||
</div>
|
||
<button class="btn">Open PR ▸</button>
|
||
</div>
|
||
|
||
<div class="main-body">
|
||
<div class="doc">
|
||
<div class="diff-summary">
|
||
<span class="count">Editing on ben/clarify-graduation</span>
|
||
<span>·</span>
|
||
<span>Inline track-changes shown · click a pin to open its conversation</span>
|
||
<span class="sub-spacer" style="flex:1"></span>
|
||
<span style="color:var(--add)">2 accepted</span>
|
||
<span style="color:var(--warn-ink)">2 pending in the diff view</span>
|
||
</div>
|
||
|
||
<div class="edit-doc">
|
||
<h2>3. Schema</h2>
|
||
<p>
|
||
An OHM record is a YAML document with a frontmatter block of canonical
|
||
fields and an open free-form body. The canonical fields are
|
||
<span class="tracked-delete">name</span><span class="tracked-insert">display_name</span><span class="anchor-pin" data-open-tendril="t1">1</span>,
|
||
<code>handle</code>, <code>pronouns</code>, <span class="tracked-delete">and </span><code>bio</code><span class="tracked-insert">, and <code>established_at</code></span>.
|
||
<span class="tracked-insert">The <code>display_name</code> rename clarifies that this field is the human-facing label, distinct from the immutable <code>handle</code>.</span>
|
||
</p>
|
||
|
||
<p>
|
||
<span class="tracked-insert">A record's <code>established_at</code> is the date the contributor first claimed this OHM, expressed as ISO-8601. Subsequent edits do not bump it.</span>
|
||
</p>
|
||
|
||
<h2>4. Versioning</h2>
|
||
<p>
|
||
OHM follows semantic versioning at the document level. Major bumps may
|
||
break consumers; minor bumps add fields without breaking; patch bumps
|
||
are clarifications.
|
||
<span class="anchor-pin" data-open-tendril="t2">2</span>
|
||
<em style="color:var(--warn-ink);font-size:12px">— 1 pending proposal in this paragraph</em>
|
||
</p>
|
||
|
||
<p>
|
||
Migration scripts live alongside the RFC.
|
||
<span class="anchor-pin" data-open-tendril="t3">3</span>
|
||
<em style="color:var(--warn-ink);font-size:12px">— 1 pending proposal</em>
|
||
</p>
|
||
|
||
<h2>5. Migration from profile.yaml</h2>
|
||
<p>
|
||
The migration tool reads the legacy <code>profile.yaml</code> and emits
|
||
a v1 OHM record, preserving unknown fields under a <code>legacy:</code>
|
||
key.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── State: PR ─────────────────────────────────────────────────── -->
|
||
<div data-state="pr">
|
||
<div class="main-sub">
|
||
<span class="ref-pill pr">⇪ PR #4</span>
|
||
<span style="font-size:13px;color:var(--ink)">"Split §5 into §5 / §5a"</span>
|
||
<span class="cmp"><span class="arrow">◂</span> alice → <b style="color:var(--ink);font-weight:500">main</b></span>
|
||
<span class="sub-spacer"></span>
|
||
<div class="view-toggle">
|
||
<button class="active" data-toggle="pr">Diff</button>
|
||
<button data-toggle="pr-edit-disabled" title="Fork to edit">Edit (fork)</button>
|
||
</div>
|
||
<button class="btn primary">Merge PR</button>
|
||
</div>
|
||
|
||
<div class="main-body" id="pr-body">
|
||
<div class="doc">
|
||
<div class="pr-banner">
|
||
<b>PR #4 by alice</b> · opened 1d ago · 2 reviewers · all conversations resolved · <a href="#">discussion thread</a>
|
||
<span class="sub-spacer" style="flex:1"></span>
|
||
<button class="btn">Fork this PR to edit</button>
|
||
</div>
|
||
|
||
<div class="diff-summary">
|
||
<span class="count">8 changes</span>
|
||
<span class="add">+47 lines</span>
|
||
<span class="rem">−12 lines</span>
|
||
<span>·</span>
|
||
<span><b style="color:var(--add)">All 8 accepted</b> on the branch</span>
|
||
<div class="scope-toggle" data-scope-target="pr-body">
|
||
<button class="active" data-scope="full">Full doc</button>
|
||
<button data-scope="changes">Changes only</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ctx">
|
||
<h2>1. Why this RFC exists</h2>
|
||
<p><span class="loc">§1 ¶1</span>The Open Human Model (OHM) is the schema that lets a contributor describe themselves to the rest of the system in a way that other tools can read uniformly.</p>
|
||
<h2>2. Scope</h2>
|
||
<p><span class="loc">§2 ¶1</span>This RFC defines the on-disk shape of an OHM record, its required and optional fields, the versioning policy, and the migration path from the prior ad-hoc <code>profile.yaml</code>.</p>
|
||
<h2>3. Schema</h2>
|
||
<p><span class="loc">§3 ¶1</span>An OHM record is a YAML document with a frontmatter block of canonical fields and an open free-form body. The canonical fields are <code>display_name</code>, <code>handle</code>, <code>pronouns</code>, <code>bio</code>, and <code>established_at</code>.</p>
|
||
<h2>4. Versioning</h2>
|
||
<p><span class="loc">§4 ¶1</span>OHM follows semantic versioning at the document level. Major bumps may break consumers; minor bumps add optional fields; patch bumps are clarifications.</p>
|
||
</div>
|
||
<div class="skip-region">▴ §1–4 unchanged on this PR · expand</div>
|
||
|
||
<div class="hunk">
|
||
<div class="hunk-header">
|
||
<span class="loc">§5 → §5 + §5a (split)</span>
|
||
<span class="prov">Claude · opus-4</span>
|
||
<span class="convo-link" data-open-tendril="t4">from anchored thread "section is too long"</span>
|
||
<span class="state accepted">Accepted on branch</span>
|
||
</div>
|
||
<div class="hunk-body">
|
||
<div class="line rem"><span class="gutter">−</span><span class="content">## 5. Migration from profile.yaml</span></div>
|
||
<div class="line rem"><span class="gutter">−</span><span class="content">The migration tool reads the legacy profile.yaml and emits a v1 OHM record…</span></div>
|
||
<div class="line add"><span class="gutter">+</span><span class="content">## 5. Migration: legacy → v1</span></div>
|
||
<div class="line add"><span class="gutter">+</span><span class="content">Migrating an existing <code>profile.yaml</code> happens once per contributor, on first sign-in after this RFC ships.</span></div>
|
||
<div class="line add"><span class="gutter">+</span><span class="content"></span></div>
|
||
<div class="line add"><span class="gutter">+</span><span class="content">## 5a. Migration tool reference</span></div>
|
||
<div class="line add"><span class="gutter">+</span><span class="content">The tool reads the legacy file, emits a v1 OHM record, and preserves unknown fields under a <code>legacy:</code> key.</span></div>
|
||
</div>
|
||
<div class="hunk-actions">
|
||
<span>Conversation resolved · approved by raj</span>
|
||
<span class="spacer"></span>
|
||
<button class="btn">View convo</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ctx">
|
||
<p><span class="loc">§5a ¶2</span>The tool runs once per contributor, idempotently. Re-running it on an account that's already migrated is a no-op.</p>
|
||
<p><span class="loc">§5a ¶3</span>Failure modes are noisy by design. A malformed legacy file aborts the migration with a clear error pointing at the offending line; nothing is half-migrated.</p>
|
||
<h2>6. Open questions</h2>
|
||
<p><span class="loc">§6 ¶1</span>Should the migration tool be runnable offline, or only at first sign-in? Leaving as future work.</p>
|
||
</div>
|
||
<div class="skip-region">▴ §5a ¶2–3 + §6 unchanged · expand</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<!-- ── Right pane ────────────────────────────────────────────────── -->
|
||
<aside class="right" id="right-pane" data-mode="list">
|
||
<div class="right-tabs">
|
||
<button class="active" data-rtab="convos">Conversations <span class="count">4</span></button>
|
||
<button data-rtab="changes">Changes <span class="count">5</span></button>
|
||
</div>
|
||
|
||
<div class="right-body">
|
||
<!-- Mode: tendril list -->
|
||
<div class="branch-thread">
|
||
<span class="icon">💬</span>
|
||
<span class="label">Branch chat</span>
|
||
<span class="meta">2 msgs · whole-doc context</span>
|
||
</div>
|
||
|
||
<div class="tendril-list">
|
||
<div class="tendril-item active" data-tendril="t1">
|
||
<div class="anchor">§3 ¶2 · 24 chars selected</div>
|
||
<div class="quote">"The canonical fields are name, handle, pronouns…"</div>
|
||
<div class="summary">"the name field is ambiguous — display label or stable id?" — Claude proposed renaming + adding established_at field.</div>
|
||
<div class="meta">
|
||
<span>4 msgs</span>
|
||
<span class="accepted">1 accepted</span>
|
||
<span>·</span>
|
||
<span>4h ago</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tendril-item" data-tendril="t2">
|
||
<div class="anchor">§4 ¶1 · 86 chars selected</div>
|
||
<div class="quote">"Major bumps may break consumers; minor bumps…"</div>
|
||
<div class="summary">"do we really need MAJOR/MINOR/PATCH for a schema this small?" — discussion led to a more explicit producer/consumer contract.</div>
|
||
<div class="meta">
|
||
<span>9 msgs</span>
|
||
<span class="pending">1 pending</span>
|
||
<span>·</span>
|
||
<span>3h ago</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tendril-item" data-tendril="t3">
|
||
<div class="anchor">§4 ¶3 · 0 chars (paragraph)</div>
|
||
<div class="summary">"how does a downstream tool know there's a migration to run?" — Claude proposed naming convention for migration scripts.</div>
|
||
<div class="meta">
|
||
<span>3 msgs</span>
|
||
<span class="pending">1 pending</span>
|
||
<span>·</span>
|
||
<span>1h ago</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tendril-item" data-tendril="t5">
|
||
<div class="anchor">§5 ¶1 · 42 chars selected</div>
|
||
<div class="summary">"should we ever delete the legacy file?" — Claude proposed destructive migration; declined by ben.</div>
|
||
<div class="meta">
|
||
<span>5 msgs</span>
|
||
<span style="color:var(--ink-4)">1 declined</span>
|
||
<span>·</span>
|
||
<span>2h ago</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mode: convo detail -->
|
||
<div class="convo-detail">
|
||
<div class="convo-header">
|
||
<button class="back" onclick="setRightMode('list')">← All conversations (4)</button>
|
||
<div class="anchor">§3 ¶2 — anchored to selection</div>
|
||
<div class="quote">"The canonical fields are name, handle, pronouns…"</div>
|
||
</div>
|
||
<div class="convo-messages">
|
||
<div class="msg user">
|
||
<div class="who"><span class="dot"></span>ben</div>
|
||
<div class="bubble">the name field here is ambiguous — is it the display label or the stable identifier? I keep tripping over it when writing downstream code.</div>
|
||
</div>
|
||
<div class="msg ai">
|
||
<div class="who"><span class="dot"></span>Claude <span class="model">sonnet-4.5</span></div>
|
||
<div class="bubble">Good catch — it's currently overloaded. Two clean options: (1) rename <code>name</code> → <code>display_name</code> and keep <code>handle</code> as the immutable id, or (2) add a separate <code>display_name</code> alongside <code>name</code> and deprecate <code>name</code>. Option 1 is cleaner since <code>name</code> is already unstable in practice.</div>
|
||
</div>
|
||
<div class="msg user">
|
||
<div class="who"><span class="dot"></span>ben</div>
|
||
<div class="bubble">go with (1). also — we never wrote down "when did this OHM start existing." add an <code>established_at</code>.</div>
|
||
</div>
|
||
<div class="msg ai">
|
||
<div class="who"><span class="dot"></span>Claude <span class="model">sonnet-4.5</span></div>
|
||
<div class="bubble">Done. Proposed two coupled edits to §3 ¶2 — the rename plus the new field, with a one-line note explaining the rename.</div>
|
||
<span class="change-chip accepted">✓ 1 change accepted · §3 ¶2</span>
|
||
</div>
|
||
</div>
|
||
<div class="compose">
|
||
<textarea placeholder="Continue this conversation (anchored to §3 ¶2)…"></textarea>
|
||
<div class="compose-row">
|
||
<span class="model-pill">sonnet-4.5</span>
|
||
<span class="spacer"></span>
|
||
<button class="btn">Propose more changes</button>
|
||
<button class="btn primary">Send</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ── State switching ───────────────────────────────────────────────────
|
||
const stateBtns = document.querySelectorAll('.state-btn');
|
||
const panels = document.querySelectorAll('[data-state]');
|
||
const right = document.getElementById('right-pane');
|
||
const crumbRef = document.getElementById('crumb-ref');
|
||
const crumbSep = document.getElementById('crumb-sep');
|
||
|
||
const REF_LABELS = {
|
||
'root': '',
|
||
'branch-diff': '⎇ ben/clarify-graduation',
|
||
'branch-edit': '⎇ ben/clarify-graduation',
|
||
'pr': '⇪ PR #4 — split §5',
|
||
};
|
||
|
||
function setState(target) {
|
||
panels.forEach(p => p.classList.toggle('active', p.dataset.state === target));
|
||
stateBtns.forEach(b => b.classList.toggle('active', b.dataset.target === target));
|
||
const ref = REF_LABELS[target] || '';
|
||
crumbRef.textContent = ref;
|
||
crumbSep.style.display = ref ? '' : 'none';
|
||
// Reset right pane to list mode when state changes
|
||
setRightMode('list');
|
||
}
|
||
|
||
stateBtns.forEach(b => b.addEventListener('click', () => setState(b.dataset.target)));
|
||
|
||
// ── View toggle (Diff <-> Edit on branch) ─────────────────────────────
|
||
document.querySelectorAll('[data-toggle]').forEach(b => {
|
||
b.addEventListener('click', () => {
|
||
const t = b.dataset.toggle;
|
||
if (t === 'pr-edit-disabled') return;
|
||
setState(t);
|
||
});
|
||
});
|
||
|
||
// ── Tree-row jump links ───────────────────────────────────────────────
|
||
document.querySelectorAll('[data-jump]').forEach(el => {
|
||
el.addEventListener('click', e => { e.preventDefault(); setState(el.dataset.jump); });
|
||
});
|
||
|
||
// ── Scope toggle (full doc ↔ changes only, inside a diff body) ────────
|
||
document.querySelectorAll('.scope-toggle').forEach(t => {
|
||
const targetId = t.dataset.scopeTarget;
|
||
const target = document.getElementById(targetId);
|
||
t.querySelectorAll('button').forEach(b => {
|
||
b.addEventListener('click', () => {
|
||
t.querySelectorAll('button').forEach(x => x.classList.toggle('active', x === b));
|
||
if (target) target.classList.toggle('changes-only', b.dataset.scope === 'changes');
|
||
});
|
||
});
|
||
});
|
||
|
||
// ── Tendril open ──────────────────────────────────────────────────────
|
||
function setRightMode(mode) { right.dataset.mode = mode; }
|
||
document.querySelectorAll('[data-open-tendril]').forEach(el => {
|
||
el.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
setRightMode('convo');
|
||
});
|
||
});
|
||
|
||
// Init
|
||
setState('branch-diff');
|
||
</script>
|
||
</body>
|
||
</html>
|