docs: architecture class diagrams + keep-updated-in-PRs workflow (#40)

architecture/ holds mermaid class diagrams of the bdrive web server: the
Server core with its seams (Source, AuthProvider, Directory, QuotaProvider,
remote.Backend) and the MetaStore persistence layer.

Convention (CLAUDE.md): a PR that changes the drawn structure updates the
affected diagram in the same branch and embeds only the changed diagrams'
mermaid blocks in the PR description. A PreToolUse hook on gh pr create
(.claude/hooks/check-arch-diagrams.sh) blocks PR creation when
internal/webapp or internal/remote changed but architecture/ didn't;
override with '# skip-diagram-check' when nothing structural changed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Snow W. Lee (Sungwon)
2026-07-22 15:23:18 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 34afef17cc
commit 2b9beaf41a
5 changed files with 342 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh
# PreToolUse(Bash) hook: block `gh pr create` when server code changed but
# architecture/ didn't. Heuristic only — a pure bug fix changes no
# structure; override by appending `# skip-diagram-check` to the command.
input=$(cat)
cmd=$(printf '%s' "$input" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))' 2>/dev/null)
case "$cmd" in
*"gh pr create"*) ;;
*) exit 0 ;;
esac
case "$cmd" in
*skip-diagram-check*) exit 0 ;;
esac
base=$(git merge-base origin/main HEAD 2>/dev/null || git merge-base main HEAD 2>/dev/null) || exit 0
code=$(git diff --name-only "$base" HEAD -- 'internal/webapp/*.go' 'internal/remote/*.go')
diag=$(git diff --name-only "$base" HEAD -- architecture/)
if [ -n "$code" ] && [ -z "$diag" ]; then
cat >&2 <<EOF
This branch changes server code but architecture/ is untouched:
$code
Before creating the PR: if any of these change types or relationships drawn
in architecture/, update the affected diagram, commit it, and embed the
changed diagram(s) in the PR body. If nothing structural changed, re-run the
same command with '# skip-diagram-check' appended.
EOF
exit 2
fi
exit 0
+13
View File
@@ -10,5 +10,18 @@
"Bash(go doc:*)",
"Bash(go env:*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-arch-diagrams.sh"
}
]
}
]
}
}
+4
View File
@@ -73,6 +73,10 @@ The real coverage is the integration tests in `internal/syncer/syncer_test.go`:
`web/docs/` is the public documentation at docs.beardrive.ai — Astro 7 + [Starlight](https://starlight.astro.build) (Starlight requires Astro ^7; the cloud landing is still on Astro 5 — separate projects, so they upgrade independently), static output, Pagefind search, `llms.txt` via `starlight-llms-txt`. Unlike the hub frontend (`internal/webapp/static`) and the cloud landing page (`cloud/internal/landing/dist`) it is **not** `go:embed`ed: docs change far more often than the binary, and a search index has no business shipping in every self-hoster's install. It deploys on its own from `dist/`, so `go build` never touches it. Sidebar order is explicit in `astro.config.mjs` — a new page under `src/content/docs/` is invisible until listed there, and every page needs a `description` (meta description, search snippet, and `llms.txt` line). **The sidebar order is the recommended path, and that path is agent-first**: `Start here` (what it is → set up with your agent → your first hour) never mentions installing a binary; the CLI route lives in `Manual setup (optional)` (install the CLI, set up by hand, skills and hooks in detail) — same destination, one click away, never on the critical path. New onboarding content belongs in `Start here` and should say what to ask an agent, not what to type. `Use cases` (after `Manual setup`) holds job-shaped pages — "Share work across your team's agents", "Turn a personal brain into a company brain" — with the persona named in the first line and the `description`, not in the title; they route into the guides and never re-teach a feature. Moved URLs keep `redirects` entries in `astro.config.mjs` (static builds emit meta-refresh; real 301s live in the host config, see `web/docs/README.md`). **Guides are agent-workflow docs, not CLI tutorials** (`Working with agents`: shared agent memory, artifacts/links, read heat, scoping); command-by-command detail belongs in `reference/cli.md`. Design tokens are **generated, never copied**: `scripts/tokens.mjs` reads the `@theme` block in `internal/webapp/frontend/src/tw.css` and emits the gitignored `src/styles/tokens.gen.css`, which `src/styles/custom.css` maps onto Starlight's `--sl-color-*` — so the palette cannot drift and there is no checker to maintain (contrast the cloud landing, which keeps a copy policed by its own `check-tokens.mjs`). Because the build reads that file *outside* `web/docs`, a deploy host must check out the whole repo, not the subdirectory. Note `llms.txt` convention wants the root domain, so `beardrive.ai/llms.txt` should point at the docs subdomain — that redirect lives in the cloud landing and is the one cross-repo coordination point.
## Architecture diagrams in PRs
`architecture/` holds mermaid class diagrams of the server (`webapp-server.md`). Before `gh pr create`: if the branch changes types or relationships drawn there (new/removed types, new seams, changed fields/implements/ownership in `internal/webapp` or `internal/remote`), update the affected diagram and commit it on the branch, then include ONLY the changed diagrams' mermaid blocks in the PR description under an "Architecture changes" section (GitHub renders them). No structural change → no section, and append `# skip-diagram-check` to the `gh pr create` command to satisfy the pre-PR hook.
## Docs to keep in sync
- `README.md` and `plugin/skills/beardrive/SKILL.md` both document CLI behavior, flags, output formats, and the on-disk layout. When changing CLI commands, flags, output, or layout, update both — the skill is what makes Claude Code beardrive-aware for end users and must match the actual binary.
+13
View File
@@ -0,0 +1,13 @@
# Architecture diagrams
Mermaid diagrams of the current implementation, kept next to the code so PRs
can update them alongside the change.
**Convention:** when a PR changes the structure drawn here (new/removed types,
new seams, changed relationships), update the affected diagram in the same PR
and embed the changed diagrams' mermaid blocks in the PR description under an
"Architecture changes" section, so reviewers see the structural delta. A
pre-PR hook (`.claude/hooks/check-arch-diagrams.sh`) reminds Claude Code
sessions when server code changed but no diagram did.
- [webapp-server.md](webapp-server.md) — class diagram of the `bdrive web` server (`internal/webapp` + its `internal/remote` seam)
+283
View File
@@ -0,0 +1,283 @@
# `bdrive web` server — class diagram
Source of truth: `internal/webapp` (server, services, persistence) and
`internal/remote` (storage backends). Reflects the code as of this commit;
update this file in any PR that changes these types or their relationships.
## Server core, sources, and services
```mermaid
classDiagram
direction LR
class Server {
+Source Source
+Volume string
+Root remote.Backend
+Projects *ProjectDB
+Device Identity
+Refresh time.Duration
+Upload UploadConfig
+Auth AuthProvider
+Devices *DeviceRegistry
+Shares *ShareDB
+Reads *ReadLedger
+Dir Directory
+Quota QuotaProvider
+ShareRPM int
-vols per-project volume cache
+Handler() http.Handler
}
class volume {
-source Source
-refresh time.Duration
-snap *snapshot
+snapshot(ctx)
+invalidate()
}
class Source {
<<interface>>
+Files(ctx) map path→FileInfo
+Open(ctx, path, fi) io.ReadCloser
}
class DirSource {
+Dir string
}
class RemoteSource {
+Backend remote.Backend
+Device Identity
}
class Uploader {
<<interface>>
+Upload(ctx, path, r, size, who)
}
class DirectUploader {
<<interface>>
+SignBlobPut(ctx, blob, size, ttl)
+HasBlob(ctx, blob)
+Commit(ctx, path, blob, size, who)
}
class Backend {
<<interface>>
+Put +Get +List +Exists +Close
}
class PutSigner {
<<interface>>
+SignPut(ctx, key, size, ttl)
}
note for Backend "internal/remote — impls: localBackend (file://), s3Backend, gcsBackend, httpBackend (https:// hub), Prefixed wrapper"
class AuthProvider {
<<interface>>
+CLILoginPath()
+Authenticate(r) User
+Register(mux)
+Accounts() []User
}
class AccountApprover {
<<interface>>
+PendingUsers() +Approve +Deny +SetPolicy +Policy
}
class BuiltinAuth {
+AllowSignup bool
+AllowedDomains
+RequireVerification bool
+RequireApproval bool
+Admins
+InviteValid func(token)
-store AccountRepo
-users, tokens, pending
}
class Mailer
class User {
+ID +Email +Name +Admin
}
class Directory {
<<interface>>
+Role(org, email)
+Get +OrgsFor +ListInvites +ValidInvite +ManageURL
+Create +Rename +AddMember +SetRole +RemoveMember
+CreateInvite +RevokeInvite +Redeem
}
class LocalDirectory {
+ManageURL(orgID)
}
class OrgDB {
-repo OrgRepo
-byID, invites
}
class Org {
+ID +Name +Members email→role +Created
}
class OrgInvite {
+Token +Org +Creator +Expires +Uses
}
class ProjectDB {
-repo ProjectRepo
-byID
+Get +Create +Rename +List
}
class Project {
+ID +Name +Org +Created
}
class ShareDB {
-repo ShareRepo
-byToken
+Create +Get +Revoke
}
class Share {
+Token +Project +Path +Creator +Expires
}
class DeviceRegistry {
-repo DeviceRepo
-byID
+Observe(DeviceInfo)
}
class DeviceInfo {
+ID +Name +OS +User +IP +LastSeen
}
class ReadLedger {
-repo ReadRepo
-retention
-byKey, dirty, seen
+Record(...)
+Heat(project, prefix, days)
}
class ReadStat {
+Project +Path +Day +Kind +Actor +Count +Last
}
class HeatEntry {
+Human +Agent +Share +Readers +LastRead
}
class QuotaProvider {
<<interface>>
+CheckWrite(org, bytes)
+CheckSeat(org, members)
+RecordUsage(org, bytes)
}
class UnlimitedQuota
Server o-- "0..1" Source : single-volume mode
Server o-- "0..1" Backend : Root (hub mode)
Server o-- ProjectDB
Server o-- AuthProvider
Server o-- Directory
Server o-- DeviceRegistry
Server o-- ShareDB
Server o-- ReadLedger
Server o-- QuotaProvider
Server *-- volume : per project, cached
volume o-- Source
Source <|.. DirSource
Source <|.. RemoteSource
Uploader <|-- DirectUploader
DirectUploader <|.. RemoteSource
RemoteSource o-- Backend : Prefixed(Root, projectID)
Backend <|-- PutSigner : optional capability
AuthProvider <|.. BuiltinAuth
AccountApprover <|.. BuiltinAuth
BuiltinAuth o-- Mailer : nil → log links
AuthProvider ..> User
Directory <|.. LocalDirectory
LocalDirectory *-- OrgDB : embeds
OrgDB ..> Org
OrgDB ..> OrgInvite
BuiltinAuth ..> OrgDB : InviteValid wiring
ProjectDB ..> Project
ShareDB ..> Share
DeviceRegistry ..> DeviceInfo
ReadLedger ..> ReadStat
ReadLedger ..> HeatEntry
QuotaProvider <|.. UnlimitedQuota
```
## Metadata persistence (`MetaStore`)
Service structs keep in-memory maps + logic; every change persists as one
record through a typed repo. Blobs and journals never touch this layer.
```mermaid
classDiagram
direction LR
class MetaStore {
<<interface>>
+Accounts() AccountRepo
+Projects() ProjectRepo
+Orgs() OrgRepo
+Shares() ShareRepo
+Devices() DeviceRepo
+Reads() ReadRepo
+Close()
}
class AccountRepo {
<<interface>>
+Load() +PutAccount +DeleteAccount +PutToken +DeleteToken +PutPolicy
}
class ProjectRepo {
<<interface>>
+Load() +Put +Delete
}
class OrgRepo {
<<interface>>
+Load() +PutOrg +DeleteOrg +PutInvite +DeleteInvite
}
class ShareRepo {
<<interface>>
+Load() +Put +Delete
}
class DeviceRepo {
<<interface>>
+Load() +Put
}
class ReadRepo {
<<interface>>
+Load() +PutBatch +DeleteBatch
}
note for ReadRepo "batch-oriented: one flush = one write"
class fileMetaStore {
JSON files, atomic rewrite per change
}
class sqlMetaStore {
one database/sql impl
sqlite (modernc) or postgres (pgx)
}
MetaStore <|.. fileMetaStore
MetaStore <|.. sqlMetaStore
MetaStore *-- AccountRepo
MetaStore *-- ProjectRepo
MetaStore *-- OrgRepo
MetaStore *-- ShareRepo
MetaStore *-- DeviceRepo
MetaStore *-- ReadRepo
class BuiltinAuth
class ProjectDB
class OrgDB
class ShareDB
class DeviceRegistry
class ReadLedger
BuiltinAuth o-- AccountRepo
ProjectDB o-- ProjectRepo
OrgDB o-- OrgRepo
ShareDB o-- ShareRepo
DeviceRegistry o-- DeviceRepo
ReadLedger o-- ReadRepo
```