syntax = "proto3";

package heddle.v1;

import "google/protobuf/timestamp.proto";
import "heddle/v1/common.proto";
import "heddle/v1/policies.proto";
import "heddle/v1/support.proto";
import "heddle/v1/threads.proto";

service HostedUserService {
  rpc GetCurrentUserNamespace(GetCurrentUserNamespaceRequest) returns (HostedNamespace);
  rpc CreateNamespace(CreateNamespaceRequest) returns (HostedNamespace);
  // DEPRECATED: use ListSpools (weft#394)
  rpc ListNamespaces(ListNamespacesRequest) returns (ListNamespacesResponse) {
    option deprecated = true;
  }
  rpc UpdateNamespace(UpdateNamespaceRequest) returns (HostedNamespace);
  rpc SetNamespaceVisibility(SetNamespaceVisibilityRequest) returns (HostedNamespace);
  rpc DeleteNamespace(DeleteNamespaceRequest) returns (DeleteResponse);
  rpc CreateRepository(CreateRepositoryRequest) returns (HostedRepository);
  rpc ListSpools(ListSpoolsRequest) returns (ListSpoolsResponse);
  // DEPRECATED: use ListSpools (weft#394)
  rpc ListRepositories(ListRepositoriesRequest) returns (ListRepositoriesResponse) {
    option deprecated = true;
  }
  rpc UpdateRepository(UpdateRepositoryRequest) returns (HostedRepository);
  rpc DeleteRepository(DeleteRepositoryRequest) returns (DeleteResponse);
  rpc CreateGrant(CreateGrantRequest) returns (HostedGrant);
  rpc ListGrants(ListGrantsRequest) returns (ListGrantsResponse);
  rpc UpdateGrant(UpdateGrantRequest) returns (HostedGrant);
  rpc DeleteGrant(DeleteGrantRequest) returns (DeleteResponse);

  // Resolve opaque subjects (from grants/annotations) to human display
  // info. Today only WhoAmI exists (current session); this is the batch
  // subject-resolution surface the grant/member UIs need so they can render
  // a name + handle instead of the raw subject. Backend impl lives in weft
  // (a follow-up); this is the contract only.
  rpc ResolveSubjects(ResolveSubjectsRequest) returns (ResolveSubjectsResponse);

  // ── Thread merge approvals ───────────────────────────────────────
  rpc ApproveThread(ApproveThreadRequest) returns (ThreadApproval);
  rpc RevokeApproval(RevokeApprovalRequest) returns (DeleteResponse);
  rpc ListThreadApprovals(ListThreadApprovalsRequest) returns (ListThreadApprovalsResponse);
  rpc CheckMergeEligibility(CheckMergeEligibilityRequest) returns (CheckMergeEligibilityResponse);

  // ── Namespace-scoped invitations and members (formerly admin) ────
  rpc CreateInvitation(CreateInvitationRequest) returns (Invitation);
  rpc ListInvitations(ListInvitationsRequest) returns (ListInvitationsResponse);
  rpc RevokeInvitation(RevokeInvitationRequest) returns (DeleteResponse);
  rpc ListMembers(ListMembersRequest) returns (ListMembersResponse);

  // ── Approval groups (reusable approver pools) ────────────────────
  // Admin role on the repo's parent namespace required to create or
  // mutate; Reader is enough to list.
  rpc CreateApprovalGroup(CreateApprovalGroupRequest) returns (ApprovalGroup);
  rpc ListApprovalGroups(ListApprovalGroupsRequest) returns (ListApprovalGroupsResponse);
  rpc DeleteApprovalGroup(DeleteApprovalGroupRequest) returns (DeleteResponse);
  rpc AddApprovalGroupMember(AddApprovalGroupMemberRequest) returns (ApprovalGroupMember);
  rpc RemoveApprovalGroupMember(RemoveApprovalGroupMemberRequest) returns (DeleteResponse);

  // ── Thread policies (branch protection) ──────────────────────────
  rpc CreateThreadPolicy(CreateThreadPolicyRequest) returns (ThreadPolicy);
  rpc ListThreadPolicies(ListThreadPoliciesRequest) returns (ListThreadPoliciesResponse);
  rpc DeleteThreadPolicy(DeleteThreadPolicyRequest) returns (DeleteResponse);
  rpc AddPolicyGroupRequirement(AddPolicyGroupRequirementRequest) returns (PolicyGroupRequirement);
  rpc RemovePolicyGroupRequirement(RemovePolicyGroupRequirementRequest) returns (DeleteResponse);

  // ── Operator-only listings (gate on staff scope) ─────────────────
  rpc ListActors(ListActorsRequest) returns (ListActorsResponse);
  rpc ListWorktrees(ListWorktreesRequest) returns (ListWorktreesResponse);

  // ── Support access — customer-issued temporary admin grants for
  // Heddle staff. The grantor must hold Admin on the namespace/repo;
  // the grantee is a staff email. The server materialises a real
  // `hosted_grants` row plus a `support_access_audit` row, both
  // bounded by the supplied TTL.
  rpc GrantSupportAccess(GrantSupportAccessRequest) returns (SupportAccessGrant);
  rpc ListSupportAccessGrants(ListSupportAccessGrantsRequest) returns (ListSupportAccessGrantsResponse);
  rpc RevokeSupportAccess(RevokeSupportAccessRequest) returns (DeleteResponse);

  // ── Caller-owned bookmarks ───────────────────────────────────────
  // Accepts both user and anonymous biscuits. Anonymous callers can
  // only write their own bookmark rows; promotion migrates those rows
  // to the eventual user account without granting repo/namespace write
  // permissions.
  rpc ListBookmarks(ListBookmarksRequest) returns (ListBookmarksResponse);
  rpc UpsertBookmark(UpsertBookmarkRequest) returns (Bookmark);
  rpc DeleteBookmark(DeleteBookmarkRequest) returns (DeleteResponse);

  // --- Spool child edges + monorepo resolution + facet history ---
  // (Spool epic P8a, weft#358). ADDITIVE: exposes the P4-P7 spool
  // capabilities. Handlers land in weft P8b against weft's registry methods
  // (children::{attach_child_tx,detach_child_tx,list_children,resolve_monorepo}
  // and governance/membership {current_*,*_history}); the messages below mirror
  // those return types so P8b maps 1:1.

  // Attach a child spool under `parent_path` at `mount_name`, anchored at the
  // child's current content head. Mutates only the parent's children edge-set
  // (refs/spool/children/edges), never the parent's Content facet. Mirrors
  // weft `children::attach_child_tx` -> ChildEdge.
  rpc AttachChild(AttachChildRequest) returns (ChildEdge);
  // Detach the child mounted at `mount_name` under `parent_path`. Mirrors weft
  // `children::detach_child_tx` (bool removed).
  rpc DetachChild(DetachChildRequest) returns (DetachChildResponse);
  // List the child edges of `parent_path`, each resolved with its anchored
  // state + moving-anchored-ff status. Read-only; moves no edge. Mirrors weft
  // `children::list_children` -> Vec<ChildEdge>.
  rpc ListChildren(ListChildrenRequest) returns (ListChildrenResponse);
  // Recursively resolve the monorepo rooted at `root_path` into the caller's
  // coherent visible slice (per-child visibility, cycle guard, depth bound).
  // Mirrors weft `children::resolve_monorepo` -> MonorepoNode.
  rpc ResolveMonorepo(ResolveMonorepoRequest) returns (MonorepoNode);
  // Walk `spool_path`'s governance facet history newest-first (first-parent).
  // Mirrors weft `governance::governance_history` -> Vec<GovernanceHistoryEntry>.
  rpc GetGovernanceHistory(GetGovernanceHistoryRequest) returns (GetGovernanceHistoryResponse);
  // Walk `spool_path`'s membership facet history newest-first (first-parent).
  // Mirrors weft `membership::membership_history` -> Vec<MembershipHistoryEntry>.
  rpc GetMembershipHistory(GetMembershipHistoryRequest) returns (GetMembershipHistoryResponse);
}

