Skip to content

Commit 3fe5793

Browse files
fix(clade): name every code, and stop the TSV reader eating empty fields (#50)
Two problems, both in how a `CLADE.a2ml` gets written. The second one **defeats my own merged #48** — details below. ## 1. A code cannot express a wrong belief, so it cannot be checked for one CLADE-003 checks the code is one of the 12. It **cannot check the author meant that clade** — every code is valid whatever you thought it stood for. So these passed every gate: | repo | wrote | believing | `pt`/`gv` actually mean | |---|---|---|---| | paint-type | `primary = "pt"` | "**P**ain**T**-type" | Protocols & Interop (it is an image editor → `ap`) | | gossamer | `primary = "gv"` | "**G**raphical/**V**isual" | Governance & Standards (there is no graphical clade → `dx`) | **The two letters abbreviate the CLADE's name. They never abbreviate the REPO's name.** It is a closed taxonomy of categories, not an abbreviation scheme — but nothing said so at the point of authoring, and nothing could catch it. ### Fix: write the name beside the code, and check the pair ```toml [clade] primary = "ap" primary-name = "Applications" ``` CLADE-006 (BLOCK, pre-commit) rejects any pair disagreeing with `verisim/seed/clades.a2ml`. **The redundancy is the point** — the *pair* can express the wrong belief, so the pair can be rejected. Verified on all 10 cases, including both real historical errors: ``` ap|Applications PASS dx|Developer Ecosystem PASS gv|Governance & Standards PASS (ampersand survives) pt|PainT-type FAIL <- the actual paint-type error gv|Graphical/Visual FAIL <- the actual gossamer error pt|Applications FAIL valid code, wrong name ap|Protocols & Interop FAIL valid name, wrong code zz|Applications FAIL invalid code ap|<missing> FAIL cannot omit the name UNASSIGNED|UNASSIGNED FAIL template default fails loudly ``` `clades.a2ml` and the spec now state the rule and name both real errors, so the next author reads *why* before choosing. `deploy-clade-a2ml.sh` emits `primary-name` and refuses to write an identity it cannot name. ## 2. The TSV reader collapsed empty fields — 153 files already corrupted **Tab is IFS whitespace**, so `IFS=$'\t' read` treats a run of tabs as **one** delimiter: empty fields vanish and everything after shifts left. **316 of 319** seed entries have an empty `parent` — this was the normal case, not an edge case: ``` proven <tab> fv <tab> [] <tab> standalone <tab><tab> Formally verified… <tab> hyperpolymath ^^ empty parent collapses -> parent="Formally verified…" description="hyperpolymath" owner="" ``` **The damage is real and committed — 153 CLADE.a2ml files** carry `rationale = ""` with the repo's *description* sitting in `parent`: ``` panic-attack parent = "Security scanning tool — static analysis…" flat-mate parent = "Flat/apartment sharing management…" rpa-elysium parent = "Robotic Process Automation framework…" live-files parent = "Live file synchronisation tool…" ``` ### This defeated #48, and #48's own test could not see it The `owner` support I added in #48 **never worked here**: `$7` was always empty, so `owner` always fell back to `hyperpolymath` and an explicit `owner = "metadatastician"` was ignored. #48's byte-identical regression test passed because `export-json.sh` uses `awk -F'\t'`, which does **not** collapse — the two derivation sites disagreed and only the tested one was correct. My apologies: I shipped that. ### Fix Translate tabs to US (`0x1f`) before reading. It is not IFS whitespace so runs are not collapsed, it cannot occur in the data (0 occurrences in `repos.a2ml`), and it is exactly what the ASCII unit separator is for. The parser's TSV contract is unchanged. ## Verification Scratch tree, end to end: ``` proven (empty parent — the 316-entry case) rationale = "Formally verified safety library — Idris2 dependent types…" (was "") parent = "" (was the description) uuid = ffa8e1f5-… (unchanged, matches registry) cadastra (owner = "metadatastician" in seed) uuid = da97ea76-1715-5331-963b-27f011b10d22 <- correct; #48 works for the first time primary-owner = "metadatastician" ``` Also: `repos.json` regenerates **byte-identical** (no uuid moves), both a2ml files still parse as **TOML** (the dogfood gate uses `tomllib`), and the spec renders under asciidoctor. ## Not in this PR Repairing the **153 already-corrupted files** is a separate, owner-facing job — this only stops the script producing more. Related: #49. Draft: this changes the clade contract for every repo, so it is yours to review. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 390b106 commit 3fe5793

4 files changed

Lines changed: 185 additions & 2 deletions

File tree

contractiles/must/clade-hygiene.a2ml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,35 @@ scope = "pre-commit"
3434
command = "grep 'primary = ' .machine_readable/CLADE.a2ml | grep -qE '\"(fv|nl|rm|gv|db|ap|ix|dx|pt|ax|gm|sc)\"'"
3535
message = "CLADE.a2ml primary clade code not in valid set: fv nl rm gv db ap ix dx pt ax gm sc"
3636

37+
[must]
38+
id = "CLADE-006"
39+
name = "clade-name-matches-code"
40+
description = "primary-name must be the taxonomy's name for the declared primary code"
41+
severity = "BLOCK"
42+
scope = "pre-commit"
43+
44+
# WHY THIS EXISTS.
45+
#
46+
# CLADE-003 checks the code is one of the 12. It cannot check the AUTHOR MEANT
47+
# that clade — every code is valid whatever you thought it stood for. So these
48+
# passed every gate:
49+
#
50+
# paint-type primary = "pt" believing "PainT-type"
51+
# -- pt is Protocols & Interop; paint-type is an image editor
52+
# gossamer primary = "gv" believing "Graphical/Visual"
53+
# -- gv is GoVernance & Standards; there is no graphical clade
54+
#
55+
# The two letters abbreviate the CLADE's name, never the REPO's name. Requiring
56+
# the name alongside the code makes the mistaken belief WRITABLE, and therefore
57+
# CHECKABLE: primary = "pt" + primary-name = "PainT-type" now fails here,
58+
# instead of being filed silently under Protocols & Interop.
59+
#
60+
# The pairs below are the authority in verisim/seed/clades.a2ml. Keep in sync.
61+
62+
[must.check]
63+
command = '''p=$(sed -n 's/^primary = "\(.*\)".*/\1/p' .machine_readable/CLADE.a2ml | head -1); n=$(sed -n 's/^primary-name = "\(.*\)".*/\1/p' .machine_readable/CLADE.a2ml | head -1); case "$p|$n" in 'fv|Formal Verification & Proofs'|'nl|Nextgen Languages'|'rm|Repo Management & Tooling'|'gv|Governance & Standards'|'db|Databases'|'ap|Applications'|'ix|Infrastructure & Cloud'|'dx|Developer Ecosystem'|'pt|Protocols & Interop'|'ax|AI & Neurosymbolic'|'gm|Games & Interactive'|'sc|Security') exit 0 ;; *) exit 1 ;; esac'''
64+
message = "CLADE.a2ml primary-name does not match primary code. The 2 letters abbreviate the CLADE's name, not the repo's. Valid pairs: fv=Formal Verification & Proofs, nl=Nextgen Languages, rm=Repo Management & Tooling, gv=Governance & Standards, db=Databases, ap=Applications, ix=Infrastructure & Cloud, dx=Developer Ecosystem, pt=Protocols & Interop, ax=AI & Neurosymbolic, gm=Games & Interactive, sc=Security"
65+
3766
[must]
3867
id = "CLADE-004"
3968
name = "clade-status-phase-set"

docs/SPEC-clade-verisim-portal.adoc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,46 @@ the current repository name unchanged.
7070
**Rationale**: 2 characters provide 676 possible codes (far more than needed), are short
7171
enough to avoid bloating names, and cause natural alphabetical grouping when sorted.
7272

73+
[IMPORTANT]
74+
====
75+
*The two letters abbreviate the name of the CLADE. They never abbreviate the name of the
76+
repo.* This is a closed taxonomy of twelve categories, not an abbreviation scheme: you
77+
choose the category a repo belongs to, then write down that category's code. You do not
78+
look at the repo's name and find two letters that fit it.
79+
80+
This is stated because the opposite was done, more than once:
81+
82+
[cols="1,1,2,2"]
83+
|===
84+
| Repo | Code | Read as | Actually means
85+
86+
| `paint-type` | `pt` | "*P*ain*T*-type" | Protocols & Interop — an image editor is `ap`
87+
| `gossamer` | `gv` | "*G*raphical/*V*isual" | Governance & Standards — there is no graphical clade
88+
|===
89+
90+
Both codes were *valid*, so every check passed and the repos were filed under categories
91+
they have nothing to do with. A code alone cannot express a wrong belief about what it
92+
means, so it cannot be checked for one.
93+
94+
Therefore every `CLADE.a2ml` MUST declare the clade's name alongside its code:
95+
96+
[source,toml]
97+
----
98+
[clade]
99+
primary = "ap"
100+
primary-name = "Applications" # must be the taxonomy's name for `primary`
101+
----
102+
103+
The redundancy is the point: the *pair* can express the wrong belief, and so the pair can
104+
be rejected. `primary = "pt"` with `primary-name = "PainT-type"` now fails
105+
<<clade-006,CLADE-006>> instead of passing silently. `primary-name` must match the `name`
106+
field for that code in `verisim/seed/clades.a2ml`, which is the authority.
107+
108+
[[clade-006]]
109+
Enforced by CLADE-006 in `contractiles/must/clade-hygiene.a2ml` (severity BLOCK, scope
110+
pre-commit).
111+
====
112+
73113
=== Clade Definitions
74114

75115
[cols="1,3,4"]

sync/deploy-clade-a2ml.sh

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,32 @@ parse_repos() {
3737
bash "$REPOS_DIR/gv-clade-index/sync/parse-repos.sh" "$SEED_FILE"
3838
}
3939

40+
# The taxonomy's name for a clade code. Authority: verisim/seed/clades.a2ml.
41+
#
42+
# Emitted into every CLADE.a2ml as `primary-name` so the code never travels
43+
# without its meaning. CLADE-003 can only check a code is one of the 12 — it
44+
# cannot check the author meant that clade, which is how `pt` ("PainT-type",
45+
# actually Protocols & Interop) and `gv` ("Graphical/Visual", actually
46+
# GoVernance) were filed under categories they have nothing to do with. Writing
47+
# the name down makes the belief checkable; CLADE-006 enforces the pair.
48+
clade_name() {
49+
case "$1" in
50+
fv) echo "Formal Verification & Proofs" ;;
51+
nl) echo "Nextgen Languages" ;;
52+
rm) echo "Repo Management & Tooling" ;;
53+
gv) echo "Governance & Standards" ;;
54+
db) echo "Databases" ;;
55+
ap) echo "Applications" ;;
56+
ix) echo "Infrastructure & Cloud" ;;
57+
dx) echo "Developer Ecosystem" ;;
58+
pt) echo "Protocols & Interop" ;;
59+
ax) echo "AI & Neurosymbolic" ;;
60+
gm) echo "Games & Interactive" ;;
61+
sc) echo "Security" ;;
62+
*) echo "" ;;
63+
esac
64+
}
65+
4066
deploy_clade() {
4167
local repo_name="$1"
4268
local primary="$2"
@@ -120,6 +146,17 @@ deploy_clade() {
120146
# Determine prefixed name
121147
local prefixed="${primary}-${repo_name}"
122148

149+
# Refuse to write an identity we cannot name. An unknown code here means the
150+
# seed disagrees with the taxonomy — fail loudly rather than emit a CLADE.a2ml
151+
# that would fail CLADE-006 downstream in someone else's repo.
152+
local primary_name
153+
primary_name=$(clade_name "$primary")
154+
if [[ -z "$primary_name" ]]; then
155+
echo "ERROR: $repo_name — clade code '$primary' is not one of the 12 in clades.a2ml. Skipping." >&2
156+
((FAILED++))
157+
return
158+
fi
159+
123160
# Ensure .machine_readable/ exists
124161
mkdir -p "$repo_path/.machine_readable"
125162

@@ -137,7 +174,10 @@ canonical-name = "$repo_name"
137174
prefixed-name = "$prefixed"
138175
139176
[clade]
177+
# The 2 letters abbreviate the CLADE's name below — never the repo's name.
178+
# See gv-clade-index: verisim/seed/clades.a2ml for all 12 codes.
140179
primary = "$primary"
180+
primary-name = "$primary_name"
141181
secondary = $secondary
142182
assigned = "2026-03-16"
143183
rationale = "$description"
@@ -186,9 +226,33 @@ echo "Repos dir: $REPOS_DIR"
186226
[[ "$DRY_RUN" == "--dry-run" ]] && echo "MODE: DRY RUN"
187227
echo ""
188228

189-
while IFS=$'\t' read -r name primary secondary lineage parent description owner; do
229+
# Read the parser's TSV with the tabs translated to US (0x1f) first.
230+
#
231+
# TAB IS IFS WHITESPACE, so `IFS=$'\t' read` COLLAPSES a run of tabs into one
232+
# delimiter and empty fields vanish, shifting every field after them. 316 of the
233+
# 319 seed entries have an empty `parent`, so for nearly the whole registry:
234+
#
235+
# proven <tab> fv <tab> [] <tab> standalone <tab><tab> Formally verified… <tab> hyperpolymath
236+
# ^^ empty parent collapses
237+
# -> parent="Formally verified…" description="hyperpolymath" owner=""
238+
#
239+
# The damage is real and committed: 153 CLADE.a2ml files across the estate carry
240+
# `rationale = ""` with the repo's DESCRIPTION sitting in `parent` (panic-attack,
241+
# flat-mate, rpa-elysium, live-files, …). Repairing those files is a separate,
242+
# owner-facing job — this only stops the script producing more.
243+
#
244+
# It also silently defeated the `owner` support added in #48: $7 was always empty,
245+
# so `owner` always fell back to "hyperpolymath" and an explicit
246+
# `owner = "metadatastician"` in the seed was ignored. export-json.sh never had
247+
# this bug — awk -F'\t' does not collapse — which is why the two derivation sites
248+
# disagreed and why #48's byte-identical regression test did not catch it.
249+
#
250+
# US (0x1f) is not IFS whitespace, so runs are not collapsed and empty fields
251+
# survive. It cannot occur in the data (checked: 0 occurrences in repos.a2ml) and
252+
# is exactly what the ASCII unit separator is for.
253+
while IFS=$'\037' read -r name primary secondary lineage parent description owner; do
190254
deploy_clade "$name" "$primary" "$secondary" "$lineage" "$parent" "$description" "$owner"
191-
done < <(parse_repos)
255+
done < <(parse_repos | tr '\t' '\037')
192256

193257
echo ""
194258
echo "=== Summary ==="

verisim/seed/clades.a2ml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,56 @@
11
# SPDX-License-Identifier: MPL-2.0
22
# Clade taxonomy definitions for the hyperpolymath ecosystem
33
# 12 top-level clades — each repo gets exactly one primary clade
4+
#
5+
# ===========================================================================
6+
# READ THIS BEFORE ASSIGNING A CLADE — HUMAN OR BOT
7+
#
8+
# THIS IS A CLOSED TAXONOMY OF CATEGORIES. It is not an abbreviation scheme.
9+
#
10+
# The two letters abbreviate the NAME OF THE CLADE. They never abbreviate the
11+
# name of the repo. There are exactly 12 codes and no thirteenth may be coined:
12+
#
13+
# fv Formal Verification & Proofs nl Nextgen Languages
14+
# rm Repo Management & Tooling gv Governance & Standards
15+
# db Databases ap Applications
16+
# ix Infrastructure & Cloud dx Developer Ecosystem
17+
# pt Protocols & Interop ax AI & Neurosymbolic
18+
# gm Games & Interactive sc Security
19+
#
20+
# You pick the CATEGORY the repo belongs to, then write down that category's
21+
# code. You do NOT look at the repo's name and find two letters that fit it.
22+
#
23+
# This warning exists because that is exactly what happened, more than once:
24+
#
25+
# paint-type chose `pt` reading it as "PainT-type"
26+
# -- `pt` is Protocols & Interop. paint-type is an image
27+
# editor. It is `ap` (Applications).
28+
# gossamer chose `gv` reading it as "Graphical/Visual"
29+
# -- `gv` is GoVernance & Standards. There is no graphical
30+
# clade. A webview shell is `dx` (Developer Ecosystem).
31+
#
32+
# Both codes were VALID, so every check passed. The repos were simply filed
33+
# under a category they have nothing to do with. `primary-name` (below) exists
34+
# to make that failure impossible; see CLADE-006 in
35+
# contractiles/must/clade-hygiene.a2ml.
36+
#
37+
# HOW TO CHOOSE. From the spec (docs/SPEC-clade-verisim-portal.adoc):
38+
# "The primary clade reflects the project's core value proposition, not its
39+
# implementation details." Ask what the repo is FOR, not what it is built with.
40+
# A game written in Idris2 is `gm`, not `fv`. A webview shell with formal ABI
41+
# proofs is what developers build on, not a proof project.
42+
#
43+
# EVERY CLADE.a2ml MUST DECLARE BOTH:
44+
#
45+
# [clade]
46+
# primary = "ap"
47+
# primary-name = "Applications" # must match `name` below for that code
48+
#
49+
# The redundancy is deliberate: the code alone cannot express a wrong belief
50+
# about what it means, but the pair can — and CLADE-006 rejects a pair that
51+
# disagrees with this file. Writing primary = "pt" while believing it means
52+
# "paint-type" now fails a gate instead of passing silently.
53+
# ===========================================================================
454

555
[meta]
656
version = "1.0.0"

0 commit comments

Comments
 (0)