Requirements catalog
The catalog is how Touchstone ties its results to the specification’s text. Every normative clause in the LWS drafts becomes a requirement with a stable IRI. Every test cites the requirements it verifies. Coverage, the report matrix, the verdict and the EARL links are all computed from those citations.
- What is in it
- A requirement
- Who a requirement binds
- Levels and verdicts
- How requirements get in
- Tracking the draft
What is in it
| File | Source document | Requirements |
|---|---|---|
catalog/lws10-core.ttl | LWS Protocol 1.0, Working Draft of 9 November 2026 | 215 |
catalog/lws10-authn-openid.ttl | OpenID Connect authentication suite, WD 15 October 2026 | 8 |
catalog/lws10-authn-ssi-cid.ttl | Self-signed identity using Controlled Identifiers, WD 15 October 2026 | 14 |
catalog/lws10-authn-saml.ttl | SAML 2.0 authentication suite, WD 9 November 2026 | 7 |
catalog/lws10-notifications-webhook.ttl | Webhook notification suite, editor’s draft as of 2 October 2026 | 20 |
catalog/lws10-index.ttl | Search and type index services, editor’s draft as of 2 October 2026 | 57 |
catalog/vocab/touchstone-vocab.ttl | The catalog vocabulary | |
catalog/sources/ | The archived draft snapshots and raw extraction output |
The catalog holds 321 requirements in total: 253 MUST, 29 SHOULD and 39 MAY. Twenty of them are from the webhook notification suite (D-0066) and 57 from the search and type index services (D-0067), both editor’s drafts.
A requirement
req:create-post-201-location-links
a touchstone:Requirement ;
touchstone:level "MUST" ;
touchstone:specModule "lws10-core" ;
touchstone:appliesTo touchstone:Server ;
touchstone:section <https://www.w3.org/TR/lws10-core/#create-resource> ;
touchstone:sourceDraft <https://www.w3.org/TR/2026/WD-lws10-core-20261109/#create-resource> ;
touchstone:summary "POST create returns 201 with Location and atomic server-managed Link metadata (rel=up, rel=linkset)." ;
touchstone:clauseText """On success, the server MUST return the 201 status code with the new URI in the Location header. ...""" ;
touchstone:clauseHash "sha256-6808523b25cc42196caf104571dd803d918e6237c1778147d809e65ace11dd87" ;
touchstone:status touchstone:Approved ;
dcterms:created "2026-07-16"^^xsd:date .
| Property | Meaning |
|---|---|
| IRI | https://example.org/touchstone/req/<module>/<slug>. Stable: tests and reports cite it. |
touchstone:level | MUST, SHOULD or MAY: the strongest BCP 14 keyword in the clause. MUST NOT counts as MUST. The exact wording is in the clause text. |
touchstone:specModule | Which document the clause comes from, such as lws10-core. |
touchstone:appliesTo | Who the clause obliges: one or more roles. |
touchstone:section | The section anchor in the undated /TR/ URL. Reports link here. |
touchstone:sourceDraft | The same anchor in the dated snapshot the clause was extracted from. |
touchstone:clauseText | The clause, verbatim: Unicode NFC, whitespace collapsed. |
touchstone:clauseHash | The SHA-256 of the normalised clause text, used to detect drift. |
touchstone:summary | One sentence restating the clause as something testable. |
touchstone:status | Where the entry is in review: Draft (extracted and curated, awaiting review), Approved, or Deprecated (the clause changed or disappeared; the entry is kept so historical reports still resolve). |
touchstone:Requirement is a subclass of earl:TestRequirement, so the requirement IRIs fit directly into EARL reports.
Who a requirement binds
Most clauses oblige the server, but not all. A client must present its token in the Authorization header, an OpenID Provider must not sign an ID Token with none, and a webhook inbox must verify the signature of a delivery. touchstone:appliesTo names who can break the clause, one value per role (D-0076):
| Role | Who | Requirements |
|---|---|---|
Server | The LWS server: the storage and the services its storage description advertises | 247 |
AuthorizationServer | The storage’s authorization server: metadata, token exchange, access tokens, credential validation | 26 |
Client | The client application, including the credentials it issues for itself under the CID suite | 81 |
IdentityProvider | An OpenID Provider or SAML identity provider that issues credentials for an end user | 16 |
Receiver | A webhook inbox | 4 |
Specification | Another specification, such as an authentication suite, which no implementation can break | 2 |
A clause that obliges more than one role names each: 44 bind both servers and clients, such as linkset-precondition-failed-412 (“servers and clients SHOULD use conditional requests”). A clause that defines a message binds whoever produces it, so “the ID Token MUST NOT use none” is the identity provider’s, and the authorization server’s duty to refuse such a token is its own requirement.
The roles decide two things.
- Coverage. A server run answers for
ServerandAuthorizationServerrequirements, sotouchstone coverage, the MCPcoveragetool and the reports count only those (272 of 321). The others are for client testing;definitions/COVERAGE.mdlists the client and receiver requirements separately. - The lint. A test that cites requirements must cite one that binds a server or an authorization server. A negative test may instead rest on the clause of the client or identity provider whose message it forges: refusing that message is what the test checks.
Levels and verdicts
A MUST failure makes a run non-conformant. SHOULD and MAY failures are advisory. Optional features are gated by capabilities rather than failed. See Reports and verdicts.
How requirements get in
Requirements are extracted semi-automatically, and none enters the catalog without human review. The tools are in tools/extractor/. They are plain Python 3 with only the standard library.
| Script | Purpose |
|---|---|
extract_clauses.py <spec.html> <out.json> | Parses a ReSpec-rendered W3C draft and emits every block that carries BCP 14 keyword markup, with its section, heading, keywords and normalised text. |
emit_candidates.py <clauses.json> <curation.json> <catalog.ttl> | Turns the extraction plus a human curation file (a slug and a summary for each block) into catalog entries. Every block must be seeded, skipped or curated. The generated part of the file is replaced on each run, so it is never edited by hand. |
clause_hash.py --update/--check <catalog.ttl>... | Fills in or verifies the clause hashes. --check exits non-zero on a mismatch. |
check_drift.py --spec <spec.html> <catalog.ttl>... | The “spec moved” alarm, described below. |
The draft snapshot each file is based on is archived, unmodified, in catalog/sources/. Every hash can be recomputed from a byte-exact source, and checking for drift does not depend on the W3C site.
Tracking the draft
LWS is a Working Draft and it changes. To check whether the catalog still matches the current draft, fetch the draft and run the drift check:
curl -sL -o /tmp/lws10-core.html https://www.w3.org/TR/lws10-core/
python tools/extractor/check_drift.py --spec /tmp/lws10-core.html catalog/lws10-core.ttl
It re-extracts the draft and fails (exit 1) if any catalogued clause no longer appears or any section anchor no longer resolves. It also lists new normative blocks the catalog does not hold. Run against the snapshot the catalog is based on, it passes:
catalog entries checked: 215; spec normative blocks: 216; section anchors checked: 215
info: 1 uncatalogued normative block(s):
[conformance] The key words MAY, MUST, MUST NOT, OPTIONAL, RECOMMENDED, REQUIRED, SHOULD, and SHOULD NOT...
no drift: every catalog clause still appears in the spec, and every section anchor resolves
A scheduled CI job, Spec drift (.github/workflows/spec-drift.yml), does this every week for all six documents. tools/extractor/check_published.py fetches each document’s latest version from /TR/, and fails when W3C serves a newer dated version than the one the catalog is baselined on, or when a catalogued clause has changed:
python tools/extractor/check_published.py catalog/lws10-*.ttl
When the draft moves, the catalog is re-baselined. The new snapshot replaces the old one in catalog/sources/. Changed clauses are updated, new ones are added, and retired ones are removed or deprecated. Definitions that cite affected requirements are reviewed. Because re-baselining rewrites Approved entries, it is a reviewed change.
The catalog is based on the 9 November 2026 core and SAML drafts and the 15 October 2026 OpenID Connect and CID drafts, the versions W3C published as of 9 November 2026 (D-0057, D-0086, D-0100, D-0111). Since the 23 October core draft, a create in a container that does not exist reports the Target Not Found outcome, and a status table maps it to 404, Not Permitted to 403 and Unknown Requester to 401, with no BCP 14 keyword; a server MAY answer 404 instead of 403 so as not to reveal that a resource exists. The 9 November SAML draft corrects its example only. The 19 October core draft has a 401 name the requested resource’s protected resource metadata (RFC 9728) instead of the authorization server and a realm, and link to the storage a token is for, which a client checks before it asks for one. It requires only application/lws+json of a container, and a representation asked for in the LWS profile must be RDF isomorphic to it. And it lets clients move resources between containers by changing their up link, where a MoveResource capability says so. The 5 October core draft made JSON Patch (RFC 6902, application/json-patch+json) the patch format every server must support, on data resources and on linksets, where the September draft named JSON Merge Patch. Against the 21 August baseline, the September core draft removed the 428 on an unconditional PUT, the parent ETag change after a delete, the new ETag after an update and the ETag on a 201; it made conditional requests a SHOULD; and it added subject_identifier_types_supported to the authorization server metadata.
The did:key suite is not catalogued. W3C published it as a Discontinued Draft on 29 September 2026, in favour of the CID suite, which subsumes it, and Touchstone retired it (D-0058). A did:key subject is tested under the CID suite.