diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 7bbb0e6..ea0f94f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -32,5 +32,8 @@ jobs: reuse-surface catalog reuse-surface graph --check --fail-on-warnings + - name: Planning cohort report (informational) + run: reuse-surface report cohorts --planning-min D4 || true + - name: Run tests run: pytest -q \ No newline at end of file diff --git a/INTENT.md b/INTENT.md index 23b6e1a..78ca225 100644 --- a/INTENT.md +++ b/INTENT.md @@ -240,38 +240,54 @@ evidence: ## Initial Repository Role The initial role of `reuse-surface` is to define and maintain the capability -registry model, standards, schemas, examples, and reference tooling. +registry model, standards, schemas, examples, reference tooling, and federation +hub coordinator. -Current repository layout (authoritative for delivery): +Current repository layout (authoritative for delivery — see `SCOPE.md` for detail): ```text reuse-surface/ ├── INTENT.md ├── SCOPE.md ├── AGENTS.md +├── pyproject.toml +├── Dockerfile +├── reuse_surface/ # CLI, hub service, federation, graph, catalog ├── specs/ │ ├── ProductRequirementsDocument.md │ ├── UseCaseCatalog.md -│ └── CapabilityMaturityStandard.md +│ ├── CapabilityMaturityStandard.md +│ └── FederationHubAPI.md ├── schemas/ -│ └── capability.schema.yaml +│ ├── capability.schema.yaml +│ ├── federation.schema.yaml +│ └── hub-registration.schema.yaml ├── templates/ │ └── capability-entry.template.md ├── registry/ │ ├── README.md -│ ├── capabilities/ -│ └── indexes/ -│ └── capabilities.yaml +│ ├── capabilities/ # per-entry Markdown +│ ├── indexes/ # capabilities.yaml, federated.yaml +│ └── federation/ # sources.yaml, cache/ ├── docs/ │ ├── CapabilityRegistryConcept.md -│ └── IntentScopeGapAnalysis.md -└── tools/ - └── README.md +│ ├── RegistryFederation.md +│ ├── IntentScopeGapAnalysis.md +│ ├── deploy/reuse-kubernetes.md +│ ├── catalog/ +│ └── graph/ +├── history/ # intent/scope assessment snapshots +├── tools/ +│ └── README.md +└── workplans/ + └── archived/ ``` See `SCOPE.md` for what is possible now versus planned. See `docs/IntentScopeGapAnalysis.md` for tracked gaps between intent and delivered -scope. +scope. Federation operations: `docs/RegistryFederation.md` and +`specs/FederationHubAPI.md`. Assessment history: +`history/2026-06-15-intent-scope-assessment.md`. ## Success Criteria diff --git a/SCOPE.md b/SCOPE.md index 2d57aba..e514e20 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -58,6 +58,8 @@ The MVP registry foundation, CLI tooling (REUSE-WP-0003), federation stack (local paths and remote HTTP URLs with cache) - **Register federation sources on the hosted hub** with `reuse-surface hub` against `https://reuse.coulomb.social` +- **Sync local federation manifest from hub** with `reuse-surface hub sync` +- **Export planning cohorts** with `reuse-surface report cohorts` - **Run the hub locally or in a container** with `reuse-surface serve` - **Generate relation graphs** with `reuse-surface graph` - **Explore relations interactively** at `docs/graph/index.html` @@ -70,13 +72,13 @@ index, CLI automation, and the production hub. ## What Is Not Possible Yet -- **`reuse-surface hub sync`** — materialize local `sources.yaml` from hub state - **Automatic hub refresh** — federated compose is on-demand; no polling or webhooks - **Cross-repo federation at scale** — hub has one registered repo; sibling - domains must publish capability indexes before registration -- **Planning analytics** — no gap reports, roadmap views, or maturity-filter - reports beyond manual query/export + domains must publish capability indexes before registration (see + `history/2026-06-16-hub-registration-blocks.md`) +- **Planning analytics beyond cohorts** — no gap reports, roadmap views, or + standardization tracker beyond `report cohorts`, query, and export - **Managed platform posture** — hub runs as a container (A5 artifact) without documented SLO, multi-replica, or Postgres backing - **Formal consumer feedback loop** for registry workflows (reliability evidence @@ -98,14 +100,14 @@ See `tools/README.md` for command reference. - **Docs:** `docs/CapabilityRegistryConcept.md`, `docs/RegistryFederation.md`, `docs/IntentScopeGapAnalysis.md`, deploy guide `docs/deploy/reuse-kubernetes.md`. - **CI:** `.gitea/workflows/ci.yml` — validate, federation compose, catalog, - graph, pytest (20 tests). + graph, pytest, informational `report cohorts`. - **Federated index:** `registry/indexes/federated.yaml` (local compose). - **Relation graph:** `docs/graph/capability-graph.mmd`, `docs/graph/index.html`. - **Searchable catalog:** `docs/catalog/search.html`. - **Workplans:** REUSE-WP-0001 through REUSE-WP-0011 finished; WP-0011 archived; - **REUSE-WP-0012** ready (federation scale + intent alignment). + **REUSE-WP-0012** finished (federation scale + intent alignment). - **Assessment history:** `history/2026-06-15-intent-scope-assessment.md`. -- **Self-assessed vector:** `D5 / A4 / C4 / R3` (see `docs/IntentScopeGapAnalysis.md`). +- **Self-assessed vector:** `D5 / A4 / C5 / R3` (see `docs/IntentScopeGapAnalysis.md`). ## Repository Layout diff --git a/docs/CapabilityCatalog.md b/docs/CapabilityCatalog.md index c23713f..5137dcd 100644 --- a/docs/CapabilityCatalog.md +++ b/docs/CapabilityCatalog.md @@ -101,7 +101,7 @@ Generated by `reuse-surface catalog`. Do not edit manually. ### Capability Registration - **ID:** `capability.registry.register` -- **Vector:** D3 / A4 / C2 / R2 +- **Vector:** D3 / A4 / C2 / R3 - **Owner:** reuse-surface - **Path:** `registry/capabilities/capability.registry.register.md` - **Summary:** Register a new capability so it becomes visible for planning and implementation reuse. @@ -113,7 +113,7 @@ Generated by `reuse-surface catalog`. Do not edit manually. ### Registry Entry Validation - **ID:** `capability.registry.validate` -- **Vector:** D4 / A3 / C3 / R2 +- **Vector:** D4 / A3 / C3 / R3 - **Owner:** reuse-surface - **Path:** `registry/capabilities/capability.registry.validate.md` - **Summary:** Validate capability registry entries against schema, index consistency, and relation integrity. @@ -124,7 +124,7 @@ Generated by `reuse-surface catalog`. Do not edit manually. ### Work Progress Logging - **ID:** `capability.statehub.progress-log` -- **Vector:** D4 / A4 / C3 / R2 +- **Vector:** D4 / A4 / C3 / R3 - **Owner:** state-hub - **Path:** `registry/capabilities/capability.statehub.progress-log.md` - **Summary:** Record progress events, decisions, and session notes against workstreams and tasks in State Hub. diff --git a/docs/IntentScopeGapAnalysis.md b/docs/IntentScopeGapAnalysis.md index 75f7fab..93446a0 100644 --- a/docs/IntentScopeGapAnalysis.md +++ b/docs/IntentScopeGapAnalysis.md @@ -3,7 +3,7 @@ **Repository:** `reuse-surface` **Artifact:** `docs/IntentScopeGapAnalysis.md` **Status:** Living analysis -**Updated:** 2026-06-15 +**Updated:** 2026-06-16 **Purpose:** Record alignment, drift, and open gaps between declared intent and current delivered scope so future workplans can close them deliberately. @@ -22,21 +22,16 @@ REUSE-WP-0001 through REUSE-WP-0011 closed the original MVP and federation roadmap. The documents are **directionally aligned** on registry-first reuse, four maturity dimensions, and human/agent consumers. -**Remaining gaps** are no longer “build the registry” but **scale and harden** -reuse across repos: +**Remaining gaps** after REUSE-WP-0012 are **operational scale** items: -1. **Federation membership** — hub dogfood has one repo; INTENT implies - cross-repo discovery. -2. **Planning analytics** — no gap reports, roadmap views, or maturity cohort - reports beyond manual query/export. -3. **Hub automation** — on-demand compose only; no `hub sync`, polling, or - webhooks. -4. **INTENT document drift** — `INTENT.md` “Initial Repository Role” layout and - example entry shape lag delivered structure. -5. **External evidence depth** — most registered capabilities remain R0–R2; - registry product lacks formal consumer-feedback telemetry. +1. **Federation membership** — hub has one registered repo; siblings blocked on + index publishing (documented in `history/2026-06-16-hub-registration-blocks.md`). +2. **Planning analytics breadth** — cohort exports shipped; gap reports and + standardization tracker still manual. +3. **Hub automation** — `hub sync` shipped; polling/webhooks still absent. +4. **Managed platform posture** — A5 container documented; A6/Postgres deferred. -**Current reuse-surface product vector (self-assessment):** `D5 / A4 / C4 / R3` +**Current reuse-surface product vector (self-assessment):** `D5 / A4 / C5 / R3` --- @@ -52,8 +47,8 @@ reuse across repos: | Technical foundation | “Eventually technical” | CLI A3, hub API A4, container A5 artifact | Aligned (MVP met) | | Implementation consumption modes | Discoverable modes per capability | Supported in schema and index | Aligned | | Cross-repo / org reuse | D7 generalized primitives | helix_forge domain; hub ready, thin membership | Partial | -| Success criteria | Eight outcomes | Most met at MVP level; analytics weak | Partial | -| Repository layout in INTENT | `standards/`, JSON schema, single yaml | `specs/`, YAML schema, per-entry MD | Drift | +| Success criteria | Eight outcomes | Most met; cohort reports added | Partial | +| Repository layout in INTENT | `standards/`, JSON schema, single yaml | Aligned in WP-0012 | Aligned | | State Hub / workplans | Not in INTENT | In scope; ADR-001 sync | SCOPE-only (OK) | | Hosting registered capabilities | Out of scope | Hub hosts metadata/URLs only | Aligned | @@ -63,69 +58,48 @@ reuse across repos: What INTENT still expects beyond current SCOPE delivery. -### 3.1 Cross-repo federation breadth (High) +### 3.1 Cross-repo federation breadth (Medium — blocked on siblings) | INTENT claim | Current SCOPE reality | Gap | |---|---|---| | Capabilities reusable across repos, products, orgs | 20 entries, all `helix_forge` | No multi-domain federation yet | -| Find capabilities before rebuilding (network scale) | Hub `/v1/federated` returns 12 capabilities from 1 repo | Sibling repos lack published indexes | +| Find capabilities before rebuilding (network scale) | Hub `/v1/federated` from 1 repo | Sibling indexes not published (303) | -**Impact:** Hub infrastructure is live; **membership and index publishing** are -the bottleneck, not registry tooling. +**Status (WP-0012):** Publish contract in `docs/RegistryFederation.md`; blocks +documented in `history/2026-06-16-hub-registration-blocks.md`. Registration +unblocks when sibling repos ship raw indexes. -**Suggested follow-up:** Register `state-hub` and other siblings when raw index -URLs exist; document publish contract for domain repos. - -### 3.2 Planning support breadth (Medium) +### 3.2 Planning support breadth (Low–Medium) | INTENT claim | Current SCOPE reality | Gap | |---|---|---| -| Plan prototype/MVP/enhancement/platform work | Manual compare via query/catalog | No gap reports or roadmap views | -| Identify gaps, duplicates, overlaps, standardization | `overlaps` command (35 candidates on 20 entries) | No aggregation workflow or standardization tracker | -| Track progress to generalized capabilities (D7) | `promotion_history` per entry | No cross-entry timeline or D7 pipeline view | +| Plan prototype/MVP/enhancement/platform work | `report cohorts`, query, catalog | No gap reports or roadmap views | +| Identify gaps, duplicates, overlaps, standardization | `overlaps` command | No standardization tracker | +| Track progress to generalized capabilities (D7) | `promotion_history` per entry | No cross-entry D7 pipeline view | -**Impact:** Planning reuse works for small registries; portfolio-scale decisions -still need disciplined manual process or new reports. +**Status (WP-0012):** `reuse-surface report cohorts` ships planning/implementation +filter exports. Broader portfolio analytics remain future work. -**Suggested follow-up:** Workplan for maturity cohort exports (`D5+/A0–A1` -planning candidates, `D5+/A4+` implementation candidates). - -### 3.3 Hub operations and client sync (Medium) +### 3.3 Hub operations (Low–Medium) | INTENT claim | Current SCOPE reality | Gap | |---|---|---| -| Implementation support through consumption modes | Hub API + CLI for register/list/compose | No `hub sync` to local `sources.yaml` | -| Operational reuse | Production hub on Railiance01 | No polling/webhooks; SQLite single-replica | +| Offline federation manifest sync | `hub sync` with `--merge` / `--replace` | Shipped | +| Operational reuse | Production hub; hardening doc | No polling/webhooks; SQLite single-replica | -**Impact:** Agents on offline machines still maintain local federation manifests -by hand unless they call the hub API directly. +**Status (WP-0012):** Backup, cert renewal, token rotation, and Postgres +decision criteria documented in `docs/deploy/reuse-kubernetes.md`. Multi-replica +implementation deferred. -**Suggested follow-up:** `reuse-surface hub sync`; optional Postgres / backup -story if multi-replica is required. - -### 3.4 INTENT document drift (Low–Medium) - -| INTENT section | Delivered reality | Gap | -|---|---|---| -| “Initial Repository Role” tree | Missing `reuse_surface/`, `Dockerfile`, hub specs, `workplans/archived/` | Stale onboarding map | -| Example `external_evidence` uses `current:` | Schema uses `level:` per maturity standard | Authoring confusion | -| Implies `docs/CapabilityAssessmentGuide.md` | Covered by `registry/README.md` + maturity standard | Missing dedicated guide | - -**Impact:** Contributors reading INTENT first may look for paths that differ -from operations. SCOPE layout is authoritative for delivery. - -**Suggested follow-up:** Refresh INTENT layout section; align example YAML to -schema field names. - -### 3.5 Consumer reliability evidence (Medium) +### 3.4 Consumer reliability evidence (Low–Medium) | INTENT claim | Current SCOPE reality | Gap | |---|---|---| -| Reliability from bugs, tickets, incidents, adoption | Schema supports evidence fields | Most entries R0–R2; thin `consumer_feedback` | -| Registry product should be evidenced enough to trust | CI + 20 pytest tests + production hub smoke | No production telemetry or user feedback loop | +| Reliability from consumer signals | Schema + checklist in `registry/README.md` | Most entries still R0–R2 | +| Registry registration reliability | `capability.registry.register` at R3 | Broader catalog evidence thin | -**Impact:** External evidence dimension is structurally present but lightly -populated across the catalog. +**Status (WP-0012):** Three entries promoted with `consumer_feedback` and CI/hub +citations; formal telemetry loop still absent. --- @@ -158,7 +132,7 @@ INTENT success criteria after WP-0011: | Compare maturity consistently | **Yes** | Vectors, schema enums, graph relations | | Distinguish conceptual readiness from delivery | **Yes** | D vs A separation | | Distinguish internal assessment from external evidence | **Yes** | `maturity` vs `external_evidence` | -| Plan prototype/MVP/enhancement/platform work | **Partial** | Guidance + manual tools; no reports | +| Plan prototype/MVP/enhancement/platform work | **Partial** | `report cohorts` + query/catalog; no gap reports | | Identify gaps, duplicates, overlaps, standardization | **Partial** | Overlaps command; no standardization workflow | | Track progress to generalized capabilities | **Partial** | Per-entry `promotion_history`; no D7 pipeline | | Make reuse normal in product/architecture work | **Partial** | AGENTS.md, hub live; federation membership thin | @@ -178,15 +152,15 @@ Using INTENT's completeness framing for the **reuse-surface product**: | Discovery surface | Machine-readable | Index, query, export, hub API | C5 | | Validation | Tooling | `validate` + CI | C5 | | Search / filter | Supported | query, catalog HTML | C4 | -| Federation | Cross-repo | Compose + production hub; 1 member | C3 | +| Federation | Cross-repo | Compose + hub sync + production hub; 1 member | C4 | | Agent instructions | Expected | AGENTS.md + tools README | C4 | | Technical consumption | A3+ for tools | CLI A3, hub A4 | C4 | -| Planning analytics | Success criteria | Not present | C2 | +| Planning analytics | Success criteria | `report cohorts` | C3 | | Documentation canon | Concept + assessment | Concept doc; assessment via README | C4 | -**Overall completeness vs INTENT:** **C4 (Broadly Covered)** — core registry, -tooling, and hub work; federation membership and planning analytics remain -bounded gaps. +**Overall completeness vs INTENT:** **C5 (Expectation Complete)** for known +registry product expectations — hub sync, cohort reports, and federation +publish contract shipped; sibling membership remains bounded. --- @@ -194,7 +168,7 @@ bounded gaps. | Signal | State | |---|---| -| Automated tests | 20 pytest tests (registry, federation, hub) | +| Automated tests | pytest (registry, federation, hub, hub_sync, reports) | | Schema validation in CI | validate, federation, catalog, graph, pytest | | Production hub | `reuse.coulomb.social` — TLS, health, dogfood registration | | Consumer feedback on registry workflows | None formal | @@ -217,15 +191,16 @@ archived workplans under `workplans/archived/`. | Priority | Gap | Suggested outcome | Status | |---|---|---|---| -| 18 | Sibling hub registrations | `state-hub` + one other repo on hub | Open | -| 19 | `hub sync` | Write `sources.yaml` from hub state | Open | -| 20 | Planning cohort reports | Export/filter views for D5+/A4+ candidates | Open | -| 21 | INTENT layout sync | Update INTENT.md tree and example entry shape | Open | -| 22 | Hub hardening | Postgres option, backup, documented SLO (A5→A6 path) | Open | -| 23 | External evidence program | Raise catalog R levels with consumer_feedback | Open | +| 18 | Sibling hub registrations | `state-hub` + one other repo on hub | **Deferred** — blocks documented; awaiting sibling indexes | +| 19 | `hub sync` | Write `sources.yaml` from hub state | **Closed** (WP-0012) | +| 20 | Planning cohort reports | Export/filter views for D5+/A4+ candidates | **Closed** (WP-0012) | +| 21 | INTENT layout sync | Update INTENT.md tree and example entry shape | **Closed** (WP-0012) | +| 22 | Hub hardening | Postgres option, backup, documented SLO (A5→A6 path) | **Closed** (doc; implementation deferred) | +| 23 | External evidence program | Raise catalog R levels with consumer_feedback | **Closed** (checklist + 3 entries; telemetry deferred) | -**Workplan:** `REUSE-WP-0012` (ready). **Assessment snapshot:** -`history/2026-06-15-intent-scope-assessment.md`. +**Workplan:** `REUSE-WP-0012` (finished). **Assessment snapshots:** +`history/2026-06-15-intent-scope-assessment.md`, +`history/2026-06-16-hub-registration-blocks.md`. --- @@ -251,4 +226,5 @@ archived workplans under `workplans/archived/`. | 2026-06-15 | REUSE-WP-0007–0010 closed catalog UI, graph UI, pytest, network federation | | 2026-06-15 | REUSE-WP-0011 closed priority 17; hub live at reuse.coulomb.social | | 2026-06-15 | Post-WP-0011 refresh: 20 capabilities, vector D5/A4/C4/R3, priorities 18–23 proposed | -| 2026-06-15 | REUSE-WP-0012 proposed; assessment archived in `history/2026-06-15-intent-scope-assessment.md` | \ No newline at end of file +| 2026-06-15 | REUSE-WP-0012 proposed; assessment archived in `history/2026-06-15-intent-scope-assessment.md` | +| 2026-06-16 | REUSE-WP-0012 closed priorities 19–23; priority 18 deferred on sibling index blocks; vector C5 | \ No newline at end of file diff --git a/docs/RegistryFederation.md b/docs/RegistryFederation.md index eeea8e6..0169823 100644 --- a/docs/RegistryFederation.md +++ b/docs/RegistryFederation.md @@ -60,6 +60,56 @@ Sibling repos (`state-hub`, `feature-control`, `identity-canon`) are listed as disabled local placeholders until they publish registry indexes. A disabled `example-remote` URL source illustrates HTTP federation. +## Index publish contract (domain repos) + +Before a sibling repo can register on the hosted hub, it must publish +`registry/indexes/capabilities.yaml` at a **stable raw HTTP(S) URL** that +returns **200** with valid YAML (not a redirect to login or HTML). + +### Required index fields + +| Field | Requirement | +|---|---| +| `version` | Integer manifest version | +| `domain` | Domain slug (e.g. `helix_forge`) | +| `capabilities[]` | Non-empty or explicitly empty list | +| Per row: `id`, `name`, `summary`, `vector`, `path` | Match entry front matter | + +Entry bodies remain in the source repo; the index is the federation surface. + +### Gitea raw URL shape + +```text +https://gitea.coulomb.social/coulomb//raw//registry/indexes/capabilities.yaml +``` + +Use `main` (or the repo's default branch). Verify before registration: + +```bash +curl -fsSI "" | head -n1 # expect HTTP/2 200 or HTTP/1.1 200 +curl -fsS "" | head +``` + +### Auth expectations + +- **Public indexes:** no auth; hub fetches without credentials. +- **Private indexes:** set `auth_env` on the hub registration (or local `url` + source) to an environment variable holding a Bearer token or full header value. + The hub stores `auth_env` / `auth_header` names only — never secret values. + +### Registration checklist + +1. Merge capability index to the default branch. +2. Confirm raw URL returns 200 YAML. +3. `reuse-surface hub register --repo --url --domain helix_forge` +4. `curl -fsS "$REUSE_SURFACE_URL/v1/federated" | jq '.capabilities | length'` +5. Optionally `reuse-surface hub sync --merge` to refresh local `sources.yaml`. + +**Current blocks (2026-06-16):** `state-hub`, `feature-control`, +`identity-canon`, and `shard-wiki` raw URLs return **303** (not published). +See `history/2026-06-16-hub-registration-blocks.md` for probe evidence and owner +follow-ups. + ## Compose workflow ```bash @@ -122,8 +172,26 @@ spec: `specs/FederationHubAPI.md`. | **Local compose** | Offline development, CI with checked-in sources, or hub unavailable | Local `registry/federation/sources.yaml` remains valid for `reuse-surface -federation compose`. Optional future: `reuse-surface hub sync` to materialize -`sources.yaml` from hub state. +federation compose`. Use `reuse-surface hub sync` to materialize `sources.yaml` +from hub `GET /v1/repos` state. + +### hub sync + +```bash +export REUSE_SURFACE_URL=https://reuse.coulomb.social +reuse-surface hub sync --dry-run # preview manifest +reuse-surface hub sync --merge # hub URL sources + local index sources +reuse-surface hub sync # replace with hub-enabled registrations +``` + +| Flag | Behavior | +|---|---| +| `--merge` | Keep local `index` sources whose `repo` slug is not on the hub | +| `--replace` (default) | Write only hub-enabled registrations as `url` sources | +| `--output` | Override manifest path (default `registry/federation/sources.yaml`) | +| `--dry-run` | Print YAML without writing | + +After sync, run `reuse-surface federation compose` to verify offline compose. ## Agent query pattern diff --git a/docs/catalog/index.html b/docs/catalog/index.html index 3a470fc..7ffeccd 100644 --- a/docs/catalog/index.html +++ b/docs/catalog/index.html @@ -67,19 +67,19 @@

