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
28 changes: 28 additions & 0 deletions .machine_readable/CLADE.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,31 @@ bitbucket = "hyperpolymath/gv-clade-index"
type = "standalone"
parent = ""
born = "2026-03-16"

# Lifecycle status. Identity (uuid) and status are SEPARATE layers: the uuid above
# is immortal; `phase` is a mutable pointer to the current life-phase. Transitions
# are legal in both directions — no phase is terminal (extinct -> active is a valid
# "Gitassic Park" resurrection on the same uuid). A rename is NOT a phase change:
# the old prefixed-name goes to `aliases`, the uuid and phase are untouched.
[status]
# One of: reserved incubating active dormant | merged superseded archived extinct
# living -> reserved incubating active dormant
# ended -> merged superseded archived extinct
phase = "active"
since = "2026-03-16" # when the current phase began (status-since)
present = true # derived here/not-here-as-itself:
# here = reserved|incubating|active|dormant|archived
# not-here = merged|superseded|extinct
# Companion fields — fill only the ones the current phase makes meaningful:
aliases = [] # former prefixed-names (renames; uuid is permanent)
merged-into = "" # uuid or binomial of target when phase=merged
superseded-by = "" # uuid or binomial of successor when phase=superseded
successors = [] # forward links (splits / spinoffs)
ended = "" # date an ended-family phase was first entered

# Append-only arc. NEVER rewrite earlier rows — add a new one on every transition.
# This is the audit trail that makes resurrections legible.
[[status.history]]
phase = "active"
since = "2026-03-16"
note = "initial registration"
22 changes: 22 additions & 0 deletions contractiles/must/clade-hygiene.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,25 @@ scope = "pre-commit"
[must.check]
command = "grep 'primary = ' .machine_readable/CLADE.a2ml | grep -qE '\"(fv|nl|rm|gv|db|ap|ix|dx|pt|ax|gm|sc)\"'"
message = "CLADE.a2ml primary clade code not in valid set: fv nl rm gv db ap ix dx pt ax gm sc"

[must]
id = "CLADE-004"
name = "clade-status-phase-set"
description = "CLADE.a2ml must declare a lifecycle status phase"
severity = "BLOCK"
scope = "pre-commit"

[must.check]
command = "grep -qE '^phase = ' .machine_readable/CLADE.a2ml"
message = "CLADE.a2ml missing [status] phase — run 'contractile backfill-status' (default: active)"

[must]
id = "CLADE-005"
name = "clade-status-phase-valid"
description = "Status phase must be one of the 8 lifecycle phases"
severity = "BLOCK"
scope = "pre-commit"

[must.check]
command = "grep -E '^phase = ' .machine_readable/CLADE.a2ml | head -1 | grep -qE '\"(reserved|incubating|active|dormant|merged|superseded|archived|extinct)\"'"
message = "CLADE.a2ml status phase not in valid set: reserved incubating active dormant merged superseded archived extinct"
85 changes: 85 additions & 0 deletions docs/decisions/0006-clade-lifecycle-status.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
= ADR 0006 — Clade Lifecycle Status (8 phases)

Date: 2026-06-24

== Status

Accepted.

== Context

ADR 0002 fixed the clade _taxonomy_ (where a repo sits: genus/species). It did
not say anything about a repo's _life-phase_ (whether it is alive, ended, or
something in between). The pre-existing `repo.vcl` schema carried an ad-hoc
five-value status enum — `active stale archived experimental complete` — that
conflated unlike things (`complete` is a completion metric, not a life-phase;
`experimental` overlaps `incubating`) and had no room for the ended family
(merged, superseded, extinct) or for renames.

Two failure modes had to be designed out:

. *The "misc"/redo trap* — a status set so coarse that real situations don't fit,
so people invent a junk bucket or leave it blank, and the field rots until it
is worthless.
. *Identity drift* — treating a rename or a resurrection as if it created a new
thing, losing the through-line of a single repository's history.

== Decision

Separate two layers that were previously tangled:

* *Identity* (`uuid`) is immortal. It never changes for the life of the repo —
not on rename, not on archival, not on resurrection.
* *Status* (`phase`) is a mutable pointer to the current life-phase. Transitions
are legal in *both* directions; no phase is terminal.

*Eight phases*, mutually exclusive and exhaustive, in two families:

[cols="1,3"]
|===
| Living | `reserved` · `incubating` · `active` · `dormant`
| Ended | `merged` · `superseded` · `archived` · `extinct`
|===

Specifics live in *fields*, not in the enum, so the phase set stays small and
never needs a "misc" bucket:

