diff --git a/.claude/hooks/check-arch-diagrams.sh b/.claude/hooks/check-arch-diagrams.sh new file mode 100755 index 0000000..cad90e8 --- /dev/null +++ b/.claude/hooks/check-arch-diagrams.sh @@ -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 <> + +Files(ctx) map path→FileInfo + +Open(ctx, path, fi) io.ReadCloser + } + class DirSource { + +Dir string + } + class RemoteSource { + +Backend remote.Backend + +Device Identity + } + class Uploader { + <> + +Upload(ctx, path, r, size, who) + } + class DirectUploader { + <> + +SignBlobPut(ctx, blob, size, ttl) + +HasBlob(ctx, blob) + +Commit(ctx, path, blob, size, who) + } + + class Backend { + <> + +Put +Get +List +Exists +Close + } + class PutSigner { + <> + +SignPut(ctx, key, size, ttl) + } + note for Backend "internal/remote — impls: localBackend (file://), s3Backend, gcsBackend, httpBackend (https:// hub), Prefixed wrapper" + + class AuthProvider { + <> + +CLILoginPath() + +Authenticate(r) User + +Register(mux) + +Accounts() []User + } + class AccountApprover { + <> + +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 { + <> + +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 { + <> + +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 { + <> + +Accounts() AccountRepo + +Projects() ProjectRepo + +Orgs() OrgRepo + +Shares() ShareRepo + +Devices() DeviceRepo + +Reads() ReadRepo + +Close() + } + + class AccountRepo { + <> + +Load() +PutAccount +DeleteAccount +PutToken +DeleteToken +PutPolicy + } + class ProjectRepo { + <> + +Load() +Put +Delete + } + class OrgRepo { + <> + +Load() +PutOrg +DeleteOrg +PutInvite +DeleteInvite + } + class ShareRepo { + <> + +Load() +Put +Delete + } + class DeviceRepo { + <> + +Load() +Put + } + class ReadRepo { + <> + +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 +```