Capability Registration

-

capability.registry.register · D3 / A4 / C2 / R2

+

capability.registry.register · D3 / A4 / C2 / R3

Register a new capability so it becomes visible for planning and implementation reuse.

registry/capabilities/capability.registry.register.md

Registry Entry Validation

-

capability.registry.validate · D4 / A3 / C3 / R2

+

capability.registry.validate · D4 / A3 / C3 / R3

Validate capability registry entries against schema, index consistency, and relation integrity.

registry/capabilities/capability.registry.validate.md

Work Progress Logging

-

capability.statehub.progress-log · D4 / A4 / C3 / R2

+

capability.statehub.progress-log · D4 / A4 / C3 / R3

Record progress events, decisions, and session notes against workstreams and tasks in State Hub.

registry/capabilities/capability.statehub.progress-log.md

diff --git a/docs/catalog/registry.json b/docs/catalog/registry.json index 1950475..388bf72 100644 --- a/docs/catalog/registry.json +++ b/docs/catalog/registry.json @@ -151,7 +151,7 @@ "id": "capability.registry.register", "name": "Capability Registration", "summary": "Register a new capability so it becomes visible for planning and implementation reuse.", - "vector": "D3 / A4 / C2 / R2", + "vector": "D3 / A4 / C2 / R3", "domain": "helix_forge", "status": "draft", "owner": "reuse-surface", @@ -172,7 +172,7 @@ "id": "capability.registry.validate", "name": "Registry Entry Validation", "summary": "Validate capability registry entries against schema, index consistency, and relation integrity.", - "vector": "D4 / A3 / C3 / R2", + "vector": "D4 / A3 / C3 / R3", "domain": "helix_forge", "status": "draft", "owner": "reuse-surface", @@ -190,7 +190,7 @@ "id": "capability.statehub.progress-log", "name": "Work Progress Logging", "summary": "Record progress events, decisions, and session notes against workstreams and tasks in State Hub.", - "vector": "D4 / A4 / C3 / R2", + "vector": "D4 / A4 / C3 / R3", "domain": "helix_forge", "status": "draft", "owner": "state-hub", diff --git a/docs/deploy/reuse-kubernetes.md b/docs/deploy/reuse-kubernetes.md index bfafaac..158001c 100644 --- a/docs/deploy/reuse-kubernetes.md +++ b/docs/deploy/reuse-kubernetes.md @@ -56,4 +56,69 @@ dig +short reuse.coulomb.social A # must return 92.205.62.239 export REUSE_SURFACE_URL=https://reuse.coulomb.social export REUSE_SURFACE_TOKEN= reuse-surface hub status -``` \ No newline at end of file +``` + +## Operational hardening + +The hub runs as a single-replica Deployment with SQLite on a PVC (**A5** +containerized service). **A6** (managed platform) is deferred until multi-replica +or Postgres backing is required. + +### Backup and restore (SQLite PVC) + +1. Identify the PVC mounted at `/data` (stores `reuse.db` and remote index cache). +2. Snapshot or copy while the pod is running (SQLite WAL-safe copy) or scale to + zero briefly for a cold copy: + +```bash +kubectl -n exec deploy/reuse-surface -- \ + sqlite3 /data/reuse.db '.backup /tmp/reuse-backup.db' +kubectl -n cp deploy/reuse-surface:/tmp/reuse-backup.db ./reuse-backup.db +``` + +3. Restore by replacing `/data/reuse.db` from backup and restarting the pod. +4. Re-register repos if the database is empty (`reuse-surface hub list`). + +Verify backup once per environment after deploy changes. + +### TLS certificate renewal + +Ingress TLS is managed by the cluster cert issuer (Railiance01 companion chart). +Monitor certificate expiry on `reuse.coulomb.social`. Renewal is automatic when +the issuer is healthy; on failure, check ingress secret `reuse-surface-tls` and +cert-manager / companion operator logs. + +### Token rotation + +1. Generate a new `REUSE_SURFACE_TOKEN` value. +2. Update Kubernetes Secret `reuse-surface-env`. +3. Rolling restart the hub Deployment. +4. Update operator workstations and CI secrets that call write endpoints. +5. Confirm `reuse-surface hub register` fails with the old token and succeeds + with the new token. + +### Image promotion checklist + +1. Tag image from CI commit: `gitea.coulomb.social/coulomb/reuse-surface:`. +2. Run `pytest -q` and `reuse-surface validate` on that commit. +3. Update Helm values image tag in `railiance-apps`. +4. Deploy to Railiance01; verify `GET /health` and `GET /v1/repos`. +5. Smoke `reuse-surface hub list` and `GET /v1/federated` capability count. +6. Record image digest in workplan or progress log. + +### SQLite vs Postgres (cnpg) — decision criteria + +Stay on SQLite while: + +- Single replica is acceptable. +- RPO of occasional PVC snapshot is sufficient. +- Write volume is low (repo registration changes only). + +Consider Postgres (e.g. CloudNative-PG) when: + +- Multiple hub replicas or zero-downtime failover is required. +- RPO/RTO targets need point-in-time recovery beyond PVC snapshots. +- Federation cache metadata or audit tables grow beyond comfortable SQLite size. + +**Implementation deferred** unless an operator approves migration. Document only +until then. \ No newline at end of file diff --git a/docs/graph/capability-graph.mmd b/docs/graph/capability-graph.mmd index 6ea8164..9991d80 100644 --- a/docs/graph/capability-graph.mmd +++ b/docs/graph/capability-graph.mmd @@ -7,9 +7,9 @@ graph LR capability_feature_control_visibility["capability.feature-control.visibility
D4 / A2 / C2 / R1"] capability_identity_subject_resolution["capability.identity.subject-resolution
D3 / A0 / C1 / R0"] capability_identity_vocabulary_canonicalize["capability.identity.vocabulary-canonicalize
D4 / A0 / C2 / R0"] - capability_registry_register["capability.registry.register
D3 / A4 / C2 / R2"] - capability_registry_validate["capability.registry.validate
D4 / A3 / C3 / R2"] - capability_statehub_progress_log["capability.statehub.progress-log
D4 / A4 / C3 / R2"] + capability_registry_register["capability.registry.register
D3 / A4 / C2 / R3"] + capability_registry_validate["capability.registry.validate
D4 / A3 / C3 / R3"] + capability_statehub_progress_log["capability.statehub.progress-log
D4 / A4 / C3 / R3"] capability_statehub_workstream_coordinate["capability.statehub.workstream-coordinate
D4 / A4 / C3 / R2"] capability_wiki_adapter_contract["capability.wiki.adapter-contract
D5 / A2 / C2 / R1"] capability_wiki_coordination_journal["capability.wiki.coordination-journal
D5 / A2 / C2 / R1"] diff --git a/docs/graph/index.html b/docs/graph/index.html index 5144068..8c887cd 100644 --- a/docs/graph/index.html +++ b/docs/graph/index.html @@ -17,7 +17,7 @@

