feat(registry)!: a forge namespace is a rule, not a charset #8

Merged
seeds-agent merged 2 commits from feat/the-forge-namespace-is-a-rule into main 2026-10-28 20:31:43 +00:00
Collaborator

The rule was in a comment

forgejo_organization was style free: the caller supplied the name, the kit validated the charset and the length. The rule — a namespace is an org's long form, plus the reserved shared — lived in the type's own notes, where nothing could enforce it.

dataseeds    typeable, inside the charset, under 40 characters -> ACCEPTED

It answers no question the convention asks. A forge namespace is named by the org axis, because that is what it answers: whose is this repository. The long form and not the code — a human types it into a URL, and see/infra-conventions would read as noise.

What this adds

A new style, namespace, in all three implementations:

the caller what happens
supplies no name the module derives it from context.org
supplies a name accepted only where the rule already allows it — that is how shared gets in
supplies anything else refused, by the module and by the policy

And a namespaces block in registry/conventions.yaml declaring what the names are drawn from (org.long) beside the reserved ones. Derived, never written out — adding an org adds its namespace:

see -> seeds      per -> personal      dgt -> digitank      + shared

Two lints, because the set has to stay unambiguous

A reserved name that is also an org long form would make one namespace answer two questions — "the org seeds" and "held over every org" — and no rename repairs it: a forge namespace sits in every clone URL already written down.

And namespaces.from is confronted with the one value that is implemented, so a typo there cannot silently mean nothing.

Verification

75 module tests   (+5: 4 shared cases, 1 refusal)
55 policy tests   (+5: the org form, the reserved one, a free name, an org code,
                   and a repository that stays free)
13 lint mutations (+2), 0 silent

make check green. The module precondition was mutated to always pass and namespace_outside_the_rule failed — which is the only thing that makes its green mean anything. OpenTofu refuses a constant precondition outright, so the mutation had to stay referential (>= 0 rather than length([])).

What stays as it was

A repository stays free. It is a functional name like a DNS record, and recomposing it by the grammar would produce a URL nobody would type either.

full_name is not governed. stacks/forge/namespaces currently writes title(var.organization) → Shared. If the display label should come from the vocabulary's label field ("Seeds, the operator of the platform"), say so and it becomes a second rule — the kit has no notion of a display label today.

A second commit: who may read a reserved namespace

Asked while reviewing this: does the convention need to say that shared exists in the forge with no infrastructure behind it, public, and with no code or label?

Two of the three were already here, in registry/conventions.yaml and in CONVENTION.md section 4:

shared is not an org code: it owns nothing in a cloud, has no VMID digit, and never appears in a resource name.

The third was missing, and it turns out the kit already depends on it. "The kit is public, so no consumer's CI carries a read token" is two facts: the repository must be publicly readable — measured, an unauthenticated read answers 200, and the README says so — and so must the namespace holding it. A namespace set to limited means signed-in users only, which is the same token wearing another hat. Nothing recorded that half.

reserved:
  shared:
    reason: "What is held over every org and owned by none"
    visibility: public

Not a rule about every namespace. An org's own (seeds, personal) has no reason to be public, and making it so would publish whatever it ever comes to hold. Only a namespace that exists to be read by everyone declares it.

And nothing here can test it — a visibility is an API answer and this suite reaches no network. The value is declared so that whoever lays the namespace down takes it from the convention instead of rediscovering it, and so the consumer that can test it has something to confront: stacks/forge/namespaces in seeds-infra already asserts exactly this value, on the organisation and on the repository. It is written into the README's known limits as a declared-not-verified fact, beside the others.

Four more refusals, each mutation-tested:

a reserved name that is also an org long form   it would answer two questions
a reserved namespace with no reason             nobody can say why it exists
a reserved namespace with no visibility         the promise above rests on it
a visibility no forge knows                     public | limited | private

18 lint mutations, 0 silent. The rest of the suite did not move: the reserved map went from name -> string to name -> object, and both the tool and the policy read its keys.

🤖 Generated with Claude Code

