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)
- UTF-8. LF or CRLF accepted.
- Line-oriented
Key: Value. - Comment lines: lines whose first character is
#. Inline comments are NOT supported — a#appearing mid-line is part of the value. - Unknown keys MUST be ignored (forward compatibility).
Version:is a FILE HEADER, not a stanza field. The first non-comment, non-blank line of every file MUST be exactlyVersion: 0.1after trimming the key and value around the colon. A blank line ends the header; everything after is body/stanzas.- A vouches.txt containing only the header is valid (only
Version: 0.1means an empty vouch file with zero edges). A header-only tended.txt is malformed because the pledge fields in §1 are required. - Stanzas are groups of
Key: Valuelines separated by one or more blank lines. - Malformed files are skipped silently by crawlers — never flagged, never shamed. Malformed individual stanzas are skipped; the rest of the file is still processed. (Withdrawal implications of malformed files: §4.3.)
1. tended.txt — the pledge file
Location by protocol:
- HTTP(S):
/.well-known/tended.txt(primary) or/tended.txt(fallback) - Gemini:
gemini://host/tended.txt(capsule root) - Gopher: text selector
/tended.txtreferenced from root gophermap - Software project:
TENDED.txtat repo root (crawled via raw-file URL)
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:
Name— REQUIRED. Persistent human-readable identity; pseudonyms fine.Since— REQUIRED. ISO 8601 date of pledge.Pledge— REQUIRED. Link to the manifesto (the backlink that makes the graph walkable).Vouches— OPTIONAL, strongly encouraged. Absolute URL of this identity's vouches.txt. Any protocol.Contact— OPTIONAL. Where a human can reach you.Also— OPTIONAL, repeatable. Other presences of the same identity. Informational only in v0.1: an unverified claim; crawlers MUST NOT merge nodes on its strength (becomes provable viaKey:upgrade).Key— OPTIONAL, RESERVED for protocol upgrade. v0.1 crawlers ignore it.
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):
Site— the vouched identity's tended.txt URL or site root (resolution: §4.1). Any protocol; cross-protocol edges are normal.Work— the SPECIFIC work vouched for. Outputs, not vibes.Date— ISO 8601 date.Note— one line: what you verified and how. This is the stake.
Semantics:
- A vouch is a public, named stake on a specific work. The Note naming your verification act is the cultural spam/inflation defense.
- Un-vouching = deleting the stanza. No tombstone. Retraction is a feature. History survives in crawler snapshots (diffable) — the tamper-evidence layer.
- Vouches cover only the named Work. Multiple stanzas for multiple works: normal, encouraged.
- Self-vouches (edge whose resolved target = its own source node) are ignored by crawlers.
- A header-only file (zero stanzas) is valid.
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
- Input URLs (seeds,
Site:values) may be a pledge-file URL or a site root. - Resolution: if path ends in
tended.txtorTENDED.txt→ fetch directly. Otherwise try, in order:/.well-known/tended.txt,/tended.txt(Gemini/gopher:/tended.txtonly). - Follow redirects, max 5 hops; cross-host redirects allowed; the final destination is authoritative. (Rationale: nothing is signed in v0.1, so the redirect target controls content anyway; forbidding cross-host redirects buys no security and costs domain-migration ergonomics.
Key:upgrade closes this properly.) - Canonical node ID = final resolved URL of the tended.txt file itself, with scheme and host lowercased and default ports stripped (
:443https,:80http,:1965gemini,:70gopher). Two inputs resolving to the same canonical file URL are the same node. Also:never merges nodes in v0.1.
4.2 Crawl algorithm
- Load
seeds.txt(public, in repo; §6 acceptance rule). - For each seed and discovered node: resolve per §4.1, fetch, parse, register node.
- Follow
Vouches:pointer; parse stanzas; resolve eachSite: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). - Protocols: HTTPS, HTTP, Gemini, gopher. Short timeouts.
4.3 Withdrawal vs. unreachability vs. malformed
- Withdrawal signals (server responsive, file DEFINITIVELY ABSENT at the server level): HTTP 404/410; Gemini status 51; gopher selector-not-found or error response from an otherwise-responsive server → drop node after 2 consecutive crawls with this signal (grace week protects against deploy accidents).
- Transient failures (server-level: DNS failure, timeout, TLS failure, connection refused, HTTP 5xx, Gemini 4x): retain node with last-good data for 8 consecutive failed weekly crawls, then drop.
- Malformed or empty files are NEVER withdrawal signals. Fetch-succeeded-but-parse-failed (including an empty or truncated tended.txt) is treated like a transient failure: retain last-good data, same 8-week clock. Only definitive server-level absence signals withdrawal — an accidentally emptied file cannot delist anyone.
- If a newly submitted seed or newly discovered target has a malformed pledge file on its first crawl, no node is registered until a later crawl fetches and parses a valid pledge file.
- Dropped nodes' history remains in snapshots; a node reappearing at its canonical URL simply rejoins.
4.4 Outputs
- Emit
graph.json(§5). - 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).
nameandnoteare especially visible because they are intended for direct display. - 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
- v0.x edited by founding maintainer; published changelog; breaking changes = wire version bump; crawlers support previous wire version ≥6 months.
- Constitutional clauses (fork-only, no amendment):
- No reserved field for authorship/method of production, ever.
- 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.)
- Crawler + seeds remain public and forkable.
- Extension-approval question deliberately deferred to protocol phase.
7. Reserved for future versions
Key:activation + signed vouches files (protocol L3/L4); makesAlso:verifiable via bidirectional signing- Rekor-backed transparency log replacing git snapshots (L5)
- Multi-key meta-identity; revocation statements (L3 v2)