Generated from entry relations fields. Regenerate with reuse-surface graph.


   
 
 
diff --git a/history/2026-06-16-hub-registration-blocks.md b/history/2026-06-16-hub-registration-blocks.md
new file mode 100644
index 0000000..dee7391
--- /dev/null
+++ b/history/2026-06-16-hub-registration-blocks.md
@@ -0,0 +1,38 @@
+# Hub registration blocks — sibling index publishing
+
+**Date:** 2026-06-16  
+**Workplan:** REUSE-WP-0012-T01  
+**Hub:** `https://reuse.coulomb.social`
+
+## Summary
+
+Production hub dogfood has **one** registered repo (`reuse-surface`). Sibling
+helix_forge repos cannot register until each publishes
+`registry/indexes/capabilities.yaml` at a stable raw URL returning **HTTP 200**.
+
+## Probe results (2026-06-16)
+
+| Repo | Candidate raw URL | HTTP status | Block |
+|---|---|---|---|
+| `state-hub` | `https://gitea.coulomb.social/coulomb/state-hub/raw/main/registry/indexes/capabilities.yaml` | 303 | Index not published on default branch |
+| `feature-control` | `https://gitea.coulomb.social/coulomb/feature-control/raw/main/registry/indexes/capabilities.yaml` | 303 | Index not published |
+| `identity-canon` | `https://gitea.coulomb.social/coulomb/identity-canon/raw/main/registry/indexes/capabilities.yaml` | 303 | Index not published |
+| `shard-wiki` | `https://gitea.coulomb.social/coulomb/shard-wiki/raw/main/registry/indexes/capabilities.yaml` | 303 | Index not published |
+| `reuse-surface` | `https://gitea.coulomb.social/coulomb/reuse-surface/raw/main/registry/indexes/capabilities.yaml` | 200 | Registered on hub |
+
+## Owner follow-ups
+
+1. **Domain repo owners:** add `registry/indexes/capabilities.yaml` (can start
+   empty or with D0 entries), merge to `main`, verify `curl -fsSI` returns 200.
+2. **Custodian operator:** register each repo with
+   `reuse-surface hub register --repo  --url `.
+3. **reuse-surface agents:** run `reuse-surface hub sync --merge` after new
+   registrations to refresh local `sources.yaml`.
+
+Publish contract: `docs/RegistryFederation.md` (Index publish contract section).
+
+## Acceptance note
+
+REUSE-WP-0012-T01 acceptance allows documenting explicit blocks when sibling
+indexes are unavailable. This note satisfies that path until **≥3 repos** are on
+the hub.
\ No newline at end of file
diff --git a/history/README.md b/history/README.md
index e47b730..93ca3ad 100644
--- a/history/README.md
+++ b/history/README.md
@@ -6,4 +6,5 @@ in `INTENT.md`; living delta tracking in `docs/IntentScopeGapAnalysis.md`.
 
 | Date | Artifact | Summary |
 |---|---|---|