## The rule was in a comment `forgejo_organization` was style `free`: the caller supplied the name, the kit validated the charset and the length. The rule — a namespace is an org's long form, plus the reserved `shared` — lived in the type's own `notes`, where nothing could enforce it. ``` dataseeds typeable, inside the charset, under 40 characters -> ACCEPTED ``` It answers no question the convention asks. A forge namespace is named by the **org axis**, because that is what it answers: *whose is this repository*. The **long** form and not the code — a human types it into a URL, and `see/infra-conventions` would read as noise. ## What this adds A new style, `namespace`, in all three implementations: | the caller | what happens | | --- | --- | | supplies no name | the module derives it from `context.org` | | supplies a name | accepted only where the rule already allows it — that is how `shared` gets in | | supplies anything else | refused, by the module **and** by the policy | And a `namespaces` block in `registry/conventions.yaml` declaring what the names are drawn from (`org.long`) beside the reserved ones. **Derived, never written out** — adding an org adds its namespace: ``` see -> seeds per -> personal dgt -> digitank + shared ``` ## Two lints, because the set has to stay unambiguous A **reserved name that is also an org long form** would make one namespace answer two questions — "the org `seeds`" and "held over every org" — and no rename repairs it: a forge namespace sits in every clone URL already written down. And `namespaces.from` is confronted with the one value that is implemented, so a typo there cannot silently mean nothing. ## Verification ``` 75 module tests (+5: 4 shared cases, 1 refusal) 55 policy tests (+5: the org form, the reserved one, a free name, an org code, and a repository that stays free) 13 lint mutations (+2), 0 silent ``` `make check` green. The module precondition was **mutated to always pass** and `namespace_outside_the_rule` failed — which is the only thing that makes its green mean anything. OpenTofu refuses a constant precondition outright, so the mutation had to stay referential (`>= 0` rather than `length([])`). ## What stays as it was **A repository stays `free`.** It is a functional name like a DNS record, and recomposing it by the grammar would produce a URL nobody would type either. **`full_name` is not governed.** `stacks/forge/namespaces` currently writes `title(var.organization)` → `Shared`. If the display label should come from the vocabulary's `label` field ("Seeds, the operator of the platform"), say so and it becomes a second rule — the kit has no notion of a display label today. ## A second commit: who may read a reserved namespace Asked while reviewing this: does the convention need to say that `shared` exists in the forge with **no infrastructure behind it**, **public**, and with **no code or label**? Two of the three were already here, in `registry/conventions.yaml` and in `CONVENTION.md` section 4: > `shared` is not an org code: it **owns nothing in a cloud**, has **no VMID digit**, and **never appears in a resource name**. The third was missing, and it turns out the kit already depends on it. *"The kit is public, so no consumer's CI carries a read token"* is **two facts**: the repository must be publicly readable — measured, an unauthenticated read answers 200, and the README says so — and so must the **namespace** holding it. A namespace set to `limited` means signed-in users only, which is the same token wearing another hat. Nothing recorded that half. ```yaml reserved: shared: reason: "What is held over every org and owned by none" visibility: public ``` **Not a rule about every namespace.** An org's own (`seeds`, `personal`) has no reason to be public, and making it so would publish whatever it ever comes to hold. Only a namespace that exists to be read by everyone declares it. **And nothing here can test it** — a visibility is an API answer and this suite reaches no network. The value is declared so that whoever lays the namespace down takes it from the convention instead of rediscovering it, and so the consumer that *can* test it has something to confront: `stacks/forge/namespaces` in `seeds-infra` already asserts exactly this value, on the organisation and on the repository. It is written into the README's known limits as a declared-not-verified fact, beside the others. Four more refusals, each mutation-tested: ``` a reserved name that is also an org long form it would answer two questions a reserved namespace with no reason nobody can say why it exists a reserved namespace with no visibility the promise above rests on it a visibility no forge knows public | limited | private ``` `18` lint mutations, 0 silent. The rest of the suite did not move: the reserved map went from `name -> string` to `name -> object`, and both the tool and the policy read its **keys**. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(registry)!: a forge namespace is a rule, not a charset
Some checks failed
ci / check (pull_request) Has been cancelled
15d6e3c5d9
`forgejo_organization` was style `free`: the caller supplied the name and the kit
validated the charset and the length. The rule — a namespace is an org's long
form, plus the reserved `shared` — lived in the type's own notes, where nothing
could enforce it. `dataseeds` was accepted: typeable, inside the charset, under
40 characters, and an answer to no question the convention asks.

