go-doc/data/docs/go-doc.md
2026-10-07 15:03:50 +02:00

8.3 KiB

go-doc

go-doc is a flat-file documentation and audience-feedback platform. It isolates guidance for three audiences inside a single Markdown document, lets anyone annotate that document as a User 🟢, Operator 🔵 or Developer 🔴, and stores everything as plain files you can read, diff and commit.

data/docs/go-doc.md                     # raw Markdown, one file per document
data/comments/go-doc-comments.json      # JSON array of audience feedback

One file carries all three viewpoints. Sections are delimited by level-2 headings and classified by the audience they address, so the reader renders the tab that matters to you while the shared preamble stays visible to everyone. Feedback badges are derived server-side from the stored role, so a client cannot spoof another audience's colour by submitting a badge field.


For Users

You use the product. You are not deploying it and you are not extending it — you want the shortest path from "I have an account" to "the thing I came here to do actually worked".

Getting oriented

Open the app and you land on the document index: every Markdown file in data/docs/, newest first. Each entry shows its size, last modification date and how much audience feedback it has collected.

Switching to your view

Every document has three tabs along the top:

Tab Badge What it shows you
User 🟢 Day-to-day workflows and everyday tasks
Operator 🔵 Deployment, health checks, rollback
Developer 🔴 API contracts, schemas, integration notes

Switching tabs swaps the guidance without duplicating content across files. The preamble above — this section — always stays visible.

Leaving feedback

The right-hand panel is your feedback channel. Pick the badge that matches who you are, write your note, and post it. It is stored as one entry in a JSON array next to the document, so your note is never lost and never overwritten by someone else's.

Filter the list by any badge to focus on the feedback relevant to you. Comments carry the author, the role, the badge and a timestamp, and you can delete your own.

Editing a document

Press Edit to open the hybrid editor. It has two synchronised views:

  • Visual Edit (WYSIWYG) — a rich surface where bold, italic, inline code, headings, lists, quotes and links are applied directly. The Markdown underneath updates as you type.
  • Source Code (Markdown) — the raw Markdown with a live preview beside it.

Both views write to the same string, so you can start in one and finish in the other. Ctrl/Cmd+S saves.


For Operators

You keep the thing running. Your concern is state convergence, blast radius, and how fast you can get back to a known-good position when a release goes wrong.

Storage layout

State is a directory, not a database:

data/docs/<slug>.md                     # one Markdown file per document
data/comments/<slug>-comments.json      # one JSON array per document

There is no daemon to restart and no migration to run. Backups are file copies; the directory layout is the schema.

Write guarantees

Every mutation is a temp-file write, fsync, rename, then fsync of the parent directory. The consequences matter operationally:

  • A reader never observes a half-written document.
  • A crash mid-write leaves either the previous file or the new one — never a truncated one.
  • Feedback read-modify-write cycles are serialised per slug, so a burst of comments cannot interleave and lose entries.

Temporary files are named .godoc-tmp-* and are excluded from the document index. If you ever see one survive a crash, that directory is safe to inspect and safe to delete.

Health checks

curl -fsS http://localhost:8080/api/health

Returns {"status":"ok","data_root":"...","time":"..."}. If the process answers at all, the data directory was opened successfully at boot.

Store-wide census

GET /api/stats reports total documents, total comments and a per-role breakdown — the fastest way to confirm a restore or a sync actually landed:

{ "docs": 120, "comments": 3000, "by_role": { "user": 1000, "operator": 1000, "developer": 1000 } }

Rollback

There is nothing to roll back except the files. Restore the previous data/ snapshot, restart the process, and confirm with the index endpoint. Deletes are permanent and are not journalled, so keep your backups.


For Developers

You integrate with it or you change it. You care about contracts, determinism, and failure modes.

REST API

Method Path Purpose
GET /api/health Liveness and the resolved data root
GET /api/roles Role → badge → colour catalogue for the UI
GET /api/docs Index; ?q= searches, ?counts=1 adds comment counts
GET /api/docs/:slug Raw Markdown (text/markdown), or ?format=json
POST /api/docs/:slug Create or fully replace; text/markdown or JSON
PUT /api/docs/:slug Alias of POST
DELETE /api/docs/:slug Permanent delete; cascades feedback
GET /api/docs/:slug/comments JSON list of comments
POST /api/docs/:slug/comments Append {author, role, content}
DELETE /api/docs/:slug/comments/:id Remove one comment
GET /api/docs/:slug/stats Totals, per-role tally, time bounds
GET /api/stats Store-wide totals

Slug contract

Titles are slugified into lower-case, hyphen-separated identifiers:

"Core Service API"        -> core-service-api
"Überwachen der Dienste"  -> uberwachen-der-dienste
"API_v2 (Internal)"       -> api-v2-internal
"!!!":                    -> rejected

Diacritics fold to ASCII, separator runs collapse to a single hyphen, the result is trimmed and capped at 120 characters. Slugs containing /, \, .., control characters or NUL bytes are rejected with 400 invalid_slug before any filesystem call, so a URL can never escape data/docs.

Because normalisation is idempotent, /api/docs/Core Service API and /api/docs/core-service-api address the same document. The POST response echoes the canonical slug — always use that value.

Comment contract

POST /api/docs/:slug/comments accepts:

{ "author": "ana", "role": "Developer", "content": "The retry budget is undocumented." }

The stored record is authoritative — the badge is derived from the role, and an incoming badge field is ignored:

{
  "id": "c1a105f4684c_9b1c081def3b00a4",
  "author": "ana",
  "role": "developer",
  "badge": "🔴",
  "content": "The retry budget is undocumented.",
  "timestamp": "2026-10-04T08:07:45.996Z"
}

Roles are canonicalised case-insensitively (Developer, DEV, developer are the same role) and anything else returns 400 invalid_role. Empty content returns 400 invalid_comment. An unset author defaults to the role label. IDs are a millisecond prefix plus 8 random bytes, so they sort roughly chronologically and do not collide under load.

Error shapes

Every failure is JSON, so clients never have to parse an HTML error page:

{
  "error": { "code": "invalid_role", "message": "storage: invalid role: \"root\" ...", "request_id": "" },
  "status": 400
}

Codes: not_found, invalid_slug, invalid_role, invalid_comment, invalid_body, body_too_large, internal_error.

Limits

Limit Value
Document body 5 MiB
Comment body 256 KiB
Slug length 120 characters
Author length 120 characters

Exceeding the document or comment ceiling returns 400 body_too_large and writes nothing.

Embedding the SPA

make build copies frontend/dist into backend/webui/dist, where //go:embed all:dist picks it up. The compiled binary then serves the SPA on / and the API on /api/* from one process with no runtime dependencies. Client-side routes such as /docs/core-service-api fall back to index.html; /api/* never does.


Other go-* projects documented in this workspace: