SPEC.md — Tended Web Conventions (wire version 0.1, document rev 0.1.3)

Status: pre-publication draft, implementation-ready per external review.

Versioning rule: the *wire version* (the value of Version: in files and "spec" in graph.json) is 0.1. The *document revision* (0.1.3) tracks editorial clarifications only. The wire version bumps only when parsing behavior or schema changes in a way that affects implementations. Rev 0.1.1–0.1.3 clarified ambiguities; no file valid under a correct reading of 0.1 was invalidated, so the wire version is unchanged.

Prime directive: every file must be hand-writable in a text editor in under two minutes. (FOAF/XFN died of markup complexity; we do not repeat that.)

0. File grammar (applies to tended.txt and vouches.txt)

1. tended.txt — the pledge file

Location by protocol:

Example (a complete, valid file):

Version: 0.1
Name: Jane Doe
Since: 2026-02-14
Pledge: https://tendedweb.example/manifesto
Vouches: https://janedoe.net/.well-known/vouches.txt
Contact: https://janedoe.net/about
Also: gemini://jd.example/

Fields:

Semantics: existence of a parseable tended.txt at a stable address = declaration. Declaration cannot be refused. Discovery requires seed submission or an inbound vouch (§6 clause 2).

2. vouches.txt — the edges

Example (a complete, valid file with two vouches):

Version: 0.1
Site: https://example.org
Work: https://example.org/essays/firmware-dump
Date: 2026-03-01
Note: Reproduced the dump procedure myself; it works as described.
Site: gemini://capsule.example/
Work: gemini://capsule.example/logs/2026-01.gmi
Date: 2026-03-10
Note: Checked their archival claims against the originals; they held.

Stanza fields (all REQUIRED):

Semantics:

3. Colophon convention (per-work disclosure)

Not a file format — deliberately. A prose note on or linked from a substantial work: what the human directed, what tools contributed, what was rejected/verified. Disclosure is a norm, never a schema. No machine-readable method/authorship field will ever be added (constitutional; fork-only).

4. Crawler behavior (normative for reference crawler; rival indexes legitimate)

4.1 URL resolution & canonical node identity

4.2 Crawl algorithm

  1. Load seeds.txt (public, in repo; §6 acceptance rule).
  2. For each seed and discovered node: resolve per §4.1, fetch, parse, register node.
  3. Follow Vouches: pointer; parse stanzas; resolve each Site: per §4.1. A resolved target is a discovery candidate; fetch and parse its pledge before registering it. Register an edge only when both source and target have registered, parseable pledge nodes. Any valid registered target joins the graph (BFS).
  4. Protocols: HTTPS, HTTP, Gemini, gopher. Short timeouts.

4.3 Withdrawal vs. unreachability vs. malformed

4.4 Outputs

  1. Emit graph.json (§5).
  2. Render directory: HTML (required), gemtext mirror (required), gophermap (optional). Renderers MUST treat every graph string field as untrusted input and escape for the output medium (stored-XSS defense; graph.json carries raw strings). name and note are especially visible because they are intended for direct display.
  3. Commit snapshot to public git as snapshots/graph-YYYY-WW.json. Git history = the v0 transparency log.

Directory tiers: Declared (0 inbound edges) and Vouched (≥1). Both listed. No other ranking. Directory MUST display who-vouches-whom with Notes — the data, not a score. No global reputation number exists in v0.1 (possibly ever; trust may remain perspective-relative).

5. graph.json schema (consumer contract)

Stable public URL. Raw strings, UTF-8, unescaped (escaping is the consumer/renderer's job). Schema changes = wire version bump.

Example (valid JSON, no comments):

{
  "spec": "0.1",
  "crawled": "2026-03-15T00:00:00Z",
  "nodes": [
    {
      "id": "https://janedoe.net/.well-known/tended.txt",
      "name": "Jane Doe",
      "since": "2026-02-14",
      "tier": "vouched",
      "vouches_url": "https://janedoe.net/.well-known/vouches.txt",
      "also": ["gemini://jd.example/"],
      "last_seen": "2026-03-15T00:00:00Z"
    },
    {
      "id": "https://example.org/.well-known/tended.txt",
      "name": "Example Archive",
      "since": "2025-11-20",
      "tier": "vouched",
      "vouches_url": null,
      "also": [],
      "last_seen": "2026-03-15T00:00:00Z"
    }
  ],
  "edges": [
    {
      "from": "https://janedoe.net/.well-known/tended.txt",
      "to": "https://example.org/.well-known/tended.txt",
      "work": "https://example.org/essays/firmware-dump",
      "date": "2026-03-01",
      "note": "Reproduced the dump procedure myself; it works as described."
    }
  ]
}

Field reference:

| Field | Type | Notes |
|---|---|---|
| `spec` | string | Wire version. Required. |
| `crawled` | string | ISO 8601 UTC timestamp of crawl. Required. |
| `nodes[].id` | string | Canonical node ID per §4.1. Required, unique. |
| `nodes[].name` | string | Raw/untrusted. Required. |
| `nodes[].since` | string | ISO 8601 date. Required. |
| `nodes[].tier` | string | `"declared"` or `"vouched"`. Required. |
| `nodes[].vouches_url` | string \| null | Required (nullable). |
| `nodes[].also` | array of strings | May be empty. Unverified claims. Required. |
| `nodes[].last_seen` | string | ISO 8601 UTC of last successful fetch. Required. |
| `edges[].from`, `edges[].to` | string | Must each match a `nodes[].id`. Required. |
| `edges[].work` | string | URL as written by the voucher. Required. |
| `edges[].date` | string | ISO 8601 date. Required. |
| `edges[].note` | string | Raw/untrusted. Required. |

Consumers MUST ignore unknown fields (mirror of the file-grammar rule).

6. Governance

  1. No reserved field for authorship/method of production, ever.
  2. Any submitted seed with a parseable pledge file is accepted, and no node reachable through the graph is ever excluded. (Declaration is unrefusable; discovery requires seed submission or an inbound vouch — stated plainly to avoid overclaiming.)
  3. Crawler + seeds remain public and forkable.

7. Reserved for future versions