// Namespace kinds in the hosted control plane. `USER` namespaces are
// per-account and auto-created at first sign-in; `ORG` and `TEAM` are
// explicit organisational containers.
enum NamespaceKind {
  NAMESPACE_KIND_UNSPECIFIED = 0;
  NAMESPACE_KIND_USER = 1;
  NAMESPACE_KIND_ORG = 2;
  NAMESPACE_KIND_TEAM = 3;
}

enum SpoolVisibility {
  SPOOL_VISIBILITY_UNSPECIFIED = 0;
  SPOOL_VISIBILITY_PRIVATE = 1;
  SPOOL_VISIBILITY_PUBLIC = 2;
}

enum SpoolStateVisibility {
  SPOOL_STATE_VISIBILITY_UNSPECIFIED = 0;
  SPOOL_STATE_VISIBILITY_PUBLIC = 1;
  SPOOL_STATE_VISIBILITY_INTERNAL = 2;
  SPOOL_STATE_VISIBILITY_PRIVATE = 3;
}

enum SpoolBootstrapKind {
  SPOOL_BOOTSTRAP_KIND_UNSPECIFIED = 0;
  SPOOL_BOOTSTRAP_KIND_EMPTY = 1;
  SPOOL_BOOTSTRAP_KIND_GIT_REMOTE = 2;
  SPOOL_BOOTSTRAP_KIND_TEMPLATE = 3;
}

