Skip to content

Commit fa71fd6

Browse files
feat(clade): 8-phase lifecycle status schema + CLADE- autolink resolver (#38)
Adds the lifecycle layer on top of the clade taxonomy (ADR 0006). Identity (uuid) and status (phase) are separated: uuid immortal, phase a mutable pointer; transitions legal in both directions (extinct->active is valid). - repo.vcl 1.1.0: status enum -> 8 phases (reserved incubating active dormant | merged superseded archived extinct); + status_since, present (derived), aliases[], merged_into, superseded_by, successors[], ended, status_history[]. - clade.vcl 1.1.0: aggregate counts realigned (present/active/dormant/ended). - CLADE.a2ml: [status] block exemplar (this repo's own declaration). - clade-hygiene: CLADE-004 (phase present), CLADE-005 (phase valid). - worker /c/:binomial: resolves the estate CLADE-<binomial> autolink (302 to portal, ?format=json for data). Additive; node --check clean. Suite not run (no node_modules in this checkout). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 99bf5bd commit fa71fd6

6 files changed

Lines changed: 202 additions & 7 deletions

File tree

.machine_readable/CLADE.a2ml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,31 @@ bitbucket = "hyperpolymath/gv-clade-index"
2323
type = "standalone"
2424
parent = ""
2525
born = "2026-03-16"
26+
27+
# Lifecycle status. Identity (uuid) and status are SEPARATE layers: the uuid above
28+
# is immortal; `phase` is a mutable pointer to the current life-phase. Transitions
29+
# are legal in both directions — no phase is terminal (extinct -> active is a valid
30+
# "Gitassic Park" resurrection on the same uuid). A rename is NOT a phase change:
31+
# the old prefixed-name goes to `aliases`, the uuid and phase are untouched.
32+
[status]
33+
# One of: reserved incubating active dormant | merged superseded archived extinct
34+
# living -> reserved incubating active dormant
35+
# ended -> merged superseded archived extinct
36+
phase = "active"
37+
since = "2026-03-16" # when the current phase began (status-since)
38+
present = true # derived here/not-here-as-itself:
39+
# here = reserved|incubating|active|dormant|archived
40+
# not-here = merged|superseded|extinct
41+
# Companion fields — fill only the ones the current phase makes meaningful:
42+
aliases = [] # former prefixed-names (renames; uuid is permanent)
43+
merged-into = "" # uuid or binomial of target when phase=merged
44+
superseded-by = "" # uuid or binomial of successor when phase=superseded
45+
successors = [] # forward links (splits / spinoffs)
46+
ended = "" # date an ended-family phase was first entered
47+
48+
# Append-only arc. NEVER rewrite earlier rows — add a new one on every transition.
49+
# This is the audit trail that makes resurrections legible.
50+
[[status.history]]
51+
phase = "active"
52+
since = "2026-03-16"
53+
note = "initial registration"

contractiles/must/clade-hygiene.a2ml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,25 @@ scope = "pre-commit"
3333
[must.check]
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"
36+
37+
[must]
38+
id = "CLADE-004"
39+
name = "clade-status-phase-set"
40+
description = "CLADE.a2ml must declare a lifecycle status phase"
41+
severity = "BLOCK"
42+
scope = "pre-commit"
43+
44+
[must.check]
45+
command = "grep -qE '^phase = ' .machine_readable/CLADE.a2ml"
46+
message = "CLADE.a2ml missing [status] phase — run 'contractile backfill-status' (default: active)"
47+
48+
[must]
49+
id = "CLADE-005"
50+
name = "clade-status-phase-valid"
51+
description = "Status phase must be one of the 8 lifecycle phases"
52+
severity = "BLOCK"
53+
scope = "pre-commit"
54+
55+
[must.check]
56+
command = "grep -E '^phase = ' .machine_readable/CLADE.a2ml | head -1 | grep -qE '\"(reserved|incubating|active|dormant|merged|superseded|archived|extinct)\"'"
57+
message = "CLADE.a2ml status phase not in valid set: reserved incubating active dormant merged superseded archived extinct"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
= ADR 0006 — Clade Lifecycle Status (8 phases)
4+
5+
Date: 2026-06-24
6+
7+
== Status
8+
9+
Accepted.
10+
11+
== Context
12+
13+
ADR 0002 fixed the clade _taxonomy_ (where a repo sits: genus/species). It did
14+
not say anything about a repo's _life-phase_ (whether it is alive, ended, or
15+
something in between). The pre-existing `repo.vcl` schema carried an ad-hoc
16+
five-value status enum — `active stale archived experimental complete` — that
17+
conflated unlike things (`complete` is a completion metric, not a life-phase;
18+
`experimental` overlaps `incubating`) and had no room for the ended family
19+
(merged, superseded, extinct) or for renames.
20+
21+
Two failure modes had to be designed out:
22+
23+
. *The "misc"/redo trap* — a status set so coarse that real situations don't fit,
24+
so people invent a junk bucket or leave it blank, and the field rots until it
25+
is worthless.
26+
. *Identity drift* — treating a rename or a resurrection as if it created a new
27+
thing, losing the through-line of a single repository's history.
28+
29+
== Decision
30+
31+
Separate two layers that were previously tangled:
32+
33+
* *Identity* (`uuid`) is immortal. It never changes for the life of the repo —
34+
not on rename, not on archival, not on resurrection.
35+
* *Status* (`phase`) is a mutable pointer to the current life-phase. Transitions
36+
are legal in *both* directions; no phase is terminal.
37+
38+
*Eight phases*, mutually exclusive and exhaustive, in two families:
39+
40+
[cols="1,3"]
41+
|===
42+
| Living | `reserved` · `incubating` · `active` · `dormant`
43+
| Ended | `merged` · `superseded` · `archived` · `extinct`
44+
|===
45+
46+
Specifics live in *fields*, not in the enum, so the phase set stays small and
47+
never needs a "misc" bucket:
48+
49+
* `status_since` — when the current phase began.
50+
* `present` — derived boolean (here / not-here-as-itself): `true` for
51+
`reserved|incubating|active|dormant|archived`, `false` for
52+
`merged|superseded|extinct`. The crisp binary, alongside the rich phase.
53+
* `aliases[]` — former prefixed-names. A *rename is not a phase change*: the old
54+
binomial moves here; `uuid` and `phase` are untouched.
55+
* `merged_into`, `superseded_by`, `successors[]` — the ended-family links.
56+
* `status_history[]` — append-only arc of `{phase, since, note}`. It records the
57+
full path, including resurrections: `extinct -> active` ("a Gitassic Park
58+
event") is a legal transition on the *same* uuid. Earlier rows are never
59+
rewritten.
60+
61+
The per-repo declaration lives in `.machine_readable/CLADE.a2ml` under a
62+
`[status]` block (see this repo's own CLADE.a2ml as the reference exemplar).
63+
`clade-hygiene` gains CLADE-004 (phase present) and CLADE-005 (phase in the valid
64+
set). Existing repos are backfilled by an idempotent farm sweep
65+
(`clade-status-backfill.sh`), defaulting to `active`.
66+
67+
== Consequences
68+
69+
=== Positive
70+
- One immortal id + one mutable phase = no identity drift across rename/archive/revival.
71+
- Small, exhaustive enum → no "misc" bucket, no periodic re-do.
72+
- `present` gives consumers the cheap here/not-here answer; `status_history`
73+
preserves the full arc for anyone who needs it.
74+
75+
=== Negative / Neutral
76+
- The old five-value enum is superseded; any tooling reading the previous values
77+
(`stale`, `experimental`, `complete`) must map them
78+
(`stale -> dormant`, `experimental -> incubating`, `complete -> active`).
79+
- `present` is derived, so writers must keep it consistent with `phase` (the
80+
backfill sweep and hygiene checks enforce this).
81+
82+
== References
83+
84+
- ADR 0002 — Clade Taxonomy (the genus/species layer this complements).
85+
- `verisim/schema/repo.vcl` (SEMANTIC + TEMPORAL modalities, schema 1.1.0).

verisim/schema/clade.vcl

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
-- SPDX-License-Identifier: MPL-2.0
22
-- VQL schema: Clade definition octad
33

4-
DEFINE SCHEMA clade VERSION 1.0.0;
4+
DEFINE SCHEMA clade VERSION 1.1.0;
5+
-- 1.1.0: aggregate counts realigned to the repo lifecycle phases (repo.vcl 1.1.0).
56

67
DEFINE GRAPH clade {
78
members: [UUID], -- repos in this clade (primary membership)
@@ -13,10 +14,12 @@ DEFINE GRAPH clade {
1314
};
1415

1516
DEFINE TENSOR clade {
16-
member_count: INT,
17+
member_count: INT, -- all members regardless of phase
1718
avg_completion: FLOAT,
18-
active_count: INT,
19-
stale_count: INT
19+
present_count: INT, -- members with present=true (here-as-themselves)
20+
active_count: INT, -- members in phase=active
21+
dormant_count: INT, -- members in phase=dormant
22+
ended_count: INT -- members in merged|superseded|archived|extinct
2023
} TEMPORAL;
2124

2225
DEFINE SEMANTIC clade {

verisim/schema/repo.vcl

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
-- VQL schema: Repository octad
33
-- Each repository is an 8-modality entity in VeriSimDB
44

5-
DEFINE SCHEMA repo VERSION 1.0.0;
5+
DEFINE SCHEMA repo VERSION 1.1.0;
6+
-- 1.1.0: lifecycle — 8-phase status enum + companion fields + status_history.
7+
-- Identity (uuid) and status are SEPARATE layers: uuid is immortal, status is a
8+
-- mutable current-phase pointer; transitions in both directions are legal.
69

710
-- Graph modality: relationships between repos
811
DEFINE GRAPH repo {
@@ -45,7 +48,20 @@ DEFINE SEMANTIC repo {
4548
license: STRING, -- SPDX identifier
4649
languages: [STRING], -- programming languages used
4750
version: STRING?, -- current version if applicable
48-
status: ENUM("active", "stale", "archived", "experimental", "complete"),
51+
-- Lifecycle phase: mutually exclusive, exhaustive. Living = reserved..dormant;
52+
-- ended = merged..extinct. Rename is NOT a phase (uuid is permanent → the old
53+
-- prefixed-name goes to aliases[], status stays where it was).
54+
status: ENUM("reserved", "incubating", "active", "dormant",
55+
"merged", "superseded", "archived", "extinct"),
56+
status_since: TIMESTAMP, -- when the CURRENT phase began
57+
present: BOOL, -- derived here/not-here-as-itself:
58+
-- here = reserved|incubating|active|dormant|archived
59+
-- not-here = merged|superseded|extinct
60+
aliases: [STRING], -- former prefixed-names (renames; uuid unchanged)
61+
merged_into: UUID?, -- target repo when status=merged
62+
superseded_by: UUID?, -- successor when status=superseded
63+
successors: [UUID], -- forward links (splits / spinoffs)
64+
ended: TIMESTAMP?, -- when an ended-family phase was first entered
4965
lineage_type: ENUM("standalone", "monorepo", "monorepo-child", "inflated", "deflated")
5066
};
5167

@@ -65,6 +81,11 @@ DEFINE TEMPORAL repo {
6581
last_commit: TIMESTAMP?, -- most recent git commit
6682
last_state_change: TIMESTAMP?,-- last meaningful state update
6783
last_activity: TIMESTAMP, -- max(commit, state_change, bot_finding)
84+
status_history: [{ -- append-only lifecycle arc. Records the full path
85+
phase: STRING, -- incl. resurrections — extinct->active ("Gitassic
86+
since: TIMESTAMP, -- Park") is a legal transition on the SAME uuid;
87+
note: STRING? -- no phase is terminal.
88+
}],
6889
sessions: [{ -- work sessions
6990
id: STRING,
7091
started: TIMESTAMP,

worker/src/index.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MPL-2.0
2-
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
33
//
44
// Cloudflare Worker: clade-registry-api
55
// Read-only API serving the hyperpolymath clade registry.
@@ -16,6 +16,7 @@
1616
// GET /v1/ready — Readiness check (probes VeriSimDB; "degraded" on KV fallback)
1717
// GET /v1/query/language?q=.. — Query repos by language (VeriSimDB-backed)
1818
// GET /v1/query/tag?q=... — Query repos by tag (VeriSimDB-backed)
19+
// GET /c/:binomial — Resolve a CLADE-<binomial> autolink (302 to portal; ?format=json)
1920
//
2021
// Storage: dual-store — Cloudflare KV (cache) + VeriSimDB (persistent).
2122
// loadIndex() prefers VeriSimDB and falls back to KV.
@@ -40,6 +41,7 @@ const ENDPOINT_DOCS = {
4041
'/v1/query/tag?q=...': 'Query repos by tag (VeriSimDB)',
4142
'/v1/health': 'Liveness check',
4243
'/v1/ready': 'Readiness check (backing-store health)',
44+
'/c/:binomial': 'Resolve a CLADE-<binomial> autolink (302 to portal; ?format=json for data)',
4345
};
4446

4547
// Simple text search across name + description + clade.
@@ -181,6 +183,40 @@ async function route(request, env, log, requestId) {
181183
);
182184
}
183185

186+
// GET /c/:binomial — short resolver for the estate-wide CLADE-<binomial>
187+
// custom autolink (e.g. an issue mentioning CLADE-fv-proven links here).
188+
// Resolves by prefixed-name (the binomial) first, then canonical name.
189+
// Humans get a 302 to the portal; bots can ask for JSON with ?format=json.
190+
const cladeRefMatch = path.match(/^\/c\/(.+)$/);
191+
if (cladeRefMatch) {
192+
const ref = safeDecode(cladeRefMatch[1]);
193+
if (ref === null) return error('Malformed clade reference', { status: 400, requestId });
194+
const repo =
195+
Object.values(index.by_name).find((r) => r.prefixed === ref) || index.by_name[ref] || null;
196+
if (!repo) return error(`No repo for clade reference '${ref}'`, { requestId });
197+
if (url.searchParams.get('format') === 'json') {
198+
const clade = index.clades.find((c) => c.code === repo.clade);
199+
return json(
200+
{
201+
...repo,
202+
clade_name: clade ? clade.name : repo.clade,
203+
github_url: `https://github.com/${repo.github}`,
204+
portal_url: `https://hyperpolymath.github.io/#/project/${repo.name}`,
205+
},
206+
{ requestId },
207+
);
208+
}
209+
return new Response(null, {
210+
status: 302,
211+
headers: {
212+
Location: `https://hyperpolymath.github.io/#/project/${repo.name}`,
213+
'Cache-Control': 'public, max-age=300',
214+
'X-Request-Id': requestId,
215+
...SECURITY_HEADERS,
216+
},
217+
});
218+
}
219+
184220
// GET /v1/repos?clade=xx&page=1&limit=50
185221
if (path === '/v1/repos') {
186222
const cladeFilter = url.searchParams.get('clade');

0 commit comments

Comments
 (0)