* `status_since` — when the current phase began.
* `present` — derived boolean (here / not-here-as-itself): `true` for
`reserved|incubating|active|dormant|archived`, `false` for
`merged|superseded|extinct`. The crisp binary, alongside the rich phase.
* `aliases[]` — former prefixed-names. A *rename is not a phase change*: the old
binomial moves here; `uuid` and `phase` are untouched.
* `merged_into`, `superseded_by`, `successors[]` — the ended-family links.
* `status_history[]` — append-only arc of `{phase, since, note}`. It records the
full path, including resurrections: `extinct -> active` ("a Gitassic Park
event") is a legal transition on the *same* uuid. Earlier rows are never
rewritten.

The per-repo declaration lives in `.machine_readable/CLADE.a2ml` under a
`[status]` block (see this repo's own CLADE.a2ml as the reference exemplar).
`clade-hygiene` gains CLADE-004 (phase present) and CLADE-005 (phase in the valid
set). Existing repos are backfilled by an idempotent farm sweep
(`clade-status-backfill.sh`), defaulting to `active`.

== Consequences

=== Positive
- One immortal id + one mutable phase = no identity drift across rename/archive/revival.
- Small, exhaustive enum → no "misc" bucket, no periodic re-do.
- `present` gives consumers the cheap here/not-here answer; `status_history`
preserves the full arc for anyone who needs it.

=== Negative / Neutral
- The old five-value enum is superseded; any tooling reading the previous values
(`stale`, `experimental`, `complete`) must map them
(`stale -> dormant`, `experimental -> incubating`, `complete -> active`).
- `present` is derived, so writers must keep it consistent with `phase` (the
backfill sweep and hygiene checks enforce this).

== References

- ADR 0002 — Clade Taxonomy (the genus/species layer this complements).
- `verisim/schema/repo.vcl` (SEMANTIC + TEMPORAL modalities, schema 1.1.0).
11 changes: 7 additions & 4 deletions verisim/schema/clade.vcl
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
-- SPDX-License-Identifier: MPL-2.0
-- VQL schema: Clade definition octad

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

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

DEFINE TENSOR clade {
member_count: INT,
member_count: INT, -- all members regardless of phase
avg_completion: FLOAT,
active_count: INT,
stale_count: INT
present_count: INT, -- members with present=true (here-as-themselves)
active_count: INT, -- members in phase=active
dormant_count: INT, -- members in phase=dormant
ended_count: INT -- members in merged|superseded|archived|extinct
} TEMPORAL;

DEFINE SEMANTIC clade {
Expand Down
25 changes: 23 additions & 2 deletions verisim/schema/repo.vcl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
-- VQL schema: Repository octad
-- Each repository is an 8-modality entity in VeriSimDB

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

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

Expand All @@ -65,6 +81,11 @@ DEFINE TEMPORAL repo {
last_commit: TIMESTAMP?, -- most recent git commit
last_state_change: TIMESTAMP?,-- last meaningful state update
last_activity: TIMESTAMP, -- max(commit, state_change, bot_finding)
status_history: [{ -- append-only lifecycle arc. Records the full path
phase: STRING, -- incl. resurrections — extinct->active ("Gitassic
since: TIMESTAMP, -- Park") is a legal transition on the SAME uuid;
note: STRING? -- no phase is terminal.
}],
sessions: [{ -- work sessions
id: STRING,
started: TIMESTAMP,
Expand Down
38 changes: 37 additions & 1 deletion worker/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//
// Cloudflare Worker: clade-registry-api
// Read-only API serving the hyperpolymath clade registry.
Expand All @@ -16,6 +16,7 @@
// GET /v1/ready — Readiness check (probes VeriSimDB; "degraded" on KV fallback)
// GET /v1/query/language?q=.. — Query repos by language (VeriSimDB-backed)
// GET /v1/query/tag?q=... — Query repos by tag (VeriSimDB-backed)
// GET /c/:binomial — Resolve a CLADE-<binomial> autolink (302 to portal; ?format=json)
//
// Storage: dual-store — Cloudflare KV (cache) + VeriSimDB (persistent).
// loadIndex() prefers VeriSimDB and falls back to KV.
Expand All @@ -40,6 +41,7 @@ const ENDPOINT_DOCS = {
'/v1/query/tag?q=...': 'Query repos by tag (VeriSimDB)',
'/v1/health': 'Liveness check',
'/v1/ready': 'Readiness check (backing-store health)',
'/c/:binomial': 'Resolve a CLADE-<binomial> autolink (302 to portal; ?format=json for data)',
};

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

// GET /c/:binomial — short resolver for the estate-wide CLADE-<binomial>
// custom autolink (e.g. an issue mentioning CLADE-fv-proven links here).
// Resolves by prefixed-name (the binomial) first, then canonical name.
// Humans get a 302 to the portal; bots can ask for JSON with ?format=json.
const cladeRefMatch = path.match(/^\/c\/(.+)$/);
if (cladeRefMatch) {
const ref = safeDecode(cladeRefMatch[1]);
if (ref === null) return error('Malformed clade reference', { status: 400, requestId });
const repo =
Object.values(index.by_name).find((r) => r.prefixed === ref) || index.by_name[ref] || null;
if (!repo) return error(`No repo for clade reference '${ref}'`, { requestId });
if (url.searchParams.get('format') === 'json') {
const clade = index.clades.find((c) => c.code === repo.clade);
return json(
{
...repo,
clade_name: clade ? clade.name : repo.clade,
github_url: `https://github.com/${repo.github}`,
portal_url: `https://hyperpolymath.github.io/#/project/${repo.name}`,
},
{ requestId },
);
}
return new Response(null, {
status: 302,
headers: {
Location: `https://hyperpolymath.github.io/#/project/${repo.name}`,
'Cache-Control': 'public, max-age=300',
'X-Request-Id': requestId,
...SECURITY_HEADERS,
},
});
}

// GET /v1/repos?clade=xx&page=1&limit=50
if (path === '/v1/repos') {
const cladeFilter = url.searchParams.get('clade');
Expand Down
Loading