enum SpoolBootstrapSyncDirection {
  SPOOL_BOOTSTRAP_SYNC_DIRECTION_UNSPECIFIED = 0;
  SPOOL_BOOTSTRAP_SYNC_DIRECTION_PULL = 1;
  SPOOL_BOOTSTRAP_SYNC_DIRECTION_PUSH = 2;
  SPOOL_BOOTSTRAP_SYNC_DIRECTION_BIDI = 3;
}

enum SpoolWritePolicy {
  SPOOL_WRITE_POLICY_UNSPECIFIED = 0;
  SPOOL_WRITE_POLICY_DEVELOPERS = 1;
  SPOOL_WRITE_POLICY_MAINTAINERS = 2;
  SPOOL_WRITE_POLICY_OWNER_ONLY = 3;
}

enum SpoolChildPolicy {
  SPOOL_CHILD_POLICY_UNSPECIFIED = 0;
  SPOOL_CHILD_POLICY_MAINTAINERS = 1;
  SPOOL_CHILD_POLICY_DEVELOPERS = 2;
  SPOOL_CHILD_POLICY_DISABLED = 3;
}

enum SpoolSyncBehavior {
  SPOOL_SYNC_BEHAVIOR_UNSPECIFIED = 0;
  SPOOL_SYNC_BEHAVIOR_MANUAL = 1;
  SPOOL_SYNC_BEHAVIOR_HEDDLE_START = 2;
  SPOOL_SYNC_BEHAVIOR_HEDDLE_START_WITH_PRESENCE = 3;
}

message SpoolInitialTooling {
  bool readme = 1;
  string license = 2;
  string gitignore = 3;
  string language_preset = 4;
}

message SpoolSettings {
  SpoolVisibility visibility = 1;
  SpoolStateVisibility default_state_visibility = 2;
  SpoolBootstrapKind bootstrap_kind = 3;
  string bootstrap_source = 4;
  SpoolWritePolicy write_policy = 5;
  SpoolChildPolicy child_policy = 6;
  SpoolInitialTooling initial_tooling = 7;
  SpoolSyncBehavior sync_behavior = 8;
  SpoolBootstrapSyncDirection bootstrap_sync_direction = 9;
  string description = 10;
}

message ListNamespacesRequest {}

message GetCurrentUserNamespaceRequest {}