A forge namespace is named by the ORG AXIS, because that is what it answers:
whose is this repository. The long form and not the code — a human types it into
a URL, and `see/infra-conventions` would read as noise.

So: a new style, `namespace`, in all three implementations.

  name supplied by nobody   the module derives it from context.org
  name supplied             accepted only if the rule already allows it
  anything else             refused, by the module AND by the policy

And a `namespaces` block in `registry/conventions.yaml` declaring what the names
are drawn from (`org.long`) beside the reserved ones (`shared`). Derived, never
written out: adding an org adds its namespace.

  see -> seeds        per -> personal        dgt -> digitank        + shared

TWO LINTS, because the set has to stay unambiguous. A reserved name that is also
an org long form would make one namespace answer two questions — "the org seeds"
and "held over every org" — and no rename repairs it: a forge namespace sits in
every clone URL already written down. And `namespaces.from` is confronted with
the one value that is implemented, so a typo there cannot silently mean nothing.

Verified, and each guard mutation-tested:

  75 module tests   (+5: 4 shared cases, 1 refusal)
  55 policy tests   (+5: the org form, the reserved one, a free name, an org
                     code, and a repository that stays free)
  13 lint mutations (+2), 0 silent

The module precondition was mutated to always pass and `namespace_outside_the_rule`
failed, which is the only thing that makes its green mean anything. OpenTofu
refuses a constant precondition outright, so the mutation had to stay referential.

A repository stays `free`: it is a functional name like a DNS record, and
recomposing it by the grammar would produce a URL nobody would type either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
seeds-agent force-pushed feat/the-forge-namespace-is-a-rule from 15d6e3c5d9
Some checks failed
ci / check (pull_request) Has been cancelled
to b176a88c29
Some checks failed
ci / check (pull_request) Has been cancelled
2026-10-28 19:51:10 +00:00
Compare
feat(registry): a reserved namespace declares who may read it
Some checks failed
ci / check (pull_request) Has been cancelled
49a65fb9ed
"The kit is public, so no consumer's CI carries a read token" is TWO facts and
the kit recorded one. The repository must be publicly readable - measured, an
unauthenticated read answers 200, and the README says so. So must the NAMESPACE
holding it: a namespace set to `limited` means signed-in users only, which is
the same token wearing another hat. Nothing here said that, which is how a
dependency goes unrecorded until it breaks.

So a reserved namespace declares its visibility:

  reserved:
    shared:
      reason: "What is held over every org and owned by none"
      visibility: public

NOT A RULE ABOUT EVERY NAMESPACE. An org's own - seeds, personal - has no reason
to be public, and making it so would publish whatever it ever comes to hold.
Only a namespace that exists to be read by everyone declares it.

AND NOTHING HERE CAN TEST IT. A visibility is an API answer and this suite
reaches no network. The value is declared so that whoever lays the namespace
down takes it FROM the convention instead of rediscovering it, and so that the
consumer which CAN test it has something to confront - stacks/forge/namespaces
in seeds-infra already asserts exactly this value, on the organisation and on
the repository. Written into the known limits as a declared-not-verified fact,
beside the others.

What lint-registry now refuses, each mutation-tested:

  a reserved name that is also an org long form   it would answer two questions
  a reserved namespace with no reason             nobody can say why it exists
  a reserved namespace with no visibility         the promise above rests on it
  a visibility no forge knows                     public | limited | private

18 lint mutations, 0 silent. The rest of the suite is unchanged: the reserved
map went from name -> string to name -> object, and both the tool and the policy
read its KEYS, which is why neither moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
seeds-agent deleted branch feat/the-forge-namespace-is-a-rule 2026-10-28 20:31:43 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
shared/infra-conventions!8
No description provided.