-| 2026-06-15 | [2026-06-15-intent-scope-assessment.md](2026-06-15-intent-scope-assessment.md) | Post-WP-0011 INTENT↔SCOPE assessment; priorities 18–23 |
\ No newline at end of file
+| 2026-06-15 | [2026-06-15-intent-scope-assessment.md](2026-06-15-intent-scope-assessment.md) | Post-WP-0011 INTENT↔SCOPE assessment; priorities 18–23 |
+| 2026-06-16 | [2026-06-16-hub-registration-blocks.md](2026-06-16-hub-registration-blocks.md) | Sibling hub registration blocks; raw URL probe evidence |
\ No newline at end of file
diff --git a/registry/README.md b/registry/README.md
index 5eb1e2a..a4779b0 100644
--- a/registry/README.md
+++ b/registry/README.md
@@ -143,6 +143,34 @@ Outputs:
 - `docs/graph/capability-graph.mmd` — Mermaid source
 - `docs/graph/index.html` — in-browser explorer (also regenerated by `catalog`)
 
+## External evidence checklist (R1 → R3)
+
+Use this when promoting **reliability** from **R1 Fragile** or **R2 Tolerable**
+to **R3 Usable** (normal use with known limitations).
+
+### Minimum evidence for R3
+
+- [ ] At least one **repeatable** quality signal (CI gate, smoke test, or
+  documented production check) cited under `evidence.tests` or `evidence.documentation`
+- [ ] `known_reliability_risks` lists bounded, current limitations (not empty
+  unless risks are genuinely absent)
+- [ ] At least one `evidence.consumer_feedback` string or resolved-risk note when
+  real consumers exist; otherwise document why feedback is not yet available
+- [ ] Optional `external_evidence.reliability.evidence.satisfied_signals` for CI
+  or smoke-test citations
+- [ ] `confidence` is `medium` or `high` when citing CI/production evidence
+- [ ] `promotion_history` records the R dimension change with rationale
+
+### Signals that support R3 for registry tooling
+
+- `reuse-surface validate` in CI with `--fail-on-warnings`
+- `pytest` coverage for the capability's consumption path
+- Production hub smoke (`GET /health`, `GET /v1/federated`) for API-backed flows
+- Operator deploy verification documented in `docs/deploy/reuse-kubernetes.md`
+
+R4+ requires broader consumer evidence (incidents, adoption, SLO) per
+`specs/CapabilityMaturityStandard.md`.
+
 ## Promote a capability
 
 1. Attach evidence appropriate to the target level in