message CreateNamespaceRequest {
  NamespaceKind kind = 1;
  string slug = 2;
  string parent_path = 3;
  string display_name = 4;
  SpoolSettings settings = 5;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message UpdateNamespaceRequest {
  string full_path = 1;
  string new_slug = 2;
  string display_name = 3;
  bool clear_display_name = 4;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message SetNamespaceVisibilityRequest {
  string full_path = 1;
  SpoolVisibility visibility = 2;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message DeleteNamespaceRequest {
  string full_path = 1;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message ListRepositoriesRequest {
  string namespace_path = 1;
}

message ListSpoolsRequest {
  bool repos_only = 1; // when true, return only content-bearing (repo) spools
}

message CreateRepositoryRequest {
  string namespace_path = 1;
  string slug = 2;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message UpdateRepositoryRequest {
  string full_path = 1;
  string new_slug = 2;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message DeleteRepositoryRequest {
  string full_path = 1;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message ListGrantsRequest {
  string resource = 1;
}

message CreateGrantRequest {
  string subject = 1;
  HostedRole role = 2;
  GrantTargetRef target = 3;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message UpdateGrantRequest {
  string subject = 1;
  HostedRole role = 2;
  GrantTargetRef target = 3;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message DeleteGrantRequest {
  string subject = 1;
  GrantTargetRef target = 2;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message ListActorsRequest {}

message ListWorktreesRequest {}

message HostedNamespace {
  string namespace_id = 1;
  NamespaceKind kind = 2;
  string slug = 3;
  string full_path = 4;
  string parent_id = 5;
  string display_name = 6;
  SpoolSettings settings = 7;
  // Current effective visibility of the namespace's spool. Mirrors
  // `settings.visibility` for top-level reads; populated on every response
  // builder so SetNamespaceVisibility callers see the applied value.
  SpoolVisibility visibility = 8;
}

message HostedRepository {
  string repo_id = 1;
  string namespace_id = 2;
  string slug = 3;
  string path = 4;
  string full_path = 5;
  // Unset when the serving registry has no activity data; populated by weft
  // from the refs/state tables (weft wiring is a separate weft-side change).
  google.protobuf.Timestamp last_activity_at = 6;
}

message SpoolSummary {
  string spool_id = 1;
  string full_path = 2;
  string kind = 3; // UX label only: "namespace" | "repository"/"project" — NEVER structural
  bool is_repo = 4; // capability: content-bearing spool (pushable/pullable)
}

message Bookmark {
  string id = 1;
  // "repo" | "namespace" for the current web UI. Kept as a string so
  // future bookmarkable resource kinds can ship without enum churn.
  string resource_kind = 2;
  string full_path = 3;
  string name = 4;
  string folder = 5;
  google.protobuf.Timestamp created_at = 6;
  google.protobuf.Timestamp updated_at = 7;
}

message ListBookmarksRequest {}

message ListBookmarksResponse {
  repeated Bookmark bookmarks = 1;
}

message UpsertBookmarkRequest {
  string resource_kind = 1;
  string full_path = 2;
  string name = 3;
  string folder = 4;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message DeleteBookmarkRequest {
  string resource_kind = 1;
  string full_path = 2;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message HostedGrant {
  string subject = 1;
  HostedRole role = 2;
  GrantTargetRef target = 3;
}

message ActorSummary {
  string session_id = 1;
  string thread = 2;
  optional string path = 3;
  bytes base_state = 4;
  google.protobuf.Timestamp started_at = 5;
  optional string provider = 6;
  optional string model = 7;
  string status = 8;
  optional string heddle_session_id = 9;
  optional string harness = 10;
  optional string thinking_level = 11;
  UsageSummary usage_summary = 12;
  optional google.protobuf.Timestamp last_progress_at = 13;
  optional string report_flush_state = 14;
  optional string client_instance_id = 15;
  optional string attach_reason = 16;
  optional string native_actor_key = 17;
  optional string native_parent_actor_key = 18;
  optional string probe_source = 19;
  optional float probe_confidence = 20;
}

message WorktreeSummary {
  string id = 1;
  string name = 2;
  bytes base_state = 3;
  optional string thread = 4;
  uint32 changes = 5;
  optional string session_id = 6;
  google.protobuf.Timestamp created_at = 7;
  google.protobuf.Timestamp modified_at = 8;
  bool is_current = 9;
}

message ListActorsResponse {
  repeated ActorSummary actors = 1;
}

message ListWorktreesResponse {
  repeated WorktreeSummary worktrees = 1;
}

// --- Invitations (Track D) -------------------------------------------------

message Invitation {
  string id = 1;
  string email = 2;
  string namespace_path = 3;
  HostedRole role = 4;
  string invited_by = 5;
  google.protobuf.Timestamp created_at = 6; // unix seconds
  google.protobuf.Timestamp expires_at = 7; // 0 if no expiry
  google.protobuf.Timestamp accepted_at = 8; // 0 if pending
  string accepted_by = 9; // "" if pending
  string metadata = 10; // stringified JSON — small, avoids pulling in struct
}

message CreateInvitationRequest {
  string email = 1;
  string namespace_path = 2;
  HostedRole role = 3;
  google.protobuf.Timestamp expires_at = 4; // 0 = no expiry
  string metadata = 5; // optional caller-supplied JSON; pass "{}" if none

  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

message ListInvitationsRequest {
  string namespace_path = 1;
  bool include_accepted = 2; // default false = pending only
}

message ListInvitationsResponse {
  repeated Invitation invitations = 1;
}

message RevokeInvitationRequest {
  string id = 1;
  // Idempotency (audit-idempotency enforces tag 15).
  string client_operation_id = 15;
}

// Members = direct grants + pending invitations for a namespace.
message MemberEntry {
  // Exactly one of these is populated.
  oneof entry {
    Grant grant = 1;
    Invitation invitation = 2;
  }
  message Grant {
    string subject = 1;
    HostedRole role = 2;
    string resource_path = 3;
    string resource_kind = 4; // "namespace" or "repository"
  }
}

message ListMembersRequest {
  string namespace_path = 1;
  bool include_descendants = 2; // include grants on child namespaces + repos
}

message ListMembersResponse {
  repeated MemberEntry entries = 1;
}

message ListNamespacesResponse {
  repeated HostedNamespace namespaces = 1;
}

message ListRepositoriesResponse {
  repeated HostedRepository repositories = 1;
}

message ListSpoolsResponse {
  repeated SpoolSummary spools = 1;
}

message ListGrantsResponse {
  repeated HostedGrant grants = 1;
}

// ── Subject resolution ──────────────────────────────────────────────────────
// Batch subject→display-name resolution. The grant/member UIs hold opaque
// subjects (from HostedGrant.subject / MemberEntry.Grant.subject) and today
// render them raw; this lets a consumer enrich a whole grant/member list in a
// single round trip. Backend impl is a weft follow-up.
message ResolveSubjectsRequest {
  // Opaque subjects, e.g. "user:<uuid>" / "ops-alice".
  repeated string subjects = 1;
}

message ResolveSubjectsResponse {
  repeated ResolvedPrincipal principals = 1;
}

message ResolvedPrincipal {
  // Echo of the requested subject (the join key for the caller).
  string subject = 1;
  // Human display name; empty when unresolved.
  string display_name = 2;
  // Username / handle; empty when unresolved.
  string handle = 3;
  // False when the subject couldn't be resolved (display_name + handle empty).
  bool resolved = 4;
}

// ============================================================================
// Spool child edges + monorepo resolution + facet history (Spool epic P8a,
// weft#358). ADDITIVE. Message shapes mirror weft's registry return types so
// the P8b handler phase maps 1:1. State/change ids are `bytes` (16-byte
// ChangeId), matching the package-wide wire convention.
// ============================================================================

// Who committed a facet change (audit trail). Mirrors heddle's
// `objects::Attribution { principal, agent: Option<Agent> }` — a human
// principal plus an optional AI agent that performed the change on their
// behalf.
message SpoolPrincipal {
  string name = 1;
  string email = 2;
}

message SpoolAgent {
  string provider = 1;
  string model = 2;
}

message SpoolAttribution {
  SpoolPrincipal principal = 1;
  // Present when an AI agent performed the change on the principal's behalf.
  optional SpoolAgent agent = 2;
}

// Ff/divergence status of a child edge vs the child's latest content state
// (moving-anchored-ff). Mirrors weft `children::ChildEdgeStatus`.
enum ChildEdgeStatus {
  CHILD_EDGE_STATUS_UNSPECIFIED = 0;
  // The edge's anchor already equals the child's latest content state.
  CHILD_EDGE_STATUS_UP_TO_DATE = 1;
  // The child advanced linearly past the anchor (anchor is an ancestor of the
  // child's latest state): the edge can fast-forward to it.
  CHILD_EDGE_STATUS_FAST_FORWARDABLE = 2;
  // The child's history diverged from the anchor (non-ff): the edge does not
  // move; surfaced instead of silently advancing.
  CHILD_EDGE_STATUS_DIVERGED = 3;
  // The child has no content head yet (nothing to advance toward).
  CHILD_EDGE_STATUS_NO_CHILD_HEAD = 4;
}

// One resolved child edge. Mirrors weft `children::ChildEdge`.
message ChildEdge {
  string parent_spool_id = 1;
  string child_spool_id = 2;
  string mount_name = 3;
  // The child content state-id this edge is currently anchored to (16-byte
  // ChangeId).
  bytes anchored_state_id = 4;
  // The child's latest content-facet head, if any (absent = child has no
  // content head yet).
  optional bytes child_head = 5;
  // Moving-anchored-ff status of the anchor vs the child head.
  ChildEdgeStatus status = 6;
}

message AttachChildRequest {
  // The parent spool the child is mounted under.
  string parent_path = 1;
  // The child spool to attach.
  string child_path = 2;
  // The mount name of the child inside the parent (the SPOOLLINK entry name).
  string mount_name = 3;
}

message DetachChildRequest {
  string parent_path = 1;
  string mount_name = 2;
}

message DetachChildResponse {
  // True if an edge was present and removed; false if no such mount existed.
  // Mirrors weft `children::detach_child_tx` -> bool.
  bool removed = 1;
}

message ListChildrenRequest {
  string parent_path = 1;
}

message ListChildrenResponse {
  repeated ChildEdge children = 1;
}

// Why a child edge in a resolved monorepo was NOT descended into. A resolved
// tree is always the caller's coherent visible slice: an omitted child is
// recorded, never an error. Mirrors weft `children::EdgeSkip`.
enum EdgeSkip {
  EDGE_SKIP_UNSPECIFIED = 0;
  // The caller cannot read the child spool (per-child visibility chain-walk
  // failed): the edge is withheld from the caller's slice.
  EDGE_SKIP_UNREADABLE = 1;
  // The child was already visited on this walk (cycle guard, e.g. A->B->A):
  // recorded but not re-descended, so the walk terminates.
  EDGE_SKIP_CYCLE = 2;
  // The child sits at MONOREPO_MAX_DEPTH: recorded but not descended.
  EDGE_SKIP_DEPTH_BOUNDED = 3;
}

// One edge in a resolved monorepo tree: the anchored child + either the
// recursively-resolved subtree (descended) or the reason it was not descended.
// Mirrors weft `children::MonorepoEdge`.
message MonorepoEdge {
  // The mount name of this child inside its parent (the SPOOLLINK entry name).
  string mount_name = 1;
  // The child spool this edge points at.
  string child_spool_id = 2;
  // The child content state-id the edge is anchored to (moving-anchored-ff
  // resolved — the state the caller would materialize for this child).
  bytes anchored_state_id = 3;
  // The child's current content head + ff/diverged status vs the anchor
  // (reused from the ChildEdge resolution, not re-derived).
  optional bytes child_head = 4;
  ChildEdgeStatus status = 5;
  // The recursively-resolved child subtree when descended; absent (with a
  // `skipped` reason) when the edge was withheld/bounded/cycle-guarded.
  optional MonorepoNode subtree = 6;
  // The reason the edge was not descended; absent when `subtree` is present.
  optional EdgeSkip skipped = 7;
}

// One spool in a resolved monorepo tree: its content state at the resolved
// anchor plus its visible child edges. The caller reconstructs the whole
// monorepo by materializing each node's content at `content_state` and
// mounting each edge's subtree under its `mount_name`. Mirrors weft
// `children::MonorepoNode`.
message MonorepoNode {
  string spool_id = 1;
  // The content-facet state this spool is resolved at (absent = no content
  // head). For the ROOT this is its own content head; for a descendant it is
  // the anchored state the PARENT's edge pins.
  optional bytes content_state = 2;
  // The visible child edges of this spool (unreadable children are recorded
  // with EDGE_SKIP_UNREADABLE, not silently dropped).
  repeated MonorepoEdge edges = 3;
}

message ResolveMonorepoRequest {
  string root_path = 1;
  // Optional recursion depth bound; server clamps to MONOREPO_MAX_DEPTH.
  optional uint32 max_depth = 2;
}

// One entry in a spool's governance-facet history — a governance commit and
// the document it captured. Newest-first. Mirrors weft
// `governance::GovernanceHistoryEntry`.
message GovernanceHistoryEntry {
  // The governance-facet commit id (16-byte ChangeId).
  bytes change_id = 1;
  // Access-gating visibility (`public`/`private`) as of this state.
  string visibility = 2;
  // The full non-visibility governance knobs (write_policy, child_policy,
  // bootstrap_*, initial_tooling, description, ...), captured verbatim as a
  // JSON string (weft `GovernanceDocument.settings: serde_json::Value`).
  string settings_json = 3;
  // Who committed this governance change (audit trail).
  SpoolAttribution attribution = 4;
  // When the governance change was committed.
  google.protobuf.Timestamp committed_at = 5;
}

message GetGovernanceHistoryRequest {
  string spool_path = 1;
  // Max entries to walk (server default when unset/0).
  optional uint32 limit = 2;
}

message GetGovernanceHistoryResponse {
  repeated GovernanceHistoryEntry entries = 1;
}

// One grant record captured in a membership document — a `(subject, role)`
// pair keyed on the immutable `subject_user_id` UUID. Mirrors weft
// `membership::MembershipGrant`.
message MembershipGrant {
  // The immutable `users.id` UUID this grant is keyed on (the security key).
  string subject_user_id = 1;
  // The handle as it was at commit time (display/lookup only, not a key).
  string subject = 2;
  // The role held, as the on-wire lowercase token
  // (`reader`/`developer`/`maintainer`/`admin`/`owner`).
  string role = 3;
}

// One entry in a spool's membership-facet history — a membership commit and
// the document (full grant set) it captured. Newest-first. Mirrors weft
// `membership::MembershipHistoryEntry`.
message MembershipHistoryEntry {
  // The membership-facet commit id (16-byte ChangeId).
  bytes change_id = 1;
  // Every grant held on the spool as of this state (weft
  // `MembershipDocument.grants`).
  repeated MembershipGrant grants = 2;
  // Who committed this membership change (audit trail).
  SpoolAttribution attribution = 3;
  // When the membership change was committed.
  google.protobuf.Timestamp committed_at = 4;
}

message GetMembershipHistoryRequest {
  string spool_path = 1;
  // Max entries to walk (server default when unset/0).
  optional uint32 limit = 2;
}

message GetMembershipHistoryResponse {
  repeated MembershipHistoryEntry entries = 1;
}
