docs: restructure into topic subdirectories, collapse duplicated index

Per mara's go-ahead on hyperhive#3902 ("getting started is good, but
terminal rendering does not go in there i think"):

Moved 21 top-level docs/*.md files into 7 new topic subdirectories
(existing web-ui/, turn-loop/, swarm/, tools/, crates/ untouched):
  getting-started/  setup.md
  agent-lifecycle/  agent-hierarchy.md, approvals.md, persistence.md
  trust-boundary/   boundary.md, security.md
  integrations/     forge.md, matrix.md, github.md, knowledge.md
  networking/       gateway.md, network.md, snapshot-store.md
  scheduler/        jobq.md, coordinator.md, ci.md, observability.md
  process/          conventions.md, gotchas.md, pr-review-gate.md
  web-ui/           terminal-rendering.md (moved into the EXISTING dir,
                    per mara's correction to the original getting-started
                    guess -- it's UI implementation detail, not onboarding)

The physical layout now matches docs/README.md's own topical headers,
which already amounted to this taxonomy -- see the scoping comment on
the issue for the two findings that motivated this (a genuine
duplication between CLAUDE.md's old "Reading paths" list and
docs/README.md's grouped one, since drifted out of sync with each
other; and the flat layout not matching the grouping we already had).

Fixed every cross-reference this moved across the whole repo (~120
files: docs/ internal links at every depth, Rust doc comments, nix
module option docs, crate READMEs) -- verified two ways: a grep sweep
confirming zero remaining references to any old path, and a script
that resolves every markdown link in docs/**/*.md + CLAUDE.md +
README.md against the filesystem and reports anything that doesn't
exist (zero broken links).

Collapsed CLAUDE.md's "Reading paths" section (the duplicate) down to
a pointer at docs/README.md, now the single index. Rewrote
docs/README.md itself to use the new subdirectory paths and added the
one doc it was missing that CLAUDE.md's old copy had (pr-review-gate.md).