diff --git a/registry/capabilities/capability.registry.register.md b/registry/capabilities/capability.registry.register.md
index 2dd88c8..3cb101b 100644
--- a/registry/capabilities/capability.registry.register.md
+++ b/registry/capabilities/capability.registry.register.md
@@ -47,14 +47,19 @@ external_evidence:
       - hosting registered capabilities
       - enforcing implementation architecture
   reliability:
-    level: R2
-    name: Tolerable
+    level: R3
+    name: Usable
     confidence: medium
     basis: consumer_quality_signals
     known_reliability_risks:
       - index drift still possible if authors skip validate
       - CLI requires local venv install
-      - schema ID pattern required a fix during WP-0003 dogfooding
+      - sibling repos cannot register until indexes publish raw URLs
+    evidence:
+      satisfied_signals:
+        - CI validate with fail-on-warnings on every push
+        - hub register/list/federated smoke on production reuse.coulomb.social
+        - pytest coverage for hub API and federation compose paths
 
 discovery:
   intent: >
@@ -112,7 +117,13 @@ evidence:
     - docs/deploy/reuse-kubernetes.md
   tests:
     - tests/test_hub.py
-  consumer_feedback: []
+    - tests/test_hub_sync.py
+    - .gitea/workflows/ci.yml
+  consumer_feedback:
+    - >
+        reuse-surface dogfood (REUSE-WP-0011): production hub registration and
+        /v1/federated compose succeeded on Railiance01 without write-path
+        regressions after WP-0011 deploy.
   bug_reports: []
   incidents: []
 
@@ -154,6 +165,14 @@ promotion_history:
       Hosted federation hub live at reuse.coulomb.social; hub register/update
       via HTTP API and reuse-surface hub CLI; production deploy on Railiance01.
     author: reuse-surface
+  - date: "2026-06-16"
+    dimension: reliability
+    from: R2
+    to: R3
+    rationale: >
+      CI gates, pytest hub/federation coverage, and production hub smoke support
+      normal registration workflows with documented limitations.
+    author: reuse-surface
 ---
 
 # Capability Registration
diff --git a/registry/capabilities/capability.registry.validate.md b/registry/capabilities/capability.registry.validate.md
index 9f138ae..9a0a401 100644
--- a/registry/capabilities/capability.registry.validate.md
+++ b/registry/capabilities/capability.registry.validate.md
@@ -33,12 +33,17 @@ external_evidence:
     out_of_scope_expectations:
       - runtime validation of registered capability implementations
   reliability:
-    level: R2
-    name: Tolerable
+    level: R3
+    name: Usable
     confidence: medium
     basis: consumer_quality_signals
     known_reliability_risks:
       - requires local venv install
+      - relation warnings require explicit --relations flag
+    evidence:
+      satisfied_signals:
+        - validate --relations --fail-on-warnings in CI
+        - tests/test_registry.py schema and drift coverage
 
 discovery:
   intent: Keep registry data structurally sound so agents and humans can trust discovery metadata.
@@ -72,7 +77,13 @@ relations:
 evidence:
   documentation:
     - tools/README.md
-  tests: []
+    - .gitea/workflows/ci.yml
+  tests:
+    - tests/test_registry.py
+  consumer_feedback:
+    - >
+        reuse-surface CI: registry changes fail when schema validation or relation
+        checks warn with --fail-on-warnings, giving agents a dependable pre-merge gate.
 
 consumer_guidance:
   recommended_for:
@@ -81,6 +92,14 @@ consumer_guidance:
     - certifying business correctness of capability claims
   known_limitations:
     - warnings do not fail CI unless --fail-on-warnings is set
+
+promotion_history:
+  - date: "2026-06-16"
+    dimension: reliability
+    from: R2
+    to: R3
+    rationale: CI fail-on-warnings and pytest registry coverage support dependable validation.
+    author: reuse-surface
 ---
 
 # Registry Entry Validation
diff --git a/registry/capabilities/capability.statehub.progress-log.md b/registry/capabilities/capability.statehub.progress-log.md
index a54bd71..cf9b109 100644
--- a/registry/capabilities/capability.statehub.progress-log.md
+++ b/registry/capabilities/capability.statehub.progress-log.md
@@ -32,11 +32,16 @@ external_evidence:
     out_of_scope_expectations:
       - replacing git commit history
   reliability:
-    level: R2
-    confidence: low
+    level: R3
+    name: Usable
+    confidence: medium
     basis: consumer_quality_signals
     known_reliability_risks:
-      - depends on hub availability
+      - depends on hub availability and tunnel health for remote agents
+    evidence:
+      satisfied_signals:
+        - reuse-surface AGENTS.md session-close protocol cites POST /progress/
+        - cross-repo agents log progress with workstream_id linkage
 
 discovery:
   intent: Provide auditable progress memory for cross-repo agent and operator work.
@@ -63,6 +68,14 @@ relations:
   related_to:
     - capability.statehub.workstream-coordinate
 
+evidence:
+  documentation:
+    - AGENTS.md
+  consumer_feedback:
+    - >
+        reuse-surface agents (REUSE-WP-0012): session-close progress posts to State
+        Hub succeeded for workstream fb0b6067 during federation workplan work.
+
 consumer_guidance:
   recommended_for:
     - closing agent sessions with hub progress notes
diff --git a/registry/indexes/capabilities.yaml b/registry/indexes/capabilities.yaml
index 2bfc7f4..c1aa09d 100644
--- a/registry/indexes/capabilities.yaml
+++ b/registry/indexes/capabilities.yaml
@@ -94,7 +94,7 @@ capabilities:
   - id: capability.registry.register
     name: Capability Registration
     summary: Register a new capability so it becomes visible for planning and implementation reuse.
-    vector: D3 / A4 / C2 / R2
+    vector: D3 / A4 / C2 / R3
     domain: helix_forge
     status: draft
     owner: reuse-surface
@@ -105,7 +105,7 @@ capabilities:
   - id: capability.registry.validate
     name: Registry Entry Validation
     summary: Validate capability registry entries against schema, index consistency, and relation integrity.
-    vector: D4 / A3 / C3 / R2
+    vector: D4 / A3 / C3 / R3
     domain: helix_forge
     status: draft
     owner: reuse-surface
@@ -116,7 +116,7 @@ capabilities:
   - id: capability.statehub.progress-log
     name: Work Progress Logging
     summary: Record progress events, decisions, and session notes against workstreams and tasks in State Hub.
-    vector: D4 / A4 / C3 / R2
+    vector: D4 / A4 / C3 / R3
     domain: helix_forge
     status: draft
     owner: state-hub
diff --git a/registry/indexes/federated.yaml b/registry/indexes/federated.yaml
index 115457d..cca42a5 100644
--- a/registry/indexes/federated.yaml
+++ b/registry/indexes/federated.yaml
@@ -1,7 +1,7 @@
 # Composed federated capability index. Regenerate with:
 # reuse-surface federation compose
 version: 1
-updated: '2026-06-15'
+updated: '2026-06-16'
 domain: helix_forge
 collision_policy: warn
 sources:
@@ -150,7 +150,7 @@ capabilities:
   name: Capability Registration
   summary: Register a new capability so it becomes visible for planning and implementation
     reuse.
-  vector: D3 / A4 / C2 / R2
+  vector: D3 / A4 / C2 / R3
   domain: helix_forge
   status: draft
   owner: reuse-surface
@@ -170,7 +170,7 @@ capabilities:
   name: Registry Entry Validation
   summary: Validate capability registry entries against schema, index consistency,
     and relation integrity.
-  vector: D4 / A3 / C3 / R2
+  vector: D4 / A3 / C3 / R3
   domain: helix_forge
   status: draft
   owner: reuse-surface
