diff --git a/.machine_readable/CLADE.a2ml b/.machine_readable/CLADE.a2ml index d2b1d18..cd3accc 100644 --- a/.machine_readable/CLADE.a2ml +++ b/.machine_readable/CLADE.a2ml @@ -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" diff --git a/contractiles/must/clade-hygiene.a2ml b/contractiles/must/clade-hygiene.a2ml index 0e27d81..4a9668c 100644 --- a/contractiles/must/clade-hygiene.a2ml +++ b/contractiles/must/clade-hygiene.a2ml @@ -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" diff --git a/docs/decisions/0006-clade-lifecycle-status.adoc b/docs/decisions/0006-clade-lifecycle-status.adoc new file mode 100644 index 0000000..460c43b --- /dev/null +++ b/docs/decisions/0006-clade-lifecycle-status.adoc @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += 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). diff --git a/verisim/schema/clade.vcl b/verisim/schema/clade.vcl index 85e724d..155971b 100644 --- a/verisim/schema/clade.vcl +++ b/verisim/schema/clade.vcl @@ -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) @@ -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 { diff --git a/verisim/schema/repo.vcl b/verisim/schema/repo.vcl index 39a1b7c..6db8b21 100644 --- a/verisim/schema/repo.vcl +++ b/verisim/schema/repo.vcl @@ -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 { @@ -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") }; @@ -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, diff --git a/worker/src/index.js b/worker/src/index.js index b89b3a1..d71e5ec 100644 --- a/worker/src/index.js +++ b/worker/src/index.js @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell // // Cloudflare Worker: clade-registry-api // Read-only API serving the hyperpolymath clade registry. @@ -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- autolink (302 to portal; ?format=json) // // Storage: dual-store — Cloudflare KV (cache) + VeriSimDB (persistent). // loadIndex() prefers VeriSimDB and falls back to KV. @@ -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- autolink (302 to portal; ?format=json for data)', }; // Simple text search across name + description + clade. @@ -181,6 +183,40 @@ async function route(request, env, log, requestId) { ); } + // GET /c/:binomial — short resolver for the estate-wide CLADE- + // 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');