Classified all 22 docs/*.md files first via a haiku subagent (mara's
suggestion) on two axes -- proposed grouping and operator-vs-
implementation focus -- before finalizing the taxonomy; spot-checked
the report and found internal inconsistencies (its classification
table disagreed with its own summary section for a few files), so this
taxonomy is my original proposal + the one correction mara gave
directly, not a blind application of the subagent's table. The
operator-focus data it gathered is still useful for a follow-up
content pass (docs skewing 'mixed' rather than pure operator-facing),
not addressed in this PR -- structure only.

nix fmt clean, both pre-push lints clean.
This commit is contained in:
iris 2026-09-02 01:47:05 +02:00 committed by mara
commit 07b62612b0
124 changed files with 301 additions and 377 deletions

View file

@ -18,12 +18,12 @@
//! **Multi-source**: always the internal Forgejo, plus github.com when the
//! agent has a PAT. Each source polls independently behind
//! [`Source`]; everything below is shared. Rationale
//! + host differences: [`docs/forge.md::Sources`](../../../docs/forge.md).
//! + host differences: [`docs/integrations/forge.md::Sources`](../../../docs/integrations/forge.md).
//!
//! Activation gates, self-notification filtering, body excerpt +
//! truncation + heading escape, wrapper formats (comment / review /
//! new-item / state-change), meta suffix, and review-request override
//! all live in [`docs/forge.md::Notification poller`](../../../docs/forge.md).
//! all live in [`docs/integrations/forge.md::Notification poller`](../../../docs/integrations/forge.md).
use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
@ -63,7 +63,7 @@ pub const TOKEN_RETRY_MAX: u32 = 20;
/// a `new <kind>` label. Later activity that lands on the state-change
/// path because it carries no `latest_comment_url` (e.g. a bodiless
/// review submission) fires well outside this window, so we must not
/// claim it's "new" — see docs/forge.md, "new vs activity on".
/// claim it's "new" — see docs/integrations/forge.md, "new vs activity on".
const NEW_ITEM_TOLERANCE_SECS: i64 = 120;
/// Fetch the account's own login for self-notification filtering.
@ -111,7 +111,7 @@ fn notif_type_label(t: Option<NotifySubjectType>) -> &'static str {
/// Escape strict-ATX markdown headings in a body before embedding it
/// inside the forge-notify wrapper, so a leading `## title` line
/// doesn't blow into an h2 in the dashboard render. See
/// `docs/forge.md::Body excerpt + truncation + heading escape` for
/// `docs/integrations/forge.md::Body excerpt + truncation + heading escape` for
/// the strict-ATX-vs-`#tag` rationale and the `split_inclusive`
/// trailing-newline contract.
fn escape_md_headings(body: &str) -> String {
@ -196,7 +196,7 @@ fn is_username_byte(b: u8) -> bool {
/// `@username` mention AND aren't already present (as a substring) in
/// `included_excerpt`. Surfaces tags that fell outside the truncation
/// window so addressed agents never silently miss a mention on a long
/// body. See `docs/forge.md::Body excerpt + truncation + heading
/// body. See `docs/integrations/forge.md::Body excerpt + truncation + heading
/// escape` for the truncate-before-escape ordering rule.
fn extract_truncated_mention_lines<'a>(full_body: &'a str, included_excerpt: &str) -> Vec<&'a str> {
full_body
@ -253,7 +253,7 @@ fn review_state_label(state: &str) -> Option<&str> {
/// Build a human-readable wake message for one Forgejo notification,
/// or `None` for a self-echo the caller should mark-read without
/// delivery. Wrapper format table + meta-suffix shape + number/repo
/// extraction live in `docs/forge.md::Wrapper format` +
/// extraction live in `docs/integrations/forge.md::Wrapper format` +
/// `::Meta suffix`.
async fn format_notification<S: Source>(
client: &reqwest::Client,
@ -305,7 +305,7 @@ async fn format_notification<S: Source>(
// Always fetch subject detail for assignee/reviewer metadata so
// the meta suffix can show current ownership without a follow-up
// fetch (see `docs/forge.md::Meta suffix`).
// fetch (see `docs/integrations/forge.md::Meta suffix`).
let subject = if subject_api_url.is_empty() {
None
} else {
@ -361,7 +361,7 @@ async fn format_notification<S: Source>(
// that comment was genuinely posted AFTER the close — a comment
// racing the merge inside the state-change tolerance window — append
// it so it isn't lost (best of both worlds; see
// `docs/forge.md::Merge racing a comment`). The ordinary pre-merge
// `docs/integrations/forge.md::Merge racing a comment`). The ordinary pre-merge
// last comment (created before `closed_at`) is left off.
let comment_tail = if has_comment {
fresh_post_close_comment_tail(
@ -413,7 +413,7 @@ struct NotifMeta<'a> {
/// Build the `\nassignee: ...` (and optionally `\nreviewer: ...`)
/// suffix appended to every wrapper. Shape + presence rules live in
/// `docs/forge.md::Meta suffix`.
/// `docs/integrations/forge.md::Meta suffix`.
fn build_meta_suffix(subject: Option<&serde_json::Value>, is_pr: bool) -> String {
let assignees: Vec<&str> = subject
.and_then(|s| s["assignees"].as_array())
@ -675,7 +675,7 @@ fn format_state_change_notification(
// time. A review submitted with no body carries no
// `latest_comment_url`, so it lands here instead of on the comment
// path — and its event time is well after `created_at`. Labeling
// that "new PR" is misleading (see docs/forge.md, "new vs activity
// that "new PR" is misleading (see docs/integrations/forge.md, "new vs activity
// on"): agents dismiss it as a
// duplicate of the original open notification. When we can't confirm
// creation, fall back to a neutral "activity on" label.
@ -707,7 +707,7 @@ fn format_state_change_notification(
// Review-request override: Forgejo doesn't reliably set
// `reason == "review_requested"` (often null), so we check the
// subject payload's `requested_reviewers` list directly. See
// `docs/forge.md::Review-request override`.
// `docs/integrations/forge.md::Review-request override`.
let is_review_request = is_new
&& *is_pr
&& !own_login.is_empty()
@ -751,7 +751,7 @@ fn format_state_change_notification(
/// `created_at`: within `NEW_ITEM_TOLERANCE_SECS` ⇒ creation. When
/// either timestamp is missing or unparseable we default to `true`,
/// preserving the prior "new" behavior rather than masking a genuine
/// new item behind the neutral fallback. See docs/forge.md, "new vs
/// new item behind the neutral fallback. See docs/integrations/forge.md, "new vs
/// activity on".
fn notification_is_creation(
event: Option<OffsetDateTime>,