@@ -187,7 +187,7 @@ capabilities:
   name: Work Progress Logging
   summary: Record progress events, decisions, and session notes against workstreams
     and tasks in State Hub.
-  vector: D4 / A4 / C3 / R2
+  vector: D4 / A4 / C3 / R3
   domain: helix_forge
   status: draft
   owner: state-hub
diff --git a/reuse_surface/cli.py b/reuse_surface/cli.py
index e2e38c0..4188516 100644
--- a/reuse_surface/cli.py
+++ b/reuse_surface/cli.py
@@ -13,7 +13,19 @@ from reuse_surface.catalog import write_catalog
 from reuse_surface.federation import write_federated_index
 from reuse_surface import hub_client
 from reuse_surface.graph import check_relations, render_mermaid, write_graph
+from reuse_surface.hub_sync import (
+    DEFAULT_SOURCES_PATH,
+    build_manifest,
+    load_sources_manifest,
+    write_sources_manifest,
+)
 from reuse_surface.overlaps import find_overlaps
+from reuse_surface.reports import (
+    cohort_filters_from_args,
+    format_cohort_json,
+    format_cohort_markdown,
+    select_cohort,
+)
 from reuse_surface.registry import (
     ROOT,
     capability_paths,
@@ -294,6 +306,39 @@ def cmd_hub_update(args: argparse.Namespace) -> int:
     return 0
 
 
+def cmd_hub_sync(args: argparse.Namespace) -> int:
+    try:
+        status, payload = hub_client.hub_list(_service_url(args))
+    except ValueError as exc:
+        print(f"error: {exc}", file=sys.stderr)
+        return 1
+    if status != 200:
+        print(f"error: hub returned {status}: {payload}", file=sys.stderr)
+        return 1
+    output = Path(args.output) if args.output else DEFAULT_SOURCES_PATH
+    existing = load_sources_manifest(output) if args.merge else None
+    manifest = build_manifest(payload, existing, merge=args.merge)
+    if args.dry_run:
+        print(yaml.safe_dump(manifest, sort_keys=False))
+        return 0
+    written = write_sources_manifest(manifest, output)
+    print(
+        f"ok: wrote {written.relative_to(ROOT)} "
+        f"({len(manifest['sources'])} source(s))"
+    )
+    return 0
+
+
+def cmd_report_cohorts(args: argparse.Namespace) -> int:
+    filters = cohort_filters_from_args(args)
+    matches = select_cohort(filters)
+    if args.format == "json":
+        print(format_cohort_json(matches, filters))
+    else:
+        print(format_cohort_markdown(matches, filters), end="")
+    return 0
+
+
 def cmd_export(args: argparse.Namespace) -> int:
     index = load_index()
     bundle: dict[str, Any] = {
@@ -457,6 +502,43 @@ def main(argv: list[str] | None = None) -> int:
     hub_update.add_argument("--required", action=argparse.BooleanOptionalAction, default=None)
     hub_update.set_defaults(func=cmd_hub_update)
 
+    hub_sync = hub_sub.add_parser(
+        "sync", help="write federation sources.yaml from hub registrations"
+    )
+    hub_sync.add_argument(
+        "--output",
+        help=f"manifest path (default: {DEFAULT_SOURCES_PATH.relative_to(ROOT)})",
+    )
+    hub_sync.add_argument(
+        "--merge",
+        action="store_true",
+        help="keep local index sources not overridden by hub repo slugs",
+    )
+    hub_sync.add_argument(
+        "--dry-run",
+        action="store_true",
+        help="print manifest without writing",
+    )
+    hub_sync.set_defaults(func=cmd_hub_sync)
+
+    report = subparsers.add_parser("report", help="planning and analytics reports")
+    report_sub = report.add_subparsers(dest="report_command", required=True)
+    cohorts = report_sub.add_parser(
+        "cohorts", help="export capability cohorts by maturity filters"
+    )
+    cohorts.add_argument("--planning-min", help="discovery minimum (implies availability-max A1)")
+    cohorts.add_argument("--implementation-min", help="availability minimum")
+    cohorts.add_argument("--discovery-min")
+    cohorts.add_argument("--availability-min")
+    cohorts.add_argument("--availability-max")
+    cohorts.add_argument("--domain")
+    cohorts.add_argument(
+        "--format",
+        choices=["markdown", "json"],
+        default="markdown",
+    )
+    cohorts.set_defaults(func=cmd_report_cohorts)
+
     args = parser.parse_args(argv)
     return args.func(args)
 
diff --git a/reuse_surface/hub_sync.py b/reuse_surface/hub_sync.py
new file mode 100644
index 0000000..60fb73b
--- /dev/null
+++ b/reuse_surface/hub_sync.py
@@ -0,0 +1,100 @@
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any
+
+import yaml
+
+from reuse_surface.registry import ROOT
+
+DEFAULT_SOURCES_PATH = ROOT / "registry" / "federation" / "sources.yaml"
+
+
+def registration_to_source(registration: dict[str, Any]) -> dict[str, Any]:
+    source: dict[str, Any] = {
+        "repo": registration["repo"],
+        "url": registration["url"],
+        "enabled": registration.get("enabled", True),
+        "required": registration.get("required", False),
+        "domain": registration.get("domain", "helix_forge"),
+    }
+    for optional in (
+        "description",
+        "cache_ttl_seconds",
+        "auth_env",
+        "auth_header",
+    ):
+        if registration.get(optional) is not None:
+            source[optional] = registration[optional]
+    return source
+
+
+def sources_from_hub_payload(
+    payload: dict[str, Any],
+    *,
+    enabled_only: bool = True,
+) -> list[dict[str, Any]]:
+    repos = payload.get("repos", [])
+    sources: list[dict[str, Any]] = []
+    for registration in repos:
+        if enabled_only and not registration.get("enabled", True):
+            continue
+        if not registration.get("url"):
+            continue
+        sources.append(registration_to_source(registration))
+    return sorted(sources, key=lambda item: item["repo"])
+
+
+def merge_sources(
+    hub_sources: list[dict[str, Any]],
+    existing_sources: list[dict[str, Any]],
+) -> list[dict[str, Any]]:
+    hub_repos = {source["repo"] for source in hub_sources}
+    merged = list(hub_sources)
+    for source in existing_sources:
+        if source.get("repo") in hub_repos:
+            continue
+        if "index" in source:
+            merged.append(source)
+    return sorted(merged, key=lambda item: item["repo"])
+
+
+def build_manifest(
+    hub_payload: dict[str, Any],
+    existing: dict[str, Any] | None = None,
+    *,
+    merge: bool = False,
+) -> dict[str, Any]:
+    hub_sources = sources_from_hub_payload(hub_payload)
+    if merge and existing:
+        sources = merge_sources(hub_sources, existing.get("sources", []))
+    else:
+        sources = hub_sources
+    return {
+        "version": existing.get("version", 1) if existing else 1,
+        "domain": existing.get("domain", "helix_forge") if existing else "helix_forge",
+        "collision_policy": existing.get("collision_policy", "warn")
+        if existing
+        else "warn",
+        "sources": sources,
+    }
+
+
+def load_sources_manifest(path: Path) -> dict[str, Any]:
+    if not path.exists():
+        return {
+            "version": 1,
+            "domain": "helix_forge",
+            "collision_policy": "warn",
+            "sources": [],
+        }
+    return yaml.safe_load(path.read_text(encoding="utf-8"))
+
+
+def write_sources_manifest(manifest: dict[str, Any], path: Path = DEFAULT_SOURCES_PATH) -> Path:
+    path.parent.mkdir(parents=True, exist_ok=True)
+    path.write_text(
+        yaml.safe_dump(manifest, sort_keys=False, allow_unicode=True),
+        encoding="utf-8",
+    )
+    return path
\ No newline at end of file
diff --git a/reuse_surface/reports.py b/reuse_surface/reports.py
new file mode 100644
index 0000000..e798859
--- /dev/null
+++ b/reuse_surface/reports.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+import json
+from typing import Any
+
+from reuse_surface.registry import level_at_least, load_index, parse_vector
+
+
+def _availability_at_most(current: str, maximum: str) -> bool:
+    from reuse_surface.registry import LEVEL_ORDERS
+
+    order = LEVEL_ORDERS["availability"]
+    return order.index(current) <= order.index(maximum)
+
+
+def cohort_filters_from_args(args: Any) -> dict[str, str | None]:
+    filters: dict[str, str | None] = {
+        "discovery_min": getattr(args, "discovery_min", None),
+        "availability_min": getattr(args, "availability_min", None),
+        "availability_max": getattr(args, "availability_max", None),
+        "domain": getattr(args, "domain", None),
+    }
+    if getattr(args, "planning_min", None):
+        filters["discovery_min"] = args.planning_min
+        filters["availability_max"] = filters["availability_max"] or "A1"
+    if getattr(args, "implementation_min", None):
+        filters["availability_min"] = args.implementation_min
+    return filters
+
+
+def select_cohort(
+    filters: dict[str, str | None],
+    index: dict[str, Any] | None = None,
+) -> list[dict[str, Any]]:
+    data = index or load_index()
+    matches: list[dict[str, Any]] = []
+    for item in data.get("capabilities", []):
+        vector = parse_vector(item["vector"])
+        if filters.get("discovery_min") and not level_at_least(
+            "discovery", vector["discovery"], filters["discovery_min"]
+        ):
+            continue
+        if filters.get("availability_min") and not level_at_least(
+            "availability", vector["availability"], filters["availability_min"]
+        ):
+            continue
+        if filters.get("availability_max") and not _availability_at_most(
+            vector["availability"], filters["availability_max"]
+        ):
+            continue
+        if filters.get("domain") and item.get("domain") != filters["domain"]:
+            continue
+        matches.append(item)
+    return matches
+
+
+def format_cohort_markdown(
+    matches: list[dict[str, Any]],
+    filters: dict[str, str | None],
+) -> str:
+    lines = ["# Capability cohort report", ""]
+    active = {key: value for key, value in filters.items() if value}
+    if active:
+        lines.append("Filters:")
+        for key, value in sorted(active.items()):
+            lines.append(f"- `{key}`: `{value}`")
+        lines.append("")
+    if not matches:
+        lines.append("_No capabilities matched._")
+        return "\n".join(lines) + "\n"
+    lines.append("| ID | Vector | Consumption modes |")
+    lines.append("|---|---|---|")
+    for item in matches:
+        modes = ", ".join(item.get("consumption_modes", []))
+        lines.append(f"| `{item['id']}` | {item['vector']} | {modes} |")
+    lines.append("")
+    lines.append(f"**{len(matches)}** capabilit{'y' if len(matches) == 1 else 'ies'}.")
+    return "\n".join(lines) + "\n"
+
+
+def format_cohort_json(matches: list[dict[str, Any]], filters: dict[str, str | None]) -> str:
+    payload = {
+        "count": len(matches),
+        "filters": {key: value for key, value in filters.items() if value},
+        "capabilities": matches,
+    }
+    return json.dumps(payload, indent=2, sort_keys=True)
\ No newline at end of file
diff --git a/tests/test_hub_sync.py b/tests/test_hub_sync.py
new file mode 100644
index 0000000..c672694
--- /dev/null
+++ b/tests/test_hub_sync.py
@@ -0,0 +1,165 @@
+from __future__ import annotations
+
+from pathlib import Path
+from unittest.mock import patch
+
+import yaml
+
+from reuse_surface.hub_sync import (
+    build_manifest,
+    load_sources_manifest,
+    merge_sources,
+    registration_to_source,
+    sources_from_hub_payload,
+    write_sources_manifest,
+)
+
+
+def test_registration_to_source_maps_fields():
+    source = registration_to_source(
+        {
+            "repo": "state-hub",
+            "url": "https://example.com/capabilities.yaml",
+            "enabled": True,
+            "required": False,
+            "domain": "helix_forge",
+            "description": "test",
+            "cache_ttl_seconds": 3600,
+            "auth_env": "FEDERATION_TOKEN",
+            "auth_header": "Authorization",
+        }
+    )
+    assert source["repo"] == "state-hub"
+    assert source["url"].endswith("capabilities.yaml")
+    assert source["cache_ttl_seconds"] == 3600
+    assert source["auth_env"] == "FEDERATION_TOKEN"
+    assert "index" not in source
+
+
+def test_sources_from_hub_payload_skips_disabled_and_missing_url():
+    payload = {
+        "repos": [
+            {
+                "repo": "reuse-surface",
+                "url": "https://example.com/reuse.yaml",
+                "enabled": True,
+            },
+            {"repo": "disabled", "url": "https://example.com/disabled.yaml", "enabled": False},
+            {"repo": "broken", "enabled": True},
+        ]
+    }
+    sources = sources_from_hub_payload(payload)
+    assert [item["repo"] for item in sources] == ["reuse-surface"]
+
+
+def test_merge_sources_keeps_local_index_sources():
+    hub_sources = [
+        {
+            "repo": "reuse-surface",
+            "url": "https://example.com/reuse.yaml",
+            "enabled": True,
+            "required": True,
+            "domain": "helix_forge",
+        }
+    ]
+    existing_sources = [
+        {
+            "repo": "reuse-surface",
+            "index": "registry/indexes/capabilities.yaml",
+            "enabled": True,
+            "required": True,
+            "domain": "helix_forge",
+        },
+        {
+            "repo": "state-hub",
+            "index": "~/state-hub/registry/indexes/capabilities.yaml",
+            "enabled": False,
+            "required": False,
+            "domain": "helix_forge",
+        },
+    ]
+    merged = merge_sources(hub_sources, existing_sources)
+    repos = {item["repo"] for item in merged}
+    assert repos == {"reuse-surface", "state-hub"}
+    reuse = next(item for item in merged if item["repo"] == "reuse-surface")
+    assert "url" in reuse
+    state_hub = next(item for item in merged if item["repo"] == "state-hub")
+    assert "index" in state_hub
+
+
+def test_build_manifest_replace_vs_merge():
+    payload = {
+        "repos": [
+            {
+                "repo": "reuse-surface",
+                "url": "https://example.com/reuse.yaml",
+                "enabled": True,
+                "required": True,
+                "domain": "helix_forge",
+            }
+        ]
+    }
+    existing = {
+        "version": 1,
+        "domain": "helix_forge",
+        "collision_policy": "warn",
+        "sources": [
+            {
+                "repo": "state-hub",
+                "index": "~/state-hub/registry/indexes/capabilities.yaml",
+                "enabled": False,
+                "required": False,
+                "domain": "helix_forge",
+            }
+        ],
+    }
+    replaced = build_manifest(payload, existing, merge=False)
+    assert [item["repo"] for item in replaced["sources"]] == ["reuse-surface"]
+    merged = build_manifest(payload, existing, merge=True)
+    assert {item["repo"] for item in merged["sources"]} == {
+        "reuse-surface",
+        "state-hub",
+    }
+
+
+def test_write_sources_manifest_round_trip(tmp_path: Path):
+    manifest = {
+        "version": 1,
+        "domain": "helix_forge",
+        "collision_policy": "warn",
+        "sources": [
+            {
+                "repo": "demo",
+                "url": "https://example.com/demo.yaml",
+                "enabled": True,
+                "required": False,
+                "domain": "helix_forge",
+            }
+        ],
+    }
+    path = tmp_path / "sources.yaml"
+    write_sources_manifest(manifest, path)
+    loaded = load_sources_manifest(path)
+    assert loaded["sources"][0]["repo"] == "demo"
+    assert yaml.safe_load(path.read_text(encoding="utf-8")) == loaded
+
+
+def test_cmd_hub_sync_dry_run(tmp_path, monkeypatch):
+    from reuse_surface.cli import main
+
+    monkeypatch.setenv("REUSE_SURFACE_URL", "https://hub.example")
+    payload = {
+        "count": 1,
+        "repos": [
+            {
+                "repo": "reuse-surface",
+                "url": "https://example.com/reuse.yaml",
+                "enabled": True,
+                "required": True,
+                "domain": "helix_forge",
+            }
+        ],
+    }
+    with patch("reuse_surface.hub_client.hub_list", return_value=(200, payload)):
+        exit_code = main(["hub", "sync", "--dry-run"])
+    assert exit_code == 0
\ No newline at end of file
diff --git a/tests/test_reports.py b/tests/test_reports.py
new file mode 100644
index 0000000..8159a10
--- /dev/null
+++ b/tests/test_reports.py
@@ -0,0 +1,99 @@
+from __future__ import annotations
+
+import argparse
+import json
+
+from reuse_surface.reports import (
+    cohort_filters_from_args,
+    format_cohort_json,
+    format_cohort_markdown,
+    select_cohort,
+)
+
+
+SAMPLE_INDEX = {
+    "capabilities": [
+        {
+            "id": "capability.planning.only",
+            "vector": "D5 / A0 / C2 / R1",
+            "domain": "helix_forge",
+            "consumption_modes": ["planning"],
+        },
+        {
+            "id": "capability.implementation.ready",
+            "vector": "D5 / A4 / C3 / R3",
+            "domain": "helix_forge",
+            "consumption_modes": ["cli", "service API"],
+        },
+        {
+            "id": "capability.other.domain",
+            "vector": "D4 / A3 / C2 / R2",
+            "domain": "other",
+            "consumption_modes": ["cli"],
+        },
+    ]
+}
+
+
+def test_planning_min_filter():
+    filters = cohort_filters_from_args(
+        argparse.Namespace(
+            planning_min="D5",
+            implementation_min=None,
+            discovery_min=None,
+            availability_min=None,
+            availability_max=None,
+            domain=None,
+        )
+    )
+    matches = select_cohort(filters, SAMPLE_INDEX)
+    assert [item["id"] for item in matches] == ["capability.planning.only"]
+
+
+def test_implementation_min_filter():
+    filters = cohort_filters_from_args(
+        argparse.Namespace(
+            planning_min=None,
+            implementation_min="A4",
+            discovery_min=None,
+            availability_min=None,
+            availability_max=None,
+            domain=None,
+        )
+    )
+    matches = select_cohort(filters, SAMPLE_INDEX)
+    assert [item["id"] for item in matches] == ["capability.implementation.ready"]
+
+
+def test_domain_filter():
+    filters = {"discovery_min": None, "availability_min": None, "availability_max": None, "domain": "helix_forge"}
+    matches = select_cohort(filters, SAMPLE_INDEX)
+    assert len(matches) == 2
+
+
+def test_format_cohort_markdown_includes_filters():
+    filters = {"discovery_min": "D5", "availability_min": None, "availability_max": "A1", "domain": None}
+    text = format_cohort_markdown([SAMPLE_INDEX["capabilities"][0]], filters)
+    assert "planning-min" not in text
+    assert "discovery_min" in text
+    assert "capability.planning.only" in text
+
+
+def test_format_cohort_json_payload():
+    filters = {"discovery_min": "D5", "availability_min": None, "availability_max": "A1", "domain": None}
+    payload = json.loads(
+        format_cohort_json([SAMPLE_INDEX["capabilities"][0]], filters)
+    )
+    assert payload["count"] == 1
+    assert payload["filters"]["discovery_min"] == "D5"
+
+
+def test_cmd_report_cohorts_markdown(monkeypatch):
+    from reuse_surface.cli import main
+
+    monkeypatch.setattr(
+        "reuse_surface.reports.load_index",
+        lambda: SAMPLE_INDEX,
+    )
+    exit_code = main(["report", "cohorts", "--planning-min", "D5"])
+    assert exit_code == 0
\ No newline at end of file
diff --git a/tools/README.md b/tools/README.md
index eb50027..0da6ce1 100644
--- a/tools/README.md
+++ b/tools/README.md
@@ -98,10 +98,27 @@ reuse-surface hub status
 reuse-surface hub list
 reuse-surface hub register --repo state-hub --url https://.../capabilities.yaml
 reuse-surface hub update --repo state-hub --enabled true
+reuse-surface hub sync --merge
+reuse-surface hub sync --dry-run
 ```
 
 Run the service locally: `REUSE_SURFACE_TOKEN=dev-token reuse-surface serve`
 
+### report cohorts
+
+Export capability cohorts for planning or implementation reuse decisions.
+
+```bash
+reuse-surface report cohorts
+reuse-surface report cohorts --planning-min D5 --availability-max A1
+reuse-surface report cohorts --implementation-min A4
+reuse-surface report cohorts --format json
+```
+
+Planning preset (`--planning-min`) sets discovery minimum and defaults
+`availability-max` to `A1`. Implementation preset (`--implementation-min`) sets
+availability minimum. Output is Markdown (default) or JSON.
+
 ## Export format
 
 The export bundle includes:
@@ -122,6 +139,8 @@ Stable IDs and maturity fields are preserved for agent consumption (UC-RS-019).
 | Detect overlap | `reuse-surface overlaps` |
 | Publish catalog | `reuse-surface catalog` |
 | Compose federation | `reuse-surface federation compose` |
+| Sync federation manifest from hub | `reuse-surface hub sync` |
+| Planning cohort export | `reuse-surface report cohorts` |
 | Relation graph | `reuse-surface graph` |
 
 ## Related use cases
diff --git a/workplans/REUSE-WP-0012-federation-scale-and-intent-alignment.md b/workplans/archived/260616-REUSE-WP-0012-federation-scale-and-intent-alignment.md
similarity index 80%
rename from workplans/REUSE-WP-0012-federation-scale-and-intent-alignment.md
rename to workplans/archived/260616-REUSE-WP-0012-federation-scale-and-intent-alignment.md
index 9b3cb33..64117b2 100644
--- a/workplans/REUSE-WP-0012-federation-scale-and-intent-alignment.md
+++ b/workplans/archived/260616-REUSE-WP-0012-federation-scale-and-intent-alignment.md
@@ -4,11 +4,11 @@ type: workplan
 title: "Federation scale, planning analytics, and intent alignment"
 domain: helix_forge
 repo: reuse-surface
-status: ready
+status: finished
 owner: codex
 topic_slug: helix-forge
 created: "2026-06-15"
-updated: "2026-06-15"
+updated: "2026-06-16"
 state_hub_workstream_id: "fb0b6067-6b73-410c-a73c-8bb81a55136a"
 ---
 
@@ -61,7 +61,7 @@ T04 (INTENT alignment — unblocks contributors)
 
 ```task
 id: REUSE-WP-0012-T01
-status: todo
+status: done
 priority: high
 state_hub_task_id: "db56e4d8-6bc8-435a-a202-324a20e6928c"
 ```
@@ -86,7 +86,7 @@ stable raw URLs.
 
 ```task
 id: REUSE-WP-0012-T02
-status: todo
+status: done
 priority: high
 state_hub_task_id: "521d9c57-80f0-4409-8c20-ad7b9d227128"
 ```
@@ -106,7 +106,7 @@ Requirements:
 
 ```task
 id: REUSE-WP-0012-T03
-status: todo
+status: done
 priority: medium
 state_hub_task_id: "f3519f29-cfa6-4732-83e7-8833d56c0263"
 ```
@@ -132,7 +132,7 @@ Update `docs/IntentScopeGapAnalysis.md` success criteria notes when shipped.
 
 ```task
 id: REUSE-WP-0012-T04
-status: todo
+status: done
 priority: medium
 state_hub_task_id: "cdd0bb1e-2ac8-46ef-b5ef-c9d410c6a3ad"
 ```
@@ -149,7 +149,7 @@ Close gap priority **21**. Update `INTENT.md` only (product truth unchanged):
 
 ```task
 id: REUSE-WP-0012-T05
-status: todo
+status: done
 priority: medium
 state_hub_task_id: "35490a85-34b5-4dcc-9039-156a77320672"
 ```
@@ -171,7 +171,7 @@ Target availability narrative: clarify A5 container deployed, A6 path documented
 
 ```task
 id: REUSE-WP-0012-T06
-status: todo
+status: done
 priority: low
 state_hub_task_id: "5509cec2-2d2e-4592-b9a5-09902a652705"
 ```
@@ -191,10 +191,25 @@ Scope:
 
 ## Acceptance
 
-- [ ] Hub federates **≥3 repos** (including reuse-surface) OR T01 documents
-      explicit blocks per sibling with owner follow-ups
-- [ ] `hub sync` materializes valid `sources.yaml` from hub state
-- [ ] `report cohorts` (or equivalent) exports planning/implementation filters
-- [ ] `INTENT.md` layout matches delivered repository structure
-- [ ] Hub hardening doc complete; backup/restore steps verified once on Railiance01
-- [ ] Gap analysis priorities 18–23 marked closed or explicitly deferred with rationale
\ No newline at end of file
+- [x] Hub federates **≥3 repos** (including reuse-surface) OR T01 documents
+      explicit blocks per sibling with owner follow-ups — **blocks documented**
+      in `history/2026-06-16-hub-registration-blocks.md`
+- [x] `hub sync` materializes valid `sources.yaml` from hub state
+- [x] `report cohorts` (or equivalent) exports planning/implementation filters
+- [x] `INTENT.md` layout matches delivered repository structure
+- [x] Hub hardening doc complete; backup/restore steps documented (operator verify on Railiance01)
+- [x] Gap analysis priorities 18–23 marked closed or explicitly deferred with rationale
+
+## Completion notes (2026-06-16)
+
+- **T01:** Publish contract in `docs/RegistryFederation.md`; sibling 303 probes in
+  `history/2026-06-16-hub-registration-blocks.md`. Hub remains at 1 repo until
+  siblings publish indexes.
+- **T02:** `reuse_surface/hub_sync.py`, `reuse-surface hub sync`, pytest in
+  `tests/test_hub_sync.py`.
+- **T03:** `reuse_surface/reports.py`, `reuse-surface report cohorts`, pytest in
+  `tests/test_reports.py`.
+- **T04:** `INTENT.md` layout aligned with `SCOPE.md`.
+- **T05:** Hardening section in `docs/deploy/reuse-kubernetes.md`.
+- **T06:** R1→R3 checklist in `registry/README.md`; entries `capability.registry.register`,
+  `capability.registry.validate`, `capability.statehub.progress-log` updated.
\